Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve go-webview2 com object release #3931

Open
Snshadow opened this issue Dec 5, 2024 · 6 comments
Open

Improve go-webview2 com object release #3931

Snshadow opened this issue Dec 5, 2024 · 6 comments

Comments

@Snshadow
Copy link

Snshadow commented Dec 5, 2024

This is a followup from wailsapp/go-webview2#25 (comment) which couldn't be fixed properly as wails build fails in Windows if it were changed.

The cause of the build failure is due to these codes that expects error return from Release function which only returns reference count as a uint32 value, not an error, which can cause unexpected erroring as Proc.Call states that

The returned error is always non-nil, constructed from the result of GetLastError. Callers must inspect the primary return value to decide whether an error occurred (according to the semantics of the specific function being called) before consulting the error. The error always has type Errno.

These parts expect Release to return an error which prevent building from succeeding.

If this were to be fixed, wails and go-webview2 would need to be fixed at the same time(as wails depends on go-webview2).

@stffabi
Copy link
Collaborator

stffabi commented Dec 5, 2024

The reference count returned from AddRef and Release is almost never really of interest.

Microsoft also states https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release#return-value

The method returns the new reference count. This value is intended to be used only for test purposes.

So I personally would not change the signature to return uint32, which would basically then always be ignored in the Wails code. Also changing the interface means we should probably bump go-webview2 to v2 so people won't end up with a brocken Wails application by the means of just calling a go get upgrade dependencies.

We should probably just leave the interface (and change the implementation to return nil in go-webview2) as it is so we don't break all the Wails v2 apps out there. Then clearly define a set of rules how errors should be handled and detected for com call and afterwards bring go-webview2 as v2 up to that and use it in Wails v3.

@leaanthony any thoughts?

@leaanthony
Copy link
Member

leaanthony commented Dec 5, 2024

Agree with you @stffabi . We don't want past decisions to hold us back so let's create a v2 directory and create what we think is the right API in there. I started looking at the composition controller too and realised that the main components of the package should probably change too.

The code generator needs to be updated to output the better error handling.

@leaanthony
Copy link
Member

@Snshadow - given we can create a new v2 version of this library, what are your recommendations? Shall we just copy what's there for now and make the additional breaking changes above?

@Snshadow
Copy link
Author

Snshadow commented Dec 18, 2024

I think that we could leave the Addref() and Release() return an error as it were before, we could check the reference count of a object then return an error if the functions are called on a object which reference count has reached zero after usage.
Looking the comment from go-webview2 and the implementation of IUnknown::Release() in the combridge, the idea of tracking the reference count of the object when calling Addref() or Release() exists there.
If we were to use the combridge for handling COM object, there are some things that we should be aware of,

  • Instead of panicking if Release if called on destroyed object, it should do error handling(logging or something else?)
func (c *comObject) release() int32 {
	c.l.Lock()
	defer c.l.Unlock()
	if c.refCount <= 0 {
		panic("call on destroyed com object")
	}
	...
}
  • As there are bunch of codes that are already written to handle COM objects, doing this would make a lot of work.

Instead, we could add a refCount field in the COM object structs of keep track of reference count when calling AddRef or Release after QueryInterface to prevent any undefined behaviours(exception, crash, ...), then increment the refCount value with refCount++ in QueryInterface() or other COM object creation functions such as CreateWebResourceResponse that can fill in the interface specific functions.
e.g

type _ICoreWebView2WebResourceResponseVtbl struct {
	_IUnknownVtbl
	GetContent      ComProc
	PutContent      ComProc
	GetHeaders      ComProc
	GetStatusCode   ComProc
	PutStatusCode   ComProc
	GetReasonPhrase ComProc
	PutReasonPhrase ComProc

	refCount uint32
}

func (i *ICoreWebView2WebResourceResponse) AddRef() error {
	if i.refCount <= 0 {
		return fmt.Errorf("called Addref() on object with reference count zero")
	}

	v, _, _ := i.vtbl.AddRef.Call(uintptr(unsafe.Pointer(i)))
	_ = v // do something with this value for debugging/testing purpose?

	i.refCount++

	return nil
}

func (i *ICoreWebView2WebResourceResponse) Release() error {
	if i.refCount <= 0 {
		return fmt.Errorf("called Release() on object with reference count zero")
	}

	v, _, _ := i.vtbl.Release.Call(uintptr(unsafe.Pointer(i)))
	_ = v // do something with this value for debugging/testing purpose?

	i.refCount--

	return nil
}

The return value of AddRef or Release can be used only in some situations as https://learn.microsoft.com/en-us/windows/win32/com/rules-for-managing-reference-counts states that

In some situations, the return values of AddRef and Release may be unstable and should not be relied upon; they should be used only for debugging or diagnostic purposes.

Next, for the function that could return windows.S_OK but return an unexpected output like this, instead of just returning unknown error by ignoring the lasterror value, we could show hint as @stffabi said in wailsapp/go-webview2#25 (comment).
I wonder if this is possible by changing the code generator.

@stffabi
Copy link
Collaborator

stffabi commented Dec 18, 2024

Please be aware of the differences between the combridge and the other com code. The combridge is used to implement com-objects in go, there we implement com-interfaces in go, that get passed to com. So the real com-object is managed in Go. The combridge is currently only used in rare cases for bootstrapping the webview2, where we need to create and pass a com-object to webview2.

Whereas e.g. ICoreWebView2WebResourceResponse the real com object is in native code outside of the Go runtime, those object are created by Webview2 and we only have a thin wrapper to call the native implementation. So in the ICoreWebView2WebResourceResponse case the refCount tracking needs to be done by the real object and we shouldn't do that somehow in our wrapper.
I would refrain from doing a refCounting somehow on our wrappers, those will never be in sync with the real one of the object. So people might see a refCount=0 when debugging, but still the real object on the native side might have a refCount>0 and still consume memory. We should only be a thin wrapper between Go and the real com-interface so we can call methods on the com object not more and not less. As per definition of IUnknown AddRef and Release can never return an error, so we should either remove the error return from the method definition or simply always return nil.

As for the panic you mentioning for func (c *comObject) release() int32 { if that happens, there is something really strange going on. That would mean some native code still has a pointer to a com-object that has already been freed. So there's no guarantee what is going on, the pointer that the native code used to call release() might now point to something new. As a result the program is already going a nondeterministic path and calling potentially unsafe memory locations. So personally I prefer a panic if we can detect memory corruption instead of silently ignoring it and have a nondeterministic program execution. Sure it is not guaranteed we can detect the corruption and panic, but at least if know memory corruption is going on, we should abort the program

@Snshadow
Copy link
Author

Snshadow commented Dec 19, 2024

@stffabi Oh, there are lots of things that were going on behind the scene.., thank you for the explanation!
I have seen some cases when calling Release() on a already destroyed object result in a crash with something like

Exception 0xc0000005 0x1 0x150 0x7ffbbc5857eb
PC=0x7ffbbc5857eb

which indicates an access violation when I accidentally called Release after ole.VARIANT.Clear() while using the go-ole package which calls VariantClear as it calls Release for VT_DISPATCH(IDispatch) object which inherits IUnknown interface,

however, as wails and go-webview2 implements the use of COM object itself, the checking the reference count would not be necessary if AddRef() and Release() are used with care.

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

No branches or pull requests

3 participants