[webview2loader] Add full featured go implementation (#1974)

* [webview2loader] Add full featured go implementation

The new go loader can be activated with the exp_gowebview2loader build tag.

* [build] Add information for using the new webvie2loader
This commit is contained in:
stffabi
2022-10-22 00:29:16 +11:00
committed by GitHub
parent 38f6b8787f
commit 0a20c8db96
25 changed files with 1438 additions and 73 deletions
@@ -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
}
@@ -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())
}
@@ -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))
}
@@ -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")
}
}
@@ -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
}
@@ -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
@@ -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."
@@ -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)
}
@@ -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
}
@@ -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
@@ -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
@@ -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(&params)
}
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,
&params, 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)
}
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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)
}
@@ -1,3 +1,5 @@
//go:build !exp_gowebview2loader
package webviewloader
import _ "embed"
@@ -1,3 +1,5 @@
//go:build !exp_gowebview2loader
package webviewloader
import _ "embed"

Some files were not shown because too many files have changed in this diff Show More