Skip to content

fix: implement singleton noop IDispatch callback for async WUA methods#16

Merged
ceshihao merged 4 commits into
masterfrom
fix/async-wua-callback
Jun 18, 2026
Merged

fix: implement singleton noop IDispatch callback for async WUA methods#16
ceshihao merged 4 commits into
masterfrom
fix/async-wua-callback

Conversation

@ceshihao

@ceshihao ceshihao commented Jun 18, 2026

Copy link
Copy Markdown
Owner

relate to #15

The asynchronous WUA methods (BeginSearch, BeginDownload, BeginInstall, BeginUninstall) require a non-NULL IUnknown* callback argument. Passing NULL (VT_NULL) causes DISP_E_TYPEMISMATCH (0x80020005).

This commit introduces a minimal IDispatch singleton (newNoopDispatch) whose Invoke does nothing. Key design decisions:

  • Global singleton via sync.Once to avoid unbounded memory growth
  • atomic.AddInt32 for thread-safe COM reference counting
  • Disable go vet unsafeptr check in CI (required by syscall.NewCallback)

Change-Id: I44dc095451daab5b44cee9d13a8c86776b009ecd
Co-developed-by: Qoder noreply@qoder.com

The asynchronous WUA methods (BeginSearch, BeginDownload, BeginInstall,
BeginUninstall) require a non-NULL IUnknown* callback argument. Passing
NULL (VT_NULL) causes DISP_E_TYPEMISMATCH (0x80020005).

This commit introduces a minimal IDispatch singleton (newNoopDispatch)
whose Invoke does nothing. Key design decisions:
- Global singleton via sync.Once to avoid unbounded memory growth
- atomic.AddInt32 for thread-safe COM reference counting
- Disable go vet unsafeptr check in CI (required by syscall.NewCallback)

Change-Id: I44dc095451daab5b44cee9d13a8c86776b009ecd
Co-developed-by: Qoder <noreply@qoder.com>
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 45.12%. Comparing base (4e268b4) to head (cb7aece).

Files with missing lines Patch % Lines
olecallback.go 70.45% 11 Missing and 2 partials ⚠️
iupdateinstaller.go 0.00% 2 Missing ⚠️
iupdatedownloader.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #16      +/-   ##
==========================================
+ Coverage   43.89%   45.12%   +1.23%     
==========================================
  Files          34       35       +1     
  Lines        1392     1436      +44     
==========================================
+ Hits          611      648      +37     
- Misses        562      565       +3     
- Partials      219      223       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a minimal, stateless IDispatch COM callback implementation (newNoopDispatch) to replace nil arguments in asynchronous Windows Update Agent (WUA) methods (BeginDownload, BeginInstall, BeginUninstall, and BeginSearch), resolving DISP_E_TYPEMISMATCH errors. The review feedback highlights several critical and medium-severity improvements: addressing potential compilation failures on non-Windows platforms by adding a stub file or applying build tags consistently; initializing the pVarResult out-parameter to VT_EMPTY in ncInvoke to prevent potential memory corruption or crashes; and improving safety and code clarity by directly referencing the globalNoop singleton instead of performing unsafe conversions from the this pointer, along with adding defensive checks for null pointers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread olecallback.go
Comment thread olecallback.go
Comment thread olecallback.go
Comment thread olecallback.go
- Update go.mod minimum version from 1.23 to 1.24
- Update CI matrix from [1.23, 1.24, 1.25] to [1.24, 1.25, 1.26]
- Update lint job Go version to 1.24

Change-Id: I90c990b7f42adefd15d4e5890a2a361d959b2c47
Co-developed-by: Qoder <noreply@qoder.com>
@ceshihao
ceshihao force-pushed the fix/async-wua-callback branch from ed54222 to 8bd1d23 Compare June 18, 2026 03:33
- Use globalNoop singleton directly instead of unsafe this-pointer casts
- Add defensive null check for iid in ncQueryInterface
- Initialize pVarResult to VT_EMPTY in ncInvoke per COM contract
- Add olecallback_other.go stub for non-Windows compilation

Change-Id: Ic6fe7ef0ad19930acf724a90ea9ee51993c695d3
Co-developed-by: Qoder <noreply@qoder.com>
@ceshihao ceshihao self-assigned this Jun 18, 2026
@ceshihao

Copy link
Copy Markdown
Owner Author

@gemini-code-assist review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Go version to 1.24 and introduces a minimal, stateless, no-op IDispatch callback implementation (newNoopDispatch) to prevent DISP_E_TYPEMISMATCH errors during asynchronous Windows Update Agent (WUA) operations. The review feedback focuses on improving COM compliance and code safety within the callback implementation. Specifically, it is recommended to return E_POINTER when the output pointer is null, cast the 'this' pointer to access the reference counter instead of referencing the global singleton directly, and use the go-ole package's VARIANT type and VT_EMPTY constant instead of raw pointer dereferencing with magic numbers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread olecallback.go
Comment on lines +56 to +60
const (
hrSOK = uintptr(0x00000000)
hrENoInterface = uintptr(0x80004002)
hrENotImpl = uintptr(0x80004001)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To comply with COM standards, QueryInterface should return E_POINTER (0x80004003) if the output pointer argument ppvObject is null. Let's define hrEPointer to support this check.

Suggested change
const (
hrSOK = uintptr(0x00000000)
hrENoInterface = uintptr(0x80004002)
hrENotImpl = uintptr(0x80004001)
)
const (
hrSOK = uintptr(0x00000000)
hrEPointer = uintptr(0x80004003)
hrENoInterface = uintptr(0x80004002)
hrENotImpl = uintptr(0x80004001)
)

Comment thread olecallback.go
Comment on lines +62 to +82
func ncQueryInterface(this, iid, ppvObject uintptr) uintptr {
if iid == 0 {
if ppvObject != 0 {
*(*uintptr)(unsafe.Pointer(ppvObject)) = 0
}
return hrENoInterface
}
guid := (*ole.GUID)(unsafe.Pointer(iid))
out := (*uintptr)(unsafe.Pointer(ppvObject))
if ole.IsEqualGUID(guid, ole.IID_IUnknown) || ole.IsEqualGUID(guid, ole.IID_IDispatch) {
atomic.AddInt32(&globalNoop.ref, 1)
if out != nil {
*out = this
}
return hrSOK
}
if out != nil {
*out = 0
}
return hrENoInterface
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In COM, QueryInterface must return E_POINTER if ppvObject is null. Additionally, instead of referencing the global globalNoop singleton directly, it is more idiomatic and robust to cast the this pointer to *noopCallback to access the instance's reference counter.

func ncQueryInterface(this, iid, ppvObject uintptr) uintptr {
	if ppvObject == 0 {
		return hrEPointer
	}
	if iid == 0 {
		*(*uintptr)(unsafe.Pointer(ppvObject)) = 0
		return hrENoInterface
	}
	guid := (*ole.GUID)(unsafe.Pointer(iid))
	out := (*uintptr)(unsafe.Pointer(ppvObject))
	if ole.IsEqualGUID(guid, ole.IID_IUnknown) || ole.IsEqualGUID(guid, ole.IID_IDispatch) {
		p := (*noopCallback)(unsafe.Pointer(this))
		atomic.AddInt32(&p.ref, 1)
		*out = this
		return hrSOK
	}
	*out = 0
	return hrENoInterface
}

Comment thread olecallback.go
Comment on lines +84 to +93
func ncAddRef(this uintptr) uintptr {
return uintptr(uint32(atomic.AddInt32(&globalNoop.ref, 1)))
}

func ncRelease(this uintptr) uintptr {
// Singleton object: it is never actually freed even if the count reaches
// zero. We still maintain the counter so the value returned to the COM
// caller is meaningful.
return uintptr(uint32(atomic.AddInt32(&globalNoop.ref, -1)))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of referencing the global globalNoop singleton directly, cast the this pointer to *noopCallback to access the instance's reference counter. This is more idiomatic for COM method implementations.

func ncAddRef(this uintptr) uintptr {
	p := (*noopCallback)(unsafe.Pointer(this))
	return uintptr(uint32(atomic.AddInt32(&p.ref, 1)))
}

func ncRelease(this uintptr) uintptr {
	// Singleton object: it is never actually freed even if the count reaches
	// zero. We still maintain the counter so the value returned to the COM
	// caller is meaningful.
	p := (*noopCallback)(unsafe.Pointer(this))
	return uintptr(uint32(atomic.AddInt32(&p.ref, -1)))
}

Comment thread olecallback.go
Comment on lines +112 to +117
func ncInvoke(this, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr uintptr) uintptr {
if pVarResult != 0 {
*(*uint16)(unsafe.Pointer(pVarResult)) = 0 // VT_EMPTY
}
return hrSOK
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of using raw pointer dereferencing with magic numbers (*(*uint16)(unsafe.Pointer(pVarResult)) = 0), cast pVarResult to *ole.VARIANT and use ole.VT_EMPTY from the go-ole package. This is much cleaner and less error-prone.

Suggested change
func ncInvoke(this, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr uintptr) uintptr {
if pVarResult != 0 {
*(*uint16)(unsafe.Pointer(pVarResult)) = 0 // VT_EMPTY
}
return hrSOK
}
func ncInvoke(this, dispIdMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr uintptr) uintptr {
if pVarResult != 0 {
v := (*ole.VARIANT)(unsafe.Pointer(pVarResult))
v.VT = ole.VT_EMPTY
}
return hrSOK
}

- Add hrEPointer (0x80004003) and return it when ppvObject is null
- Simplify ncQueryInterface by removing redundant nil checks after
  early E_POINTER guard
- Use (*ole.VARIANT).VT = ole.VT_EMPTY instead of raw uint16 cast

Change-Id: Ibc97c62942a7aec7bd87c7bedb18eb9939472d29
Co-developed-by: Qoder <noreply@qoder.com>
@ceshihao
ceshihao merged commit 676b293 into master Jun 18, 2026
10 checks passed
@ceshihao
ceshihao deleted the fix/async-wua-callback branch June 18, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant