//go:build windows // +build windows package edge import ( "errors" "log" "os" "path/filepath" "sync/atomic" "syscall" "unsafe" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/internal/w32" "golang.org/x/sys/windows" ) type Rect = w32.Rect type Chromium struct { hwnd uintptr controller *ICoreWebView2Controller webview *ICoreWebView2 inited uintptr envCompleted *iCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler controllerCompleted *iCoreWebView2CreateCoreWebView2ControllerCompletedHandler webMessageReceived *iCoreWebView2WebMessageReceivedEventHandler permissionRequested *iCoreWebView2PermissionRequestedEventHandler webResourceRequested *iCoreWebView2WebResourceRequestedEventHandler acceleratorKeyPressed *ICoreWebView2AcceleratorKeyPressedEventHandler navigationCompleted *ICoreWebView2NavigationCompletedEventHandler environment *ICoreWebView2Environment padding Rect // Settings Debug bool DataPath string BrowserPath string // permissions permissions map[CoreWebView2PermissionKind]CoreWebView2PermissionState globalPermission *CoreWebView2PermissionState // Callbacks MessageCallback func(string) WebResourceRequestedCallback func(request *ICoreWebView2WebResourceRequest, args *ICoreWebView2WebResourceRequestedEventArgs) NavigationCompletedCallback func(sender *ICoreWebView2, args *ICoreWebView2NavigationCompletedEventArgs) AcceleratorKeyCallback func(uint) bool } func NewChromium() *Chromium { e := &Chromium{} /* All these handlers are passed to native code through syscalls with 'uintptr(unsafe.Pointer(handler))' and we know that a pointer to those will be kept in the native code. Furthermore these handlers als contain pointer to other Go structs like the vtable. This violates the unsafe.Pointer rule '(4) Conversion of a Pointer to a uintptr when calling syscall.Syscall.' because theres no guarantee that Go doesn't move these objects. AFAIK currently the Go runtime doesn't move HEAP objects, so we should be safe with these handlers. But they don't guarantee it, because in the future Go might use a compacting GC. There's a proposal to add a runtime.Pin function, to prevent moving pinned objects, which would allow to easily fix this issue by just pinning the handlers. The https://go-review.googlesource.com/c/go/+/367296/ should land in Go 1.19. */ e.envCompleted = newICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler(e) e.controllerCompleted = newICoreWebView2CreateCoreWebView2ControllerCompletedHandler(e) e.webMessageReceived = newICoreWebView2WebMessageReceivedEventHandler(e) e.permissionRequested = newICoreWebView2PermissionRequestedEventHandler(e) e.webResourceRequested = newICoreWebView2WebResourceRequestedEventHandler(e) e.acceleratorKeyPressed = newICoreWebView2AcceleratorKeyPressedEventHandler(e) e.navigationCompleted = newICoreWebView2NavigationCompletedEventHandler(e) e.permissions = make(map[CoreWebView2PermissionKind]CoreWebView2PermissionState) return e } func (e *Chromium) Embed(hwnd uintptr) bool { e.hwnd = hwnd dataPath := e.DataPath if dataPath == "" { currentExePath := make([]uint16, windows.MAX_PATH) _, err := windows.GetModuleFileName(windows.Handle(0), ¤tExePath[0], windows.MAX_PATH) if err != nil { // What to do here? return false } currentExeName := filepath.Base(windows.UTF16ToString(currentExePath)) dataPath = filepath.Join(os.Getenv("AppData"), currentExeName) } if e.BrowserPath != "" { if _, err := os.Stat(e.BrowserPath); errors.Is(err, os.ErrNotExist) { log.Printf("Browser path %s does not exist", e.BrowserPath) return false } } if err := createCoreWebView2EnvironmentWithOptions(e.BrowserPath, dataPath, e.envCompleted); err != nil { log.Printf("Error calling Webview2Loader: %v", err) return false } var msg w32.Msg for { if atomic.LoadUintptr(&e.inited) != 0 { break } r, _, _ := w32.User32GetMessageW.Call( uintptr(unsafe.Pointer(&msg)), 0, 0, 0, ) if r == 0 { break } w32.User32TranslateMessage.Call(uintptr(unsafe.Pointer(&msg))) w32.User32DispatchMessageW.Call(uintptr(unsafe.Pointer(&msg))) } e.Init("window.external={invoke:s=>window.chrome.webview.postMessage(s)}") return true } func (e *Chromium) SetPadding(padding Rect) { if e.padding.Top == padding.Top && e.padding.Bottom == padding.Bottom && e.padding.Left == padding.Left && e.padding.Right == padding.Right { return } e.padding = padding e.Resize() } func (e *Chromium) Resize() { if e.hwnd == 0 { return } var bounds w32.Rect w32.User32GetClientRect.Call(e.hwnd, uintptr(unsafe.Pointer(&bounds))) bounds.Top += e.padding.Top bounds.Bottom -= e.padding.Bottom bounds.Left += e.padding.Left bounds.Right -= e.padding.Right e.SetSize(bounds) } func (e *Chromium) Navigate(url string) { e.webview.vtbl.Navigate.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(url))), ) } func (e *Chromium) Init(script string) { e.webview.vtbl.AddScriptToExecuteOnDocumentCreated.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(script))), 0, ) } func (e *Chromium) Eval(script string) { _script, err := windows.UTF16PtrFromString(script) if err != nil { log.Fatal(err) } e.webview.vtbl.ExecuteScript.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(_script)), 0, ) } func (e *Chromium) Show() error { return e.controller.PutIsVisible(true) } func (e *Chromium) Hide() error { return e.controller.PutIsVisible(false) } func (e *Chromium) QueryInterface(_, _ uintptr) uintptr { return 0 } func (e *Chromium) AddRef() uintptr { return 1 } func (e *Chromium) Release() uintptr { return 1 } func (e *Chromium) EnvironmentCompleted(res uintptr, env *ICoreWebView2Environment) uintptr { 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 env.vtbl.CreateCoreWebView2Controller.Call( uintptr(unsafe.Pointer(env)), e.hwnd, uintptr(unsafe.Pointer(e.controllerCompleted)), ) return 0 } func (e *Chromium) CreateCoreWebView2ControllerCompleted(res uintptr, controller *ICoreWebView2Controller) uintptr { 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 var token _EventRegistrationToken controller.vtbl.GetCoreWebView2.Call( uintptr(unsafe.Pointer(controller)), uintptr(unsafe.Pointer(&e.webview)), ) e.webview.vtbl.AddRef.Call( uintptr(unsafe.Pointer(e.webview)), ) e.webview.vtbl.AddWebMessageReceived.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(e.webMessageReceived)), uintptr(unsafe.Pointer(&token)), ) e.webview.vtbl.AddPermissionRequested.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(e.permissionRequested)), uintptr(unsafe.Pointer(&token)), ) e.webview.vtbl.AddWebResourceRequested.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(e.webResourceRequested)), uintptr(unsafe.Pointer(&token)), ) e.webview.vtbl.AddNavigationCompleted.Call( uintptr(unsafe.Pointer(e.webview)), uintptr(unsafe.Pointer(e.navigationCompleted)), uintptr(unsafe.Pointer(&token)), ) e.controller.AddAcceleratorKeyPressed(e.acceleratorKeyPressed, &token) atomic.StoreUintptr(&e.inited, 1) return 0 } func (e *Chromium) MessageReceived(sender *ICoreWebView2, args *iCoreWebView2WebMessageReceivedEventArgs) uintptr { var message *uint16 args.vtbl.TryGetWebMessageAsString.Call( uintptr(unsafe.Pointer(args)), uintptr(unsafe.Pointer(&message)), ) if e.MessageCallback != nil { e.MessageCallback(w32.Utf16PtrToString(message)) } sender.vtbl.PostWebMessageAsString.Call( uintptr(unsafe.Pointer(sender)), uintptr(unsafe.Pointer(message)), ) windows.CoTaskMemFree(unsafe.Pointer(message)) return 0 } func (e *Chromium) SetPermission(kind CoreWebView2PermissionKind, state CoreWebView2PermissionState) { e.permissions[kind] = state } func (e *Chromium) SetGlobalPermission(state CoreWebView2PermissionState) { e.globalPermission = &state } func (e *Chromium) PermissionRequested(_ *ICoreWebView2, args *iCoreWebView2PermissionRequestedEventArgs) uintptr { var kind CoreWebView2PermissionKind args.vtbl.GetPermissionKind.Call( uintptr(unsafe.Pointer(args)), uintptr(kind), ) var result CoreWebView2PermissionState if e.globalPermission != nil { result = *e.globalPermission } else { var ok bool result, ok = e.permissions[kind] if !ok { result = CoreWebView2PermissionStateDefault } } args.vtbl.PutState.Call( uintptr(unsafe.Pointer(args)), uintptr(result), ) return 0 } func (e *Chromium) WebResourceRequested(sender *ICoreWebView2, args *ICoreWebView2WebResourceRequestedEventArgs) uintptr { req, err := args.GetRequest() if err != nil { log.Fatal(err) } defer req.Release() if e.WebResourceRequestedCallback != nil { e.WebResourceRequestedCallback(req, args) } return 0 } func (e *Chromium) AddWebResourceRequestedFilter(filter string, ctx COREWEBVIEW2_WEB_RESOURCE_CONTEXT) { err := e.webview.AddWebResourceRequestedFilter(filter, ctx) if err != nil { log.Fatal(err) } } func (e *Chromium) Environment() *ICoreWebView2Environment { return e.environment } // AcceleratorKeyPressed is called when an accelerator key is pressed. // If the AcceleratorKeyCallback method has been set, it will defer handling of the keypress // to the callback. That callback returns a bool indicating if the event was handled. func (e *Chromium) AcceleratorKeyPressed(sender *ICoreWebView2Controller, args *ICoreWebView2AcceleratorKeyPressedEventArgs) uintptr { if e.AcceleratorKeyCallback == nil { return 0 } eventKind, _ := args.GetKeyEventKind() if eventKind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN || eventKind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN { virtualKey, _ := args.GetVirtualKey() status, _ := args.GetPhysicalKeyStatus() if !status.WasKeyDown { args.PutHandled(e.AcceleratorKeyCallback(virtualKey)) return 0 } } args.PutHandled(false) return 0 } func (e *Chromium) GetSettings() (*ICoreWebViewSettings, error) { return e.webview.GetSettings() } func (e *Chromium) GetController() *ICoreWebView2Controller { return e.controller } func boolToInt(input bool) int { if input { return 1 } return 0 } func (e *Chromium) NavigationCompleted(sender *ICoreWebView2, args *ICoreWebView2NavigationCompletedEventArgs) uintptr { if e.NavigationCompletedCallback != nil { e.NavigationCompletedCallback(sender, args) } return 0 } func (e *Chromium) NotifyParentWindowPositionChanged() error { //It looks like the wndproc function is called before the controller initialization is complete. //Because of this the controller is nil if e.controller == nil { return nil } return e.controller.NotifyParentWindowPositionChanged() } func (e *Chromium) Focus() { err := e.controller.MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC) if err != nil { log.Fatal(err) } } func (e *Chromium) PutZoomFactor(zoomFactor float64) { err := e.controller.PutZoomFactor(zoomFactor) if err != nil { log.Fatal(err) } } func (e *Chromium) OpenDevToolsWindow() { e.webview.OpenDevToolsWindow() }