diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/bridge.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/bridge.go new file mode 100644 index 00000000..2f837fed --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/bridge.go @@ -0,0 +1,237 @@ +package combridge + +import ( + "fmt" + "runtime" + "sync" + "sync/atomic" +) + +var ( + comIfcePointersL sync.RWMutex + comIfcePointers = map[uintptr]*comObject{} // Map from ComInterfacePointer to the Go ComObject +) + +// Resolve the GoInterface of the specified ComInterfacePointer +func Resolve[T IUnknown](ifceP uintptr) T { + comIfcePointersL.RLock() + comObj := comIfcePointers[ifceP] + comIfcePointersL.RUnlock() + + var n T + if comObj != nil { + t := comObj.resolve(ifceP) + if t != nil { + n = t.(T) + } + } + + return n +} + +// New returns a new ComObject which implements the specified Com Interface, com calls will be redirected +// to the specified go interface. +func New[T IUnknown](obj T) *ComObject[T] { + cObj := new( + ifceDef[T]{obj}, + ) + return newComObject[T](cObj) +} + +// New2 returns a new ComObject which implements the two specified Com Interfaces, com calls will be redirected +// to those interfaces accordingly. +// This is needed if a ComObject should implement two interfaces that are not descendants of each other, +// then you get multiple inheritance. +func New2[T IUnknown, T2 IUnknown](obj T, obj2 T2) *ComObject[T] { + cObj := new( + ifceDef[T]{obj}, + ifceDef[T2]{obj2}, + ) + return newComObject[T](cObj) +} + +// new returns a new ComObject which implements multiple specified Com Interfaces, com calls will be redirected +// to the specified go interfaces accordingly. +// This is needed if a ComObject should implement multiple interfaces that are not descendants of each other, +// then you get multiple inheritance. +func new(impls ...ifceImpl) *comObject { + impls = append([]ifceImpl{ifceDef[IUnknown]{}}, impls...) + + cObj := &comObject{ + refCount: 1, + ifces: map[string]int{}, + ifcesImpl: make([]comInterfaceDesc, len(impls)), + } + + for i, ifceDef := range impls { + vtable, err := ifceDef.ifce() + if err != nil { + panic(err) + } + + needsImplement := false + for table := vtable; table != nil; table = table.Parent { + guid := table.ComGUID + if i, found := cObj.ifces[guid]; found { + // This Interface is already implemented + if guid == iUnknownGUID { + // IUnknown is a special interface and never has an user specific implementation + } else if cObj.ifcesImpl[i].impl != ifceDef.impl() { + panic(fmt.Sprintf("Interface '%s' is already implemented by another object", table.Name)) + } + + break + } + + needsImplement = true + cObj.ifces[guid] = i + } + + if !needsImplement { + continue + } + + ifceP, ifcePSlice := allocUintptrObject(1) + ifcePSlice[0] = vtable.ComVTable + cObj.ifcesImpl[i] = comInterfaceDesc{ifceP, ifceDef.impl()} + } + + comIfcePointersL.Lock() + for _, ifceImpl := range cObj.ifcesImpl { + comIfcePointers[ifceImpl.ref] = cObj + } + comIfcePointersL.Unlock() + + return cObj +} + +func newComObject[T IUnknown](comObj *comObject) *ComObject[T] { + c := &ComObject[T]{obj: comObj} + // Make sure to async release since release needs locks and might block the finalizer goroutine for a longer period + runtime.SetFinalizer(c, func(obj *ComObject[T]) { obj.close(true) }) + return c +} + +// ComObject describes an exported go instance to be used as a ComObject which implements +// the specified Interface. +type ComObject[T IUnknown] struct { + obj *comObject + closed int32 +} + +// Ref returns the native uintptr that points to the ComObject that is an interface pointer to T. +// This can be used in native calls. If the object has been closed this function will panic. +func (o *ComObject[T]) Ref() uintptr { + if atomic.LoadInt32(&o.closed) != 0 { + panic("ComObject has been released") + } + return o.obj.queryInterface(guidOf[T](), false) +} + +// Close releases the native com object from the go side. It will only be destroyed if the ref counter +// reaches zero. +// After closing `Ref()` will panic. +func (o *ComObject[T]) Close() error { + o.close(false) + return nil +} + +// close releases the native com object from the go side. It will only be destroyed if the ref counter +// reaches zero. +// After closing `Ref()` will panic. +func (o *ComObject[T]) close(asyncRelease bool) { + if atomic.CompareAndSwapInt32(&o.closed, 0, 1) { + runtime.SetFinalizer(o, nil) + if asyncRelease { + go o.obj.release() + } else { + o.obj.release() + } + } +} + +type comInterfaceDesc struct { + ref uintptr // The native Com InterfacePointer + impl any // The golang target object +} + +type comObject struct { + l sync.Mutex + + refCount int32 + ifces map[string]int // Map of ComInterfaceGUID to Interface Slots + ifcesImpl []comInterfaceDesc // Slots with InterfaceDescriptors +} + +func (c *comObject) queryInterface(ifceGUID string, withAddRef bool) uintptr { + c.l.Lock() + defer c.l.Unlock() + if c.refCount <= 0 { + panic("call on released com object") + } + + i, found := c.ifces[ifceGUID] + if !found { + return 0 + } + + if withAddRef { + c.refCount++ + } + return c.ifcesImpl[i].ref +} + +func (c *comObject) resolve(ifceP uintptr) any { + c.l.Lock() + defer c.l.Unlock() + if c.refCount <= 0 { + panic("call on destroyed com object") + } + + for _, ifce := range c.ifcesImpl { + if ifce.ref != ifceP { + continue + } + + return ifce.impl + } + return nil +} + +func (c *comObject) addRef() int32 { + c.l.Lock() + defer c.l.Unlock() + if c.refCount <= 0 { + panic("call on destroyed com object") + } + + c.refCount++ + return c.refCount +} + +func (c *comObject) release() int32 { + c.l.Lock() + defer c.l.Unlock() + if c.refCount <= 0 { + panic("call on destroyed com object") + } + + if c.refCount--; c.refCount == 0 { + comIfcePointersL.Lock() + for _, ref := range c.ifcesImpl { + delete(comIfcePointers, ref.ref) + } + comIfcePointersL.Unlock() + + for _, impl := range c.ifcesImpl { + ref := impl.ref + if ref == 0 { + continue + } + + globalFree(ref) + } + } + + return c.refCount +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/iunknown.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/iunknown.go new file mode 100644 index 00000000..00790d74 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/iunknown.go @@ -0,0 +1,54 @@ +package combridge + +import ( + "golang.org/x/sys/windows" +) + +const iUnknownGUID = "{00000000-0000-0000-C000-000000000046}" + +func init() { + registerVTableInternal[IUnknown, IUnknown]( + iUnknownGUID, + true, + iUnknownQueryInterface, + iUnknownAddRef, + iUnknownRelease, + ) +} + +type IUnknown interface{} + +func iUnknownQueryInterface(this uintptr, refiid *windows.GUID, ppvObject *uintptr) uintptr { + if refiid == nil || ppvObject == nil { + return uintptr(windows.E_INVALIDARG) + } + + comIfcePointersL.RLock() + obj := comIfcePointers[this] + comIfcePointersL.RUnlock() + + ref := obj.queryInterface(refiid.String(), true) + if ref != 0 { + *ppvObject = ref + return windows.NO_ERROR + } + + *ppvObject = 0 + return uintptr(windows.E_NOINTERFACE) +} + +func iUnknownAddRef(this uintptr) uintptr { + comIfcePointersL.RLock() + obj := comIfcePointers[this] + comIfcePointersL.RUnlock() + + return uintptr(obj.addRef()) +} + +func iUnknownRelease(this uintptr) uintptr { + comIfcePointersL.RLock() + obj := comIfcePointers[this] + comIfcePointersL.RUnlock() + + return uintptr(obj.release()) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/iunknown_impl.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/iunknown_impl.go new file mode 100644 index 00000000..b08d1292 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/iunknown_impl.go @@ -0,0 +1,72 @@ +package combridge + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// IUnknownFromPointer cast a generic pointer into a IUnknownImpl pointer +func IUnknownFromPointer(ref unsafe.Pointer) *IUnknownImpl { + return (*IUnknownImpl)(ref) +} + +// IUnknownFromPointer cast native pointer into a IUnknownImpl pointer +func IUnknownFromUintptr(ref uintptr) *IUnknownImpl { + return IUnknownFromPointer(unsafe.Pointer(ref)) +} + +type IUnknownVtbl struct { + queryInterface uintptr + addRef uintptr + release uintptr +} + +func (i *IUnknownVtbl) QueryInterface(this unsafe.Pointer, refiid *windows.GUID, ppvObject **IUnknownImpl) error { + r, _, _ := syscall.SyscallN( + i.queryInterface, + uintptr(this), + uintptr(unsafe.Pointer(refiid)), + uintptr(unsafe.Pointer(ppvObject)), + ) + + if r != uintptr(windows.S_OK) { + return syscall.Errno(r) + } + + return nil +} + +func (i *IUnknownVtbl) AddRef(this unsafe.Pointer) uint32 { + r, _, _ := syscall.SyscallN( + i.addRef, + uintptr(this), + ) + return uint32(r) +} + +func (i *IUnknownVtbl) Release(this unsafe.Pointer) uint32 { + r, _, _ := syscall.SyscallN( + i.release, + uintptr(this), + ) + + return uint32(r) +} + +type IUnknownImpl struct { + vtbl *IUnknownVtbl +} + +func (i *IUnknownImpl) QueryInterface(refiid *windows.GUID, ppvObject **IUnknownImpl) error { + return i.vtbl.QueryInterface(unsafe.Pointer(i), refiid, ppvObject) +} + +func (i *IUnknownImpl) AddRef() uint32 { + return i.vtbl.AddRef(unsafe.Pointer(i)) +} + +func (i *IUnknownImpl) Release() uint32 { + return i.vtbl.Release(unsafe.Pointer(i)) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/syscall.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/syscall.go new file mode 100644 index 00000000..819a4fa2 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/syscall.go @@ -0,0 +1,37 @@ +package combridge + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGlobalAlloc = modkernel32.NewProc("GlobalAlloc") + procGlobalFree = modkernel32.NewProc("GlobalFree") + + uintptrSize = unsafe.Sizeof(uintptr(0)) +) + +func allocUintptrObject(size int) (uintptr, []uintptr) { + v := globalAlloc(uintptr(size) * uintptrSize) + slice := unsafe.Slice((*uintptr)(unsafe.Pointer(v)), size) + return v, slice +} + +func globalAlloc(dwBytes uintptr) uintptr { + ret, _, _ := procGlobalAlloc.Call(uintptr(0), dwBytes) + if ret == 0 { + panic("globalAlloc failed") + } + + return ret +} + +func globalFree(data uintptr) { + ret, _, _ := procGlobalFree.Call(data) + if ret != 0 { + panic("globalFree failed") + } +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/vtables.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/vtables.go new file mode 100644 index 00000000..3872dc0b --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge/vtables.go @@ -0,0 +1,145 @@ +package combridge + +import ( + "fmt" + "reflect" + "sync" + + "golang.org/x/sys/windows" +) + +var ( + vTablesL sync.Mutex + vTables = make(map[string]*vTable) +) + +// RegisterVTable registers the vtable trampoline methods for the specified ComInterface +// TBase is the base interface of T, and must be another ComInterface which roots in IUnknown or IUnknown itself. +// The first paramter of the fn is always the uintptr of the ComObject and the GoObject can be resolved with Resolve(). +// After having resolved the GoObject the call must be redirected to the GoObject. +// Typically a trampoline FN looks like this. +// +// func _ICoreWebView2NavigationCompletedEventHandlerInvoke(this uintptr, sender *ICoreWebView2, args *ICoreWebView2NavigationCompletedEventArgs) uintptr { +// return combridge.Resolve[_ICoreWebView2NavigationCompletedEventHandler](this).NavigationCompleted(sender, args) +// } +// +// The order of registration must be in the correct order as specified in the IDL of the interface. +func RegisterVTable[TParent, T IUnknown](guid string, fns ...interface{}) { + registerVTableInternal[TParent, T](guid, false, fns...) +} + +type vTable struct { + Parent *vTable + + Name string + ComGUID string + ComVTable uintptr + ComProcs []uintptr +} + +func registerVTableInternal[TParent, T IUnknown](guid string, isInternal bool, fns ...interface{}) { + vTablesL.Lock() + defer vTablesL.Unlock() + + t, tName := typeInterfaceToString[T]() + tParent, tParentName := typeInterfaceToString[TParent]() + if !t.Implements(tParent) { + panic(fmt.Errorf("RegisterVTable '%s': '%s' must implement '%s'", tName, tName, tParentName)) + } + + if !isInternal { + if t == reflect.TypeOf((*IUnknown)(nil)).Elem() { + panic(fmt.Errorf("RegisterVTable '%s' IUnknown can't be registered", tName)) + } + + if t == tParent { + panic(fmt.Errorf("RegisterVTable '%s': T and TParent can't be the same type", tName)) + } + } + + var parent *vTable + var parentProcs []uintptr + var parentProcsCount int + if t != tParent { + parent = vTables[tParentName] + if parent == nil { + panic(fmt.Errorf("RegisterVTable '%s': Parent VTable '%s' not registered", tName, tParentName)) + } + + parentProcs = parent.ComProcs + parentProcsCount = len(parentProcs) + } + + comGuid, err := windows.GUIDFromString(guid) + if err != nil { + panic(fmt.Errorf("RegisterVTable '%s': invalid guid: %s", tName, err)) + } + + vTable := &vTable{ + Parent: parent, + Name: tName, + ComGUID: comGuid.String(), + } + vTable.ComVTable, vTable.ComProcs = allocUintptrObject(parentProcsCount + len(fns)) + + for i, proc := range parentProcs { + vTable.ComProcs[i] = proc + } + + for i, fn := range fns { + vTable.ComProcs[parentProcsCount+i] = windows.NewCallback(fn) + } + + vTables[tName] = vTable +} + +func typeInterfaceToString[T any]() (reflect.Type, string) { + t := reflect.TypeOf((*T)(nil)) + if t.Kind() != reflect.Pointer { + panic("must be a (*yourInterfaceType)(nil)") + } + t = t.Elem() + return t, t.PkgPath() + "/" + t.Name() +} + +func typeInterfaceToStringOnly[T any]() string { + _, nane := typeInterfaceToString[T]() + return nane +} + +func guidOf[T any]() string { + vtable := vTableOf[T]() + if vtable == nil { + return "" + } + return vtable.ComGUID +} + +func vTableOf[T any]() *vTable { + name := typeInterfaceToStringOnly[T]() + vTablesL.Lock() + defer vTablesL.Unlock() + + return vTables[name] +} + +type ifceImpl interface { + impl() any + ifce() (*vTable, error) +} + +type ifceDef[T any] struct { + objImpl any +} + +func (i ifceDef[T]) impl() any { + return i.objImpl +} + +func (i ifceDef[T]) ifce() (*vTable, error) { + vtable := vTableOf[T]() + if vtable == nil { + return nil, fmt.Errorf("Unable to find vTable for %s", typeInterfaceToStringOnly[T]()) + } + return vtable, nil +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/chromium.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/chromium.go index a2d5c92c..686b389e 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/chromium.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/chromium.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "sync/atomic" + "syscall" "unsafe" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/internal/w32" @@ -86,34 +87,18 @@ func (e *Chromium) Embed(hwnd uintptr) bool { dataPath = filepath.Join(os.Getenv("AppData"), currentExeName) } - var browserPathPtr *uint16 = nil if e.BrowserPath != "" { - if _, err := os.Stat(e.BrowserPath); !errors.Is(err, os.ErrNotExist) { - browserPathPtr, err = windows.UTF16PtrFromString(e.BrowserPath) - if err != nil { - log.Printf("Error calling UTF16PtrFromString for %s: %v", e.BrowserPath, err) - return false - } - } else { + if _, err := os.Stat(e.BrowserPath); errors.Is(err, os.ErrNotExist) { log.Printf("Browser path %s does not exist", e.BrowserPath) return false } } - dataPathPtr, err := windows.UTF16PtrFromString(dataPath) - if err != nil { - log.Printf("Error calling UTF16PtrFromString for %s: %v", dataPath, err) + if err := createCoreWebView2EnvironmentWithOptions(e.BrowserPath, dataPath, e.envCompleted); err != nil { + log.Printf("Error calling Webview2Loader: %v", err) return false } - res, err := createCoreWebView2EnvironmentWithOptions(browserPathPtr, dataPathPtr, 0, e.envCompleted) - if err != nil { - log.Printf("Error calling Webview2Loader: %v", err) - return false - } else if res != 0 { - log.Printf("Result: %08x", res) - return false - } var msg w32.Msg for { if atomic.LoadUintptr(&e.inited) != 0 { @@ -185,8 +170,8 @@ func (e *Chromium) Release() uintptr { } func (e *Chromium) EnvironmentCompleted(res uintptr, env *ICoreWebView2Environment) uintptr { - if int64(res) < 0 { - log.Fatalf("Creating environment failed with %08x", res) + if int32(res) < 0 { + log.Fatalf("Creating environment failed with %08x: %s", res, syscall.Errno(res)) } env.vtbl.AddRef.Call(uintptr(unsafe.Pointer(env))) e.environment = env @@ -200,8 +185,8 @@ func (e *Chromium) EnvironmentCompleted(res uintptr, env *ICoreWebView2Environme } func (e *Chromium) CreateCoreWebView2ControllerCompleted(res uintptr, controller *ICoreWebView2Controller) uintptr { - if int64(res) < 0 { - log.Fatalf("Creating controller failed with %08x", res) + if int32(res) < 0 { + log.Fatalf("Creating controller failed with %08x: %s", res, syscall.Errno(res)) } controller.vtbl.AddRef.Call(uintptr(unsafe.Pointer(controller))) e.controller = controller diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/corewebview2.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/corewebview2.go index 9c23da2f..9cd7dc0c 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/corewebview2.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/corewebview2.go @@ -11,7 +11,6 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/internal/w32" - "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/webviewloader" "golang.org/x/sys/windows" ) @@ -48,15 +47,6 @@ const ( CoreWebView2PermissionStateDeny ) -func createCoreWebView2EnvironmentWithOptions(browserExecutableFolder, userDataFolder *uint16, environmentOptions uintptr, environmentCompletedHandle *iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) (uintptr, error) { - return webviewloader.CreateCoreWebView2EnvironmentWithOptions( - browserExecutableFolder, - userDataFolder, - environmentOptions, - uintptr(unsafe.Pointer(environmentCompletedHandle)), - ) -} - // ComProc stores a COM procedure. type ComProc uintptr @@ -65,8 +55,9 @@ func NewComProc(fn interface{}) ComProc { return ComProc(windows.NewCallback(fn)) } -//go:uintptrescapes // Call calls a COM procedure. +// +//go:uintptrescapes func (p ComProc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) { // The magic uintptrescapes comment is needed to prevent moving uintptr(unsafe.Pointer(p)) so calls to .Call() also // satisfy the unsafe.Pointer rule "(4) Conversion of a Pointer to a uintptr when calling syscall.Syscall." diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/create_env_go.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/create_env_go.go new file mode 100644 index 00000000..8651cab0 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/create_env_go.go @@ -0,0 +1,28 @@ +//go:build exp_gowebview2loader + +package edge + +import ( + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/webviewloader" +) + +func createCoreWebView2EnvironmentWithOptions(browserExecutableFolder, userDataFolder string, environmentCompletedHandle *iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) error { + e := &environmentCreatedHandler{environmentCompletedHandle} + return webviewloader.CreateCoreWebView2EnvironmentWithOptions( + e, + webviewloader.WithBrowserExecutableFolder(browserExecutableFolder), + webviewloader.WithUserDataFolder(userDataFolder), + ) +} + +type environmentCreatedHandler struct { + originalHandler *iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler +} + +func (r *environmentCreatedHandler) EnvironmentCompleted(errorCode webviewloader.HRESULT, createdEnvironment *webviewloader.ICoreWebView2Environment) webviewloader.HRESULT { + env := (*ICoreWebView2Environment)(unsafe.Pointer(createdEnvironment)) + res := r.originalHandler.impl.EnvironmentCompleted(uintptr(errorCode), env) + return webviewloader.HRESULT(res) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/create_env_native.go b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/create_env_native.go new file mode 100644 index 00000000..0e24eca3 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge/create_env_native.go @@ -0,0 +1,41 @@ +//go:build !exp_gowebview2loader + +package edge + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/webviewloader" + + "golang.org/x/sys/windows" +) + +func createCoreWebView2EnvironmentWithOptions(browserExecutableFolder, userDataFolder string, environmentCompletedHandle *iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) error { + browserPathPtr, err := windows.UTF16PtrFromString(browserExecutableFolder) + if err != nil { + return fmt.Errorf("Error calling UTF16PtrFromString for %s: %v", browserExecutableFolder, err) + } + + userPathPtr, err := windows.UTF16PtrFromString(userDataFolder) + if err != nil { + return fmt.Errorf("Error calling UTF16PtrFromString for %s: %v", userDataFolder, err) + } + + hr, err := webviewloader.CreateCoreWebView2EnvironmentWithOptions( + browserPathPtr, + userPathPtr, + 0, + uintptr(unsafe.Pointer(environmentCompletedHandle)), + ) + + if hr != 0 { + if err == nil || err == windows.ERROR_SUCCESS { + err = syscall.Errno(hr) + } + return err + } + + return nil +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/README.md b/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/README.md deleted file mode 100644 index 64d8c4a9..00000000 --- a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# GoWebView2Loader - -GoWebView2Loader is a port of [OpenWebView2Loader](https://github.com/jchv/OpenWebView2Loader) to Go. - -It is intended to be feature-complete in the near future with the original WebView2Loader distributed with -the WebView2 NuGet package. - -## Status - -- [ ] CompareBrowserVersions -- [ ] CreateCoreWebView2Environment -- [ ] CreateCoreWebView2EnvironmentWithOptions -- [ ] GetAvailableCoreWebView2BrowserVersionString - - [ ] Feature Complete - - [x] Fixed Runtime support diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/LICENSE b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/LICENSE similarity index 100% rename from v2/internal/frontend/desktop/windows/go-webview2/webview2loader/LICENSE rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/LICENSE diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/README.md b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/README.md new file mode 100644 index 00000000..f3e020ec --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/README.md @@ -0,0 +1,19 @@ +# Webviewloader + +Webviewloader is a port of [OpenWebView2Loader](https://github.com/jchv/OpenWebView2Loader) to Go. + +It is intended to be feature-complete with the original WebView2Loader distributed with +the WebView2 NuGet package, but some features are intentionally not implemented. + +## Status + +- [x] CompareBrowserVersions +- [x] CreateCoreWebView2Environment +- [x] CreateCoreWebView2EnvironmentWithOptions +- [x] GetAvailableCoreWebView2BrowserVersionString + +## Not implemented features + +- Registry Overrides of Parameters +- Env Variable Overrides of Parameters +- Does not incorporate `GetCurrentPackageInfo` to search for an installed runtime diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create.go new file mode 100644 index 00000000..2e2bdd55 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create.go @@ -0,0 +1,159 @@ +//go:build exp_gowebview2loader + +package webviewloader + +import ( + "fmt" + "path/filepath" + "syscall" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge" + "golang.org/x/sys/windows" +) + +func init() { + fmt.Println("DEB | Using experimental go webview2loader") +} + +type webView2RunTimeType int32 + +const ( + webView2RunTimeTypeInstalled webView2RunTimeType = 0x00 + webView2RunTimeTypeRedistributable webView2RunTimeType = 0x01 +) + +// CreateCoreWebView2Environment creates an evergreen WebView2 Environment using the installed WebView2 Runtime version. +// +// This is equivalent to running CreateCoreWebView2EnvironmentWithOptions without any options. +// For more information, see CreateCoreWebView2EnvironmentWithOptions. +func CreateCoreWebView2Environment(environmentCompletedHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) error { + return CreateCoreWebView2EnvironmentWithOptions(environmentCompletedHandler) +} + +// CreateCoreWebView2EnvironmentWithOptions creates an environment with a custom version of WebView2 Runtime, +// user data folder, and with or without additional options. +// +// See https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/webview2-idl?#createcorewebview2environmentwithoptions +func CreateCoreWebView2EnvironmentWithOptions(environmentCompletedHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, opts ...option) error { + var params environmentOptions + for _, opt := range opts { + opt(¶ms) + } + + var err error + var dllPath string + var runtimeType webView2RunTimeType + if browserExecutableFolder := params.browserExecutableFolder; browserExecutableFolder != "" { + runtimeType = webView2RunTimeTypeRedistributable + dllPath, err = findEmbeddedClientDll(browserExecutableFolder) + } else { + runtimeType = webView2RunTimeTypeInstalled + dllPath, _, err = findInstalledClientDll(params.preferCanary) + } + + if err != nil { + return err + } + + return createWebViewEnvironmentWithClientDll(dllPath, runtimeType, params.userDataFolder, + ¶ms, environmentCompletedHandler) +} + +func createWebViewEnvironmentWithClientDll(lpLibFileName string, runtimeType webView2RunTimeType, userDataFolder string, + envOptions *environmentOptions, envCompletedHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler) error { + + if !filepath.IsAbs(lpLibFileName) { + return fmt.Errorf("lpLibFileName must be absolute") + } + + dll, err := windows.LoadDLL(lpLibFileName) + if err != nil { + return fmt.Errorf("Loading DLL failed: %w", err) + } + + defer func() { + canUnloadProc, err := dll.FindProc("DllCanUnloadNow") + if err != nil { + return + } + + if r1, _, _ := canUnloadProc.Call(); r1 != windows.NO_ERROR { + return + } + + dll.Release() + }() + + createProc, err := dll.FindProc("CreateWebViewEnvironmentWithOptionsInternal") + if err != nil { + return fmt.Errorf("Unable to find CreateWebViewEnvironmentWithOptionsInternal entrypoint: %w", err) + } + + userDataPtr, err := windows.UTF16PtrFromString(userDataFolder) + if err != nil { + return err + } + + envOptionsCom := combridge.New2[iCoreWebView2EnvironmentOptions, iCoreWebView2EnvironmentOptions2]( + envOptions, envOptions) + + defer envOptionsCom.Close() + + envCompletedHandler = &environmentCreatedHandler{envCompletedHandler} + envCompletedCom := combridge.New[iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler](envCompletedHandler) + defer envCompletedCom.Close() + + const unknown = 1 + hr, _, err := createProc.Call( + uintptr(unknown), + uintptr(runtimeType), + uintptr(unsafe.Pointer(userDataPtr)), + uintptr(envOptionsCom.Ref()), + uintptr(envCompletedCom.Ref())) + + if hr != 0 { + if err == nil || err == windows.ERROR_SUCCESS { + err = syscall.Errno(hr) + } + return err + } + + return nil +} + +type environmentCreatedHandler struct { + originalHandler ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler +} + +func (r *environmentCreatedHandler) EnvironmentCompleted(errorCode HRESULT, createdEnvironment *ICoreWebView2Environment) HRESULT { + // The OpenWebview2Loader has some retry logic and retries once, didn't encounter any case when this would have been + // needed during the development: https://github.com/jchv/OpenWebView2Loader/blob/master/Source/WebView2Loader.cpp#L202 + + if createdEnvironment != nil { + // May or may not be necessary, but the official WebView2Loader seems to do it. + iidICoreWebView2Environment := windows.GUID{ + Data1: 0xb96d755e, + Data2: 0x0319, + Data3: 0x4e92, + Data4: [8]byte{0xa2, 0x96, 0x23, 0x43, 0x6f, 0x46, 0xa1, 0xfc}, + } + + if err := createdEnvironment.QueryInterface(&iidICoreWebView2Environment, &createdEnvironment); err != nil { + createdEnvironment = nil + errNo, ok := err.(syscall.Errno) + if !ok { + errNo = syscall.Errno(windows.E_FAIL) + } + errorCode = HRESULT(errNo) + } + } + + r.originalHandler.EnvironmentCompleted(errorCode, createdEnvironment) + + if createdEnvironment != nil { + createdEnvironment.Release() + } + + return HRESULT(windows.S_OK) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create_completed.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create_completed.go new file mode 100644 index 00000000..f48f62e0 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create_completed.go @@ -0,0 +1,42 @@ +//go:build exp_gowebview2loader + +package webviewloader + +import ( + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge" +) + +// HRESULT +// +// See https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values +type HRESULT int32 + +// ICoreWebView2Environment Represents the WebView2 Environment +// +// See https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2environment +type ICoreWebView2Environment = combridge.IUnknownImpl + +// ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler receives the WebView2Environment created using CreateCoreWebView2Environment. +type ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler interface { + // EnvironmentCompleted is invoked to receive the created WebView2Environment + // + // See https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2createcorewebview2environmentcompletedhandler?#invoke + EnvironmentCompleted(errorCode HRESULT, createdEnvironment *ICoreWebView2Environment) HRESULT +} + +type iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler interface { + combridge.IUnknown + ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler +} + +func init() { + combridge.RegisterVTable[combridge.IUnknown, iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler]( + "{4e8a3389-c9d8-4bd2-b6b5-124fee6cc14d}", + _iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandlerInvoke, + ) +} + +func _iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandlerInvoke(this uintptr, errorCode HRESULT, env *combridge.IUnknownImpl) uintptr { + res := combridge.Resolve[iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler](this).EnvironmentCompleted(errorCode, env) + return uintptr(res) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create_options.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create_options.go new file mode 100644 index 00000000..adfd8ca0 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/env_create_options.go @@ -0,0 +1,276 @@ +//go:build exp_gowebview2loader + +package webviewloader + +import ( + "unicode/utf16" + "unsafe" + + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/pkg/combridge" + "golang.org/x/sys/windows" +) + +// WithBrowserExecutableFolder to specify whether WebView2 controls use a fixed or installed version +// of the WebView2 Runtime that exists on a user machine. +// +// To use a fixed version of the WebView2 Runtime, +// pass the folder path that contains the fixed version of the WebView2 Runtime. +// BrowserExecutableFolder supports both relative (to the application's executable) and absolute files paths. +// To create WebView2 controls that use the installed version of the WebView2 Runtime that exists on user +// machines, pass a empty string to WithBrowserExecutableFolder. In this scenario, the API tries to find a +// compatible version of the WebView2 Runtime that is installed on the user machine (first at the machine level, +// and then per user) using the selected channel preference. The path of fixed version of the WebView2 Runtime +// should not contain \Edge\Application\. When such a path is used, the API fails with HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED). +func WithBrowserExecutableFolder(folder string) option { + return func(wvep *environmentOptions) { + wvep.browserExecutableFolder = folder + } +} + +// WithUserDataFolder specifies to user data folder location for WebView2 +// +// You may specify the userDataFolder to change the default user data folder location for WebView2. +// The path is either an absolute file path or a relative file path that is interpreted as relative +// to the compiled code for the current process. +// Dhe default user data ({Executable File Name}.WebView2) folder is created in the same directory +// next to the compiled code for the app. WebView2 creation fails if the compiled code is running +// in a directory in which the process does not have permission to create a new directory. +// The app is responsible to clean up the associated user data folder when it is done. +func WithUserDataFolder(folder string) option { + return func(wvep *environmentOptions) { + wvep.userDataFolder = folder + } +} + +// WithAdditionalBrowserArguments changes the behavior of the WebView. +// +// The arguments are passed to the +// browser process as part of the command. For more information about +// using command-line switches with Chromium browser processes, navigate to +// [Run Chromium with Flags][ChromiumDevelopersHowTosRunWithFlags]. +// The value appended to a switch is appended to the browser process, for +// example, in `--edge-webview-switches=xxx` the value is `xxx`. If you +// specify a switch that is important to WebView functionality, it is +// ignored, for example, `--user-data-dir`. Specific features are disabled +// internally and blocked from being enabled. If a switch is specified +// multiple times, only the last instance is used. +// +// \> [!NOTE]\n\> A merge of the different values of the same switch is not attempted, +// except for disabled and enabled features. The features specified by +// `--enable-features` and `--disable-features` are merged with simple +// logic.\n\> * The features is the union of the specified features +// and built-in features. If a feature is disabled, it is removed from the +// enabled features list. +// +// If you specify command-line switches and use the +// `additionalBrowserArguments` parameter, the `--edge-webview-switches` +// value takes precedence and is processed last. If a switch fails to +// parse, the switch is ignored. The default state for the operation is +// to run the browser process with no extra flags. +// +// [ChromiumDevelopersHowTosRunWithFlags]: https://www.chromium.org/developers/how-tos/run-chromium-with-flags "Run Chromium with flags | The Chromium Projects" +func WithAdditionalBrowserArguments(args string) option { + return func(wvep *environmentOptions) { + wvep.additionalBrowserArguments = args + } +} + +// WithLanguage sets the default display language for WebView. +// +// It applies to browser UI such as +// context menu and dialogs. It also applies to the `accept-languages` HTTP +// header that WebView sends to websites. It is in the format of +// +// `language[-country]` where `language` is the 2-letter code from +// [ISO 639][ISO639LanguageCodesHtml] +// and `country` is the +// 2-letter code from +// [ISO 3166][ISOStandard72482Html]. +// +// [ISO639LanguageCodesHtml]: https://www.iso.org/iso-639-language-codes.html "ISO 639 | ISO" +// [ISOStandard72482Html]: https://www.iso.org/standard/72482.html "ISO 3166-1:2020 | ISO" +func WithLanguage(lang string) option { + return func(wvep *environmentOptions) { + wvep.language = lang + } +} + +// WithTargetCompatibleBrowserVersion secifies the version of the WebView2 Runtime binaries required to be +// compatible with your app. +// +// This defaults to the WebView2 Runtime version +// that corresponds with the version of the SDK the app is using. The +// format of this value is the same as the format of the +// `BrowserVersionString` property and other `BrowserVersion` values. Only +// the version part of the `BrowserVersion` value is respected. The channel +// suffix, if it exists, is ignored. The version of the WebView2 Runtime +// binaries actually used may be different from the specified +// `TargetCompatibleBrowserVersion`. The binaries are only guaranteed to be +// compatible. Verify the actual version on the `BrowserVersionString` +// property on the `ICoreWebView2Environment`. +func WithTargetCompatibleBrowserVersion(version string) option { + return func(wvep *environmentOptions) { + wvep.targetCompatibleBrowserVersion = version + } +} + +// WithAllowSingleSignOnUsingOSPrimaryAccount is used to enable +// single sign on with Azure Active Directory (AAD) and personal Microsoft +// Account (MSA) resources inside WebView. All AAD accounts, connected to +// Windows and shared for all apps, are supported. For MSA, SSO is only enabled +// for the account associated for Windows account login, if any. +// Default is disabled. Universal Windows Platform apps must also declare +// `enterpriseCloudSSO` +// [Restricted capabilities][WindowsUwpPackagingAppCapabilityDeclarationsRestrictedCapabilities] +// for the single sign on (SSO) to work. +// +// [WindowsUwpPackagingAppCapabilityDeclarationsRestrictedCapabilities]: /windows/uwp/packaging/app-capability-declarations\#restricted-capabilities "Restricted capabilities - App capability declarations | Microsoft Docs" +func WithAllowSingleSignOnUsingOSPrimaryAccount(allow bool) option { + return func(wvep *environmentOptions) { + wvep.allowSingleSignOnUsingOSPrimaryAccount = allow + } +} + +// WithExclusiveUserDataFolderAccess specifies that the WebView environment +// obtains exclusive access to the user data folder. +// +// If the user data folder is already being used by another WebView environment with a +// different value for `ExclusiveUserDataFolderAccess` property, the creation of a WebView2Controller +// using the environment object will fail with `HRESULT_FROM_WIN32(ERROR_INVALID_STATE)`. +// When set as TRUE, no other WebView can be created from other processes using WebView2Environment +// objects with the same UserDataFolder. This prevents other processes from creating WebViews +// which share the same browser process instance, since sharing is performed among +// WebViews that have the same UserDataFolder. When another process tries to create a +// WebView2Controller from an WebView2Environment object created with the same user data folder, +// it will fail with `HRESULT_FROM_WIN32(ERROR_INVALID_STATE)`. +func WithExclusiveUserDataFolderAccess(exclusive bool) option { + return func(wvep *environmentOptions) { + wvep.exclusiveUserDataFolderAccess = exclusive + } +} + +type option func(*environmentOptions) + +var _ iCoreWebView2EnvironmentOptions = &environmentOptions{} +var _ iCoreWebView2EnvironmentOptions2 = &environmentOptions{} + +type environmentOptions struct { + browserExecutableFolder string + userDataFolder string + preferCanary bool + + additionalBrowserArguments string + language string + targetCompatibleBrowserVersion string + allowSingleSignOnUsingOSPrimaryAccount bool + exclusiveUserDataFolderAccess bool +} + +func (o *environmentOptions) AdditionalBrowserArguments() string { + return o.additionalBrowserArguments +} + +func (o *environmentOptions) Language() string { + return o.language +} + +func (o *environmentOptions) TargetCompatibleBrowserVersion() string { + v := o.targetCompatibleBrowserVersion + if v == "" { + v = kMinimumCompatibleVersion + } + return v +} + +func (o *environmentOptions) AllowSingleSignOnUsingOSPrimaryAccount() bool { + return o.allowSingleSignOnUsingOSPrimaryAccount +} + +func (o *environmentOptions) ExclusiveUserDataFolderAccess() bool { + return o.exclusiveUserDataFolderAccess +} + +type iCoreWebView2EnvironmentOptions interface { + combridge.IUnknown + + AdditionalBrowserArguments() string + Language() string + TargetCompatibleBrowserVersion() string + AllowSingleSignOnUsingOSPrimaryAccount() bool +} + +type iCoreWebView2EnvironmentOptions2 interface { + combridge.IUnknown + + ExclusiveUserDataFolderAccess() bool +} + +func init() { + combridge.RegisterVTable[combridge.IUnknown, iCoreWebView2EnvironmentOptions]( + "{2fde08a8-1e9a-4766-8c05-95a9ceb9d1c5}", + _iCoreWebView2EnvironmentOptionsAdditionalBrowserArguments, + _iCoreWebView2EnvironmentOptionsNOP, + _iCoreWebView2EnvironmentOptionsLanguage, + _iCoreWebView2EnvironmentOptionsNOP, + _iCoreWebView2EnvironmentTargetCompatibleBrowserVersion, + _iCoreWebView2EnvironmentOptionsNOP, + _iCoreWebView2EnvironmentOptionsAllowSingleSignOnUsingOSPrimaryAccount, + _iCoreWebView2EnvironmentOptionsNOP, + ) + + combridge.RegisterVTable[combridge.IUnknown, iCoreWebView2EnvironmentOptions2]( + "{ff85c98a-1ba7-4a6b-90c8-2b752c89e9e2}", + _iCoreWebView2EnvironmentOptions2ExclusiveUserDataFolderAccess, + _iCoreWebView2EnvironmentOptionsNOP, + ) +} +func _iCoreWebView2EnvironmentOptionsNOP(this uintptr) uintptr { + return uintptr(windows.S_FALSE) +} + +func _iCoreWebView2EnvironmentOptionsAdditionalBrowserArguments(this uintptr, value **uint16) uintptr { + v := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).AdditionalBrowserArguments() + *value = stringToOleString(v) + return uintptr(windows.S_OK) +} + +func _iCoreWebView2EnvironmentOptionsLanguage(this uintptr, value **uint16) uintptr { + args := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).Language() + *value = stringToOleString(args) + return uintptr(windows.S_OK) +} + +func _iCoreWebView2EnvironmentTargetCompatibleBrowserVersion(this uintptr, value **uint16) uintptr { + args := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).TargetCompatibleBrowserVersion() + *value = stringToOleString(args) + return uintptr(windows.S_OK) +} + +func _iCoreWebView2EnvironmentOptionsAllowSingleSignOnUsingOSPrimaryAccount(this uintptr, value *int32) uintptr { + v := combridge.Resolve[iCoreWebView2EnvironmentOptions](this).AllowSingleSignOnUsingOSPrimaryAccount() + *value = boolToInt(v) + return uintptr(windows.S_OK) +} + +func _iCoreWebView2EnvironmentOptions2ExclusiveUserDataFolderAccess(this uintptr, value *int32) uintptr { + v := combridge.Resolve[iCoreWebView2EnvironmentOptions2](this).ExclusiveUserDataFolderAccess() + *value = boolToInt(v) + return uintptr(windows.S_OK) +} + +func stringToOleString(v string) *uint16 { + wstr := utf16.Encode([]rune(v + "\x00")) + lwstr := len(wstr) + ptr := (*uint16)(coTaskMemAlloc(2 * lwstr)) + + copy(unsafe.Slice(ptr, lwstr), wstr) + + return ptr +} + +func boolToInt(v bool) int32 { + if v { + return 1 + } + return 0 +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/versions.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/find_dll.go similarity index 68% rename from v2/internal/frontend/desktop/windows/go-webview2/webview2loader/versions.go rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/find_dll.go index 910bd5c5..bf9dbc89 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/versions.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/find_dll.go @@ -1,25 +1,18 @@ -package webview2loader +package webviewloader import ( + "errors" "fmt" "os" "path/filepath" "runtime" + + "golang.org/x/sys/windows/registry" ) -// GetAvailableCoreWebView2BrowserVersionString get the browser version info including channel name. -func GetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder string) (string, error) { - if browserExecutableFolder != "" { - clientPath, err := findEmbeddedClientDll(browserExecutableFolder) - if err != nil { - return "", err - } - - return findEmbeddedBrowserVersion(clientPath) - } - - return "", fmt.Errorf("not implemented yet for empty browserExecutableFolder ") -} +var ( + errNoClientDLLFound = errors.New("no webview2 found") +) func findEmbeddedBrowserVersion(filename string) (string, error) { block, err := getFileVersionInfo(filename) @@ -63,7 +56,17 @@ func findClientDllInFolder(folder string) (string, error) { dllPath := filepath.Join(folder, "EBWebView", arch, "EmbeddedBrowserWebView.dll") if _, err := os.Stat(dllPath); err != nil { - return "", err + return "", mapFindErr(err) } return dllPath, nil } + +func mapFindErr(err error) error { + if errors.Is(err, registry.ErrNotExist) { + return errNoClientDLLFound + } + if errors.Is(err, os.ErrNotExist) { + return errNoClientDLLFound + } + return err +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/find_dll_installed.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/find_dll_installed.go new file mode 100644 index 00000000..4067fdd7 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/find_dll_installed.go @@ -0,0 +1,94 @@ +//go:build exp_gowebview2loader + +package webviewloader + +import ( + "path/filepath" + + "golang.org/x/sys/windows/registry" +) + +const ( + kNumChannels = 4 + kInstallKeyPath = "Software\\Microsoft\\EdgeUpdate\\ClientState\\" + kMinimumCompatibleVersion = "86.0.616.0" +) + +var ( + kChannelName = [kNumChannels]string{ + "", "beta", "dev", "canary", // "internal" + } + + kChannelUuid = [kNumChannels]string{ + "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "{2CD8A007-E189-409D-A2C8-9AF4EF3C72AA}", + "{0D50BFEC-CD6A-4F9A-964C-C7416E3ACB10}", + "{65C35B14-6C1D-4122-AC46-7148CC9D6497}", + //"{BE59E8FD-089A-411B-A3B0-051D9E417818}", + } + + minimumCompatibleVersion, _ = parseVersion(kMinimumCompatibleVersion) +) + +func findInstalledClientDll(preferCanary bool) (clientPath string, version *version, err error) { + for i := 0; i < kNumChannels; i++ { + channel := i + if preferCanary { + channel = (kNumChannels - 1) - i + } + + key := kInstallKeyPath + kChannelUuid[channel] + for _, checkSystem := range []bool{true, false} { + clientPath, version, err := findInstalledClientDllForChannel(key, checkSystem) + if err == errNoClientDLLFound { + continue + } + if err != nil { + return "", nil, err + } + + version.channel = kChannelName[channel] + return clientPath, version, nil + } + } + return "", nil, errNoClientDLLFound +} + +func findInstalledClientDllForChannel(subKey string, system bool) (clientPath string, clientVersion *version, err error) { + key := registry.LOCAL_MACHINE + if !system { + key = registry.CURRENT_USER + } + + regKey, err := registry.OpenKey(key, subKey, registry.READ|registry.WOW64_32KEY) + if err != nil { + return "", nil, mapFindErr(err) + } + defer regKey.Close() + + embeddedEdgeSubFolder, _, err := regKey.GetStringValue("EBWebView") + if err != nil { + return "", nil, mapFindErr(err) + } + + if embeddedEdgeSubFolder == "" { + return "", nil, errNoClientDLLFound + } + + versionString := filepath.Base(embeddedEdgeSubFolder) + version, err := parseVersion(versionString) + if err != nil { + return "", nil, errNoClientDLLFound + } + + if version.compare(minimumCompatibleVersion) < 0 { + return "", nil, errNoClientDLLFound + } + + dllPath, err := findEmbeddedClientDll(embeddedEdgeSubFolder) + if err != nil { + return "", nil, mapFindErr(err) + } + + return dllPath, &version, nil +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go similarity index 88% rename from v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module.go rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go index 86bfe027..cf66e869 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go @@ -1,3 +1,5 @@ +//go:build !exp_gowebview2loader + package webviewloader import ( @@ -8,7 +10,6 @@ import ( "unsafe" "github.com/jchv/go-winloader" - "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/webview2loader" "golang.org/x/sys/windows" ) @@ -63,20 +64,23 @@ func CompareBrowserVersions(v1 string, v2 string) (int, error) { return int(result), nil } -// GetWebviewVersion returns version of the webview2 runtime. +// GetAvailableCoreWebView2BrowserVersionString returns version of the webview2 runtime. // If path is empty, it will try to find installed webview2 is the system. // If there is no version installed, a blank string is returned. -func GetWebviewVersion(path string) (string, error) { +func GetAvailableCoreWebView2BrowserVersionString(path string) (string, error) { if path != "" { // The default implementation fails if CGO and a fixed browser path is used. It's caused by the go-winloader // which loads the native DLL from memory. // Use the new GoWebView2Loader in this case, in the future we will make GoWebView2Loader // feature-complete and remove the use of the native DLL and go-winloader. - version, err := webview2loader.GetAvailableCoreWebView2BrowserVersionString(path) - if errors.Is(err, os.ErrNotExist) { + version, err := goGetAvailableCoreWebView2BrowserVersionString(path) + if errors.Is(err, errNoClientDLLFound) { // Webview2 is not found return "", nil + } else if err != nil { + return "", err } + return version, nil } @@ -158,3 +162,12 @@ func preventEnvAndRegistryOverrides(browserFolder, userDataFolder *uint16) { os.Setenv("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", windows.UTF16PtrToString(browserFolder)) os.Setenv("WEBVIEW2_USER_DATA_FOLDER", windows.UTF16PtrToString(userDataFolder)) } + +func goGetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder string) (string, error) { + clientPath, err := findEmbeddedClientDll(browserExecutableFolder) + if err != nil { + return "", err + } + + return findEmbeddedBrowserVersion(clientPath) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_386.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_386.go similarity index 74% rename from v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_386.go rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_386.go index 174a0703..dd621b09 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_386.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_386.go @@ -1,3 +1,5 @@ +//go:build !exp_gowebview2loader + package webviewloader import _ "embed" diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_amd64.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_amd64.go similarity index 74% rename from v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_amd64.go rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_amd64.go index 53e715fd..9c780359 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_amd64.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_amd64.go @@ -1,3 +1,5 @@ +//go:build !exp_gowebview2loader + package webviewloader import _ "embed" diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_arm64.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_arm64.go similarity index 75% rename from v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_arm64.go rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_arm64.go index a09d3fae..b23215cb 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/module_arm64.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module_arm64.go @@ -1,3 +1,5 @@ +//go:build !exp_gowebview2loader + package webviewloader import _ "embed" diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/syscall.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/syscall.go similarity index 89% rename from v2/internal/frontend/desktop/windows/go-webview2/webview2loader/syscall.go rename to v2/internal/frontend/desktop/windows/go-webview2/webviewloader/syscall.go index 3b210251..ff604565 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webview2loader/syscall.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/syscall.go @@ -1,4 +1,4 @@ -package webview2loader +package webviewloader import ( "fmt" @@ -18,6 +18,9 @@ var ( procGetFileVersionInfoSize = modversion.NewProc("GetFileVersionInfoSizeW") procGetFileVersionInfo = modversion.NewProc("GetFileVersionInfoW") procVerQueryValue = modversion.NewProc("VerQueryValueW") + + modole32 = windows.NewLazySystemDLL("ole32.dll") + procCoTaskMemAlloc = modole32.NewProc("CoTaskMemAlloc") ) func getFileVersionInfo(path string) ([]byte, error) { @@ -126,3 +129,13 @@ func maskErrorSuccess(err error) error { } return err } + +func coTaskMemAlloc(size int) unsafe.Pointer { + ret, _, _ := procCoTaskMemAlloc.Call( + uintptr(size)) + + if ret == 0 { + panic("coTaskMemAlloc failed") + } + return unsafe.Pointer(ret) +} diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go new file mode 100644 index 00000000..2b36c634 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go @@ -0,0 +1,147 @@ +//go:build exp_gowebview2loader + +package webviewloader + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +// CompareBrowserVersions will compare the 2 given versions and return: +// +// -1 = v1 < v2 +// 0 = v1 == v2 +// 1 = v1 > v2 +func CompareBrowserVersions(v1 string, v2 string) (int, error) { + v, err := parseVersion(v1) + if err != nil { + return 0, fmt.Errorf("v1 invalid: %w", err) + } + + w, err := parseVersion(v2) + if err != nil { + return 0, fmt.Errorf("v2 invalid: %w", err) + } + + return v.compare(w), nil +} + +// GetAvailableCoreWebView2BrowserVersionString get the browser version info including channel name +// if it is the WebView2 Runtime. +// Channel names are Beta, Dev, and Canary. +func GetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder string) (string, error) { + if browserExecutableFolder != "" { + clientPath, err := findEmbeddedClientDll(browserExecutableFolder) + if errors.Is(err, errNoClientDLLFound) { + // Webview2 is not found + return "", nil + } else if err != nil { + return "", err + } + + return findEmbeddedBrowserVersion(clientPath) + } + + _, version, err := findInstalledClientDll(false) + if errors.Is(err, errNoClientDLLFound) { + return "", nil + } else if err != nil { + return "", err + } + + return version.String(), nil +} + +type version struct { + major int + minor int + patch int + build int + + channel string +} + +func (v version) String() string { + vv := fmt.Sprintf("%d.%d.%d.%d", v.major, v.minor, v.patch, v.build) + if v.channel != "" { + vv += " " + v.channel + } + + return vv +} + +func (v version) compare(o version) int { + if c := compareInt(v.major, o.major); c != 0 { + return c + } + if c := compareInt(v.minor, o.minor); c != 0 { + return c + } + if c := compareInt(v.patch, o.patch); c != 0 { + return c + } + return compareInt(v.build, o.build) +} + +func parseVersion(v string) (version, error) { + var p version + + // Split away channel information... + if i := strings.Index(v, " "); i > 0 { + p.channel = v[i+1:] + v = v[:i] + } + + vv := strings.Split(v, ".") + if len(vv) > 4 { + return p, fmt.Errorf("too many version parts") + } + + var err error + vv, p.major, err = parseInt(vv) + if err != nil { + return p, fmt.Errorf("bad major version: %w", err) + } + + vv, p.minor, err = parseInt(vv) + if err != nil { + return p, fmt.Errorf("bad minor version: %w", err) + } + + vv, p.patch, err = parseInt(vv) + if err != nil { + return p, fmt.Errorf("bad patch version: %w", err) + } + + _, p.build, err = parseInt(vv) + if err != nil { + return p, fmt.Errorf("bad build version: %w", err) + } + + return p, nil +} + +func parseInt(v []string) ([]string, int, error) { + if len(v) == 0 { + return nil, 0, nil + } + + p, err := strconv.ParseInt(v[0], 10, 32) + if err != nil { + return nil, 0, err + } + return v[1:], int(p), nil +} + +func compareInt(v1, v2 int) int { + if v1 == v2 { + return 0 + } + if v1 < v2 { + return -1 + } else { + return +1 + } +} diff --git a/v2/internal/wv2installer/wv2installer.go b/v2/internal/wv2installer/wv2installer.go index 1549cf1b..3f195fae 100644 --- a/v2/internal/wv2installer/wv2installer.go +++ b/v2/internal/wv2installer/wv2installer.go @@ -4,6 +4,7 @@ package wv2installer import ( "fmt" + "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/webviewloader" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/windows" @@ -32,7 +33,7 @@ func Process(appoptions *options.App) (string, error) { webviewPath = opts.WebviewBrowserPath } - installedVersion, err := webviewloader.GetWebviewVersion(webviewPath) + installedVersion, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(webviewPath) if err != nil { return "", err } diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go index 8ef7d438..2c85565a 100644 --- a/v2/pkg/commands/build/build.go +++ b/v2/pkg/commands/build/build.go @@ -2,14 +2,17 @@ package build import ( "fmt" - "github.com/wailsapp/wails/v2/internal/staticanalysis" - "github.com/wailsapp/wails/v2/pkg/commands/bindings" "log" "os" "path/filepath" "runtime" "strings" + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/staticanalysis" + "github.com/wailsapp/wails/v2/pkg/commands/bindings" + "github.com/wailsapp/wails/v2/internal/fs" "github.com/wailsapp/wails/v2/internal/shell" @@ -321,6 +324,20 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { outputLogger.Println("Done.") } + if options.Platform == "windows" { + const expWebView2Loader = "exp_gowebview2loader" + + message := "" + tags := options.UserTags + if lo.Contains(tags, expWebView2Loader) { + message = "Thanks for testing the new experimental Go native WebView2Loader. Please report your feedback and any bugs you think might be related to using the new loader: https://github.com/wailsapp/wails/issues/2004" + } else { + tags = append(tags, expWebView2Loader) + message = fmt.Sprintf("An experimental Go native WebView2Loader is available. We would love to hear your feedback about it and invite you to test it by building with `-tags %s`", strings.Join(tags, ",")) + } + println(colour.Green(" - " + message)) + } + return options.CompiledBinary, nil }