base webview project

This commit is contained in:
2026-06-18 21:34:25 +02:00
commit b08077eba5
17 changed files with 2017 additions and 0 deletions

170
README_webview.md Normal file
View File

@@ -0,0 +1,170 @@
# webview — низкоуровневый WinAPI webview на Go (Windows XP)
Пакет `nati-launch/webview` создаёт нативное окно Windows, целиком занятое
веб-вью, средствами **чистого WinAPI/OLE** через стандартный пакет `syscall`.
Никаких внешних зависимостей и DLL: используется встроенный в Windows
управляющий элемент Internet Explorer (`shdocvw` / `IWebBrowser2`).
Это Go-порт примера `WinAPI-Embed-Browser/embed-browser.c`, упрощённый до
«сплошного» окна (без кнопок и прочих Win32-контролов — браузер растягивается
на весь клиентский прямоугольник).
## Совместимость
- **Целевая платформа:** Windows XP и новее.
- **Тулчейн:** Go **1.10.8** — последняя версия с поддержкой XP. ⚠️ Это
**обязательно**: бинарники, собранные Go 1.11+, на XP не запускаются и дают
ошибку «не является допустимым приложением Win32» — независимо от `GOARCH`.
Системный `go` (если он новее 1.10) использовать **нельзя**.
- **Архитектура:** `GOARCH=386` (32-бит, работает и на x64 через WOW64).
- Go 1.10.8 **не поддерживает модули** — сборка ведётся в режиме **GOPATH**.
> Компилировать **на самой XP не нужно** — это обычная кросс-компиляция с
> Windows 10/11 старым тулчейном.
## Установка Go 1.10.8
1. Скачать `go1.10.8.windows-amd64.zip` со страницы релизов https://go.dev/dl/
(есть в архиве старых версий).
2. Распаковать, например, в `C:\go1.10.8` (там должен оказаться
`C:\go1.10.8\bin\go.exe`).
## GOPATH
Исходники уже лежат в стандартной структуре `src/nati-launch`. `build_xp.bat`
назначает корень проекта как `GOPATH` и собирает код прямо на месте — без
копирования, временных папок и симлинков. Кэш Go сохраняется между сборками.
## Сборка и запуск
`build_xp.bat` уже всё делает: проверяет наличие Go 1.10.8, выставляет
`GOROOT/GOOS/GOARCH/CGO_ENABLED` и собирает GUI-приложение.
```bat
set GO110_ROOT=C:\go1.10.8
build_xp.bat
```
`build_xp.bat` собирает `nati-launch.exe` как 32-битное GUI-приложение
(`-ldflags "-H windowsgui"`, без консольного окна).
### Проверить, каким Go собран .exe
```bat
findstr /M /C:"go1.10.8" nati-launch.exe
```
Команда должна вывести имя `nati-launch.exe`. Старый Go ещё не записывал
современный блок build info, поэтому `go version nati-launch.exe` его не читает.
## Пример использования
```go
package main
import "nati-launch/webview"
func main() {
w, err := webview.New(webview.Options{
Title: "My App",
Width: 1024,
Height: 768,
Resizable: true,
DisableMenu: true,
URL: "https://example.com",
})
if err != nil {
panic(err)
}
w.Show()
w.Run() // блокирует до закрытия окна
}
```
## API
| Функция | Назначение |
|---|---|
| `New(Options) (*Window, error)` | регистрация класса, создание окна, встраивание браузера |
| `(*Window) Run()` | цикл сообщений Win32 (блокирует) |
| `(*Window) Navigate(url string) error` | перейти по URL |
| `(*Window) LoadHTML(html string) error` | показать HTML-строку (через временный файл) |
| `(*Window) GoBack() / GoForward() / Refresh() / Stop()` | команды навигации |
| `(*Window) Show() / Hide()` | показать / скрыть окно |
| `(*Window) SetTitle(s) / Resize(w,h) / Center() / Close()` | управление окном |
| `(*Window) Bind(name, fn)` | пробросить Go-функцию в JS как `window.external.<name>(...)` |
### `Options`
| Поле | Описание |
|---|---|
| `Title` | заголовок окна |
| `Width`, `Height` | размер клиентской области (0 → 1024×768) |
| `X`, `Y` | позиция (0,0 → по центру экрана) |
| `URL` | URL для загрузки при старте |
| `HTML` | HTML для показа при старте (приоритетнее `URL`) |
| `Resizable` | разрешить изменение размера |
| `DisableMenu` | отключить контекстное меню IE |
| `HideScrollbars` | скрыть полосы прокрутки страницы |
## Мост JS → Go
Go-функции пробрасываются в страницу через `IDocHostUIHandler::GetExternal`
(реализован свой `IDispatch`). Из JS они доступны как
`window.external.<имя>(аргументы)`. Аргументы приходят в Go строками, а
возвращённая строка отдаётся обратно в JS как результат вызова.
```go
w, _ := webview.New(webview.Options{Width: 900, Height: 600})
// привязать ДО загрузки страницы, которая этим пользуется
w.Bind("report", func(args []string) string {
// args[0] — то, что передали из JS
return "ack: " + args[0]
})
w.LoadHTML(`<button onclick="
var r = window.external.report('hello from JS');
alert(r); // -> ack: hello from JS
">call Go</button>`)
w.Show()
w.Run()
```
Готовый пример (счётчик на JS + кнопка, вызывающая Go) — в
`src/nati-launch/cmd/nati-launch/main.go`.
JS должен быть совместим с IE (используйте `onclick`/`attachEvent`,
`setInterval`, без `addEventListener`).
## Встроенный frontend
HTML, CSS, JavaScript, изображения и другие ресурсы лежат в папке
`src/nati-launch/frontend/`.
Перед обычной сборкой обновите сгенерированный Go-файл:
```powershell
cd src/nati-launch
go generate ./cmd/nati-launch
go build -o ../../nati-launch.exe ./cmd/nati-launch
```
Генератор `src/nati-launch/tools/frontendgen` рекурсивно упаковывает все файлы в
`src/nati-launch/cmd/nati-launch/frontend_gen.go`. При запуске `LoadFiles` отдаёт ресурсы прямо
из памяти через зарегистрированный внутри процесса URLMon-протокол `nati...://`.
На диск ничего не распаковывается и сетевой порт не открывается. Относительные
ссылки внутри frontend продолжают работать, а рядом с готовым EXE папка
`frontend/` не нужна. URLMon и `IInternetProtocol` доступны в Windows XP;
`build_xp.bat` запускает кодген автоматически.
## Важные замечания
- **Один поток / STA.** `New` вызывает `runtime.LockOSThread` и `OleInitialize`;
вызывайте `New` и `Run` из одной и той же горутины.
- **Движок Trident.** На XP это IE6–IE8; современные сайты могут рендериться в
режиме совместимости. Для локального HTML и простых страниц это ожидаемо.
- По умолчанию IE рендерит во встроенном приложении в режиме IE7. При
необходимости задайте режим эмуляции через реестр
(`HKCU\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION`,
значение = имя вашего `.exe`, dword `8888` для IE8 на XP).
```

57
build_xp.bat Normal file
View File

@@ -0,0 +1,57 @@
@echo off
REM ===========================================================================
REM Build nati-launch for Windows XP.
REM
REM IMPORTANT: Windows XP is only supported by the Go 1.10.x toolchain.
REM Binaries produced by Go 1.11+ fail on XP with "is not a valid Win32
REM application", regardless of GOARCH. So the SYSTEM `go` (likely a modern
REM version) MUST NOT be used here.
REM
REM Requirements:
REM * Go 1.10.8 unpacked somewhere, e.g. C:\go1.10.8
REM * Sources located at .\src\nati-launch (standard GOPATH layout)
REM
REM Set GO110_ROOT to your Go 1.10.8 folder (or edit the default below), then
REM just run build_xp.bat.
REM ===========================================================================
if "%GO110_ROOT%"=="" set GO110_ROOT=C:\go1.10.8
if not exist "%GO110_ROOT%\bin\go.exe" (
echo ERROR: Go 1.10.8 not found at "%GO110_ROOT%".
echo Download go1.10.8.windows-amd64.zip from https://go.dev/dl/ and unpack it,
echo then set GO110_ROOT to that folder, e.g.: set GO110_ROOT=C:\go1.10.8
exit /b 1
)
if not exist "%~dp0src\nati-launch\cmd\nati-launch\main.go" (
echo ERROR: Sources not found at "%~dp0src\nati-launch".
exit /b 1
)
set GOPATH=%~dp0
set GOROOT=%GO110_ROOT%
set PATH=%GO110_ROOT%\bin;%PATH%
set GOOS=windows
set GOARCH=386
set CGO_ENABLED=0
echo Using toolchain:
"%GO110_ROOT%\bin\go.exe" version
echo GOPATH=%GOPATH%
echo.
"%GO110_ROOT%\bin\go.exe" generate nati-launch/cmd/nati-launch
if errorlevel 1 goto fail
"%GO110_ROOT%\bin\go.exe" build -ldflags "-H windowsgui" -o "%~dp0nati-launch.exe" nati-launch/cmd/nati-launch
if errorlevel 1 goto fail
echo.
echo Built nati-launch.exe (32-bit, GUI subsystem, Go 1.10.8 for Windows XP)
exit /b 0
:fail
echo Build failed.
exit /b 1

View File

@@ -0,0 +1,9 @@
// Code generated by frontendgen; DO NOT EDIT.
package main
var frontendFiles = map[string][]byte{
"app.js": []byte("// Keep this file compatible with the IE engine used by the native WebView.\nvar counter = 0;\n\nfunction tick() {\n counter = counter + 1;\n document.getElementById('count').innerHTML = counter;\n}\n\nsetInterval(tick, 1000);\n\nfunction sendToGo() {\n try {\n var reply = window.external.report(counter);\n document.getElementById('ack').innerHTML = 'Go replied: ' + reply;\n } catch (e) {\n document.getElementById('ack').innerHTML = 'bridge error: ' + e.message;\n }\n}\n"),
"index.html": []byte("<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>nati-launch</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n</head>\n<body>\n <h1>JS &harr; Go bridge</h1>\n\n <p>JS counter (tick every second):</p>\n <div id=\"count\">0</div>\n\n <button onclick=\"sendToGo()\">Send counter to Go</button>\n <div id=\"ack\">&nbsp;</div>\n\n <script src=\"app.js\"></script>\n</body>\n</html>\n"),
"styles.css": []byte("html,\nbody {\n overflow: hidden;\n}\n\nbody {\n margin: 40px;\n font-family: Tahoma, sans-serif;\n}\n\n#count {\n color: #225;\n font-size: 48px;\n font-weight: bold;\n}\n\nbutton {\n margin-top: 12px;\n padding: 6px 14px;\n font-size: 16px;\n}\n\n#ack {\n margin-top: 16px;\n color: #060;\n}\n"),
}

View File

@@ -0,0 +1,46 @@
package main
import (
"fmt"
"strconv"
"nati-launch/webview"
)
//go:generate go run ../../tools/frontendgen/main.go -src ../../frontend -out frontend_gen.go -package main
func main() {
w, err := webview.New(webview.Options{
Title: "nati-launch",
Width: 900,
Height: 600,
Resizable: true,
DisableMenu: true,
HideScrollbars: true,
})
if err != nil {
panic(err)
}
calls := 0
// Register a Go function reachable from JS as window.external.report(...).
w.Bind("report", func(args []string) string {
calls++
value := ""
if len(args) > 0 {
value = args[0]
}
// Visible proof on the Go side: update the window title.
w.SetTitle("nati-launch — counter=" + value + " (call #" + strconv.Itoa(calls) + ")")
fmt.Printf("[go] report() called: counter=%s, call #%d\n", value, calls)
return "ack #" + strconv.Itoa(calls) + " for counter " + value
})
// Load the generated frontend bundle after binding so JS can call Go.
if err := w.LoadFiles(frontendFiles, "index.html"); err != nil {
panic(err)
}
w.Show()
w.Run()
}

View File

@@ -0,0 +1,18 @@
// Keep this file compatible with the IE engine used by the native WebView.
var counter = 0;
function tick() {
counter = counter + 1;
document.getElementById('count').innerHTML = counter;
}
setInterval(tick, 1000);
function sendToGo() {
try {
var reply = window.external.report(counter);
document.getElementById('ack').innerHTML = 'Go replied: ' + reply;
} catch (e) {
document.getElementById('ack').innerHTML = 'bridge error: ' + e.message;
}
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>nati-launch</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>JS &harr; Go bridge</h1>
<p>JS counter (tick every second):</p>
<div id="count">0</div>
<button onclick="sendToGo()">Send counter to Go</button>
<div id="ack">&nbsp;</div>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
html,
body {
overflow: hidden;
}
body {
margin: 40px;
font-family: Tahoma, sans-serif;
}
#count {
color: #225;
font-size: 48px;
font-weight: bold;
}
button {
margin-top: 12px;
padding: 6px 14px;
font-size: 16px;
}
#ack {
margin-top: 16px;
color: #060;
}

View File

@@ -0,0 +1,83 @@
// Command frontendgen turns a directory into a Go map of embedded files.
// It intentionally uses only APIs available in Go 1.10.
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
)
func main() {
src := flag.String("src", "", "directory to bundle")
out := flag.String("out", "", "generated Go file")
pkg := flag.String("package", "main", "generated package name")
flag.Parse()
if *src == "" || *out == "" {
fmt.Fprintln(os.Stderr, "frontendgen: -src and -out are required")
os.Exit(2)
}
files := make(map[string][]byte)
var names []string
err := filepath.Walk(*src, func(name string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if !info.Mode().IsRegular() {
return nil
}
rel, err := filepath.Rel(*src, name)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}
files[rel] = data
names = append(names, rel)
return nil
})
if err != nil {
fmt.Fprintln(os.Stderr, "frontendgen:", err)
os.Exit(1)
}
if len(names) == 0 {
fmt.Fprintln(os.Stderr, "frontendgen: source directory is empty")
os.Exit(1)
}
sort.Strings(names)
var generated bytes.Buffer
fmt.Fprintln(&generated, "// Code generated by frontendgen; DO NOT EDIT.")
fmt.Fprintln(&generated)
fmt.Fprintf(&generated, "package %s\n\n", *pkg)
fmt.Fprintln(&generated, "var frontendFiles = map[string][]byte{")
for _, name := range names {
fmt.Fprintf(&generated, "\t%s: []byte(%s),\n", strconv.Quote(name), strconv.Quote(string(files[name])))
}
fmt.Fprintln(&generated, "}")
formatted, err := format.Source(generated.Bytes())
if err != nil {
fmt.Fprintln(os.Stderr, "frontendgen: format generated source:", err)
os.Exit(1)
}
current, _ := ioutil.ReadFile(*out)
if bytes.Equal(current, formatted) {
return
}
if err := ioutil.WriteFile(*out, formatted, 0644); err != nil {
fmt.Fprintln(os.Stderr, "frontendgen:", err)
os.Exit(1)
}
}

View File

@@ -0,0 +1,320 @@
package webview
import (
"syscall"
"unsafe"
)
// HRESULT values.
const (
sOK uintptr = 0x00000000
sFalse uintptr = 0x00000001
eNotImpl uintptr = 0x80004001
eNoInterface uintptr = 0x80004002
ePointer uintptr = 0x80004003
classENoAggregation uintptr = 0x80040110
inetEResourceNotFound uintptr = 0x800C0005
)
const ptrSize = unsafe.Sizeof(uintptr(0))
// comCall invokes method number idx in the vtable of the COM object pointed to
// by this. The first argument passed to the method is always this itself.
func comCall(this uintptr, idx int, args ...uintptr) uintptr {
vtbl := *(*uintptr)(unsafe.Pointer(this))
fn := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*ptrSize))
full := make([]uintptr, 0, len(args)+1)
full = append(full, this)
full = append(full, args...)
nargs := uintptr(len(full))
for len(full) < 3 {
full = append(full, 0)
}
var r uintptr
switch {
case nargs <= 3:
r, _, _ = syscall.Syscall(fn, nargs, full[0], full[1], full[2])
case nargs <= 6:
for len(full) < 6 {
full = append(full, 0)
}
r, _, _ = syscall.Syscall6(fn, nargs, full[0], full[1], full[2], full[3], full[4], full[5])
default:
for len(full) < 9 {
full = append(full, 0)
}
r, _, _ = syscall.Syscall9(fn, nargs, full[0], full[1], full[2], full[3], full[4], full[5], full[6], full[7], full[8])
}
return r
}
// release is a convenience wrapper for IUnknown::Release (vtable index 2).
func release(this uintptr) {
if this != 0 {
comCall(this, 2)
}
}
// The host-side COM objects. Each starts with a pointer to its vtable so the
// browser can call our functions, followed by a back-pointer to the owning
// Window so the callbacks can recover their context. (Go's GC does not move
// heap objects, so the addresses we hand to the browser stay valid as long as
// the Window keeps these structs alive.)
type oleClientSite struct {
vtbl uintptr
w *Window
}
type oleInPlaceSite struct {
vtbl uintptr
w *Window
}
type oleInPlaceFrame struct {
vtbl uintptr
w *Window
}
type docHostUIHandler struct {
vtbl uintptr
w *Window
}
// oleInPlaceFrameInfo mirrors OLEINPLACEFRAMEINFO.
type oleInPlaceFrameInfo struct {
cb uint32
fMDIApp int32
hwndFrame uintptr
haccel uintptr
cAccelEntries uint32
}
// docHostUIInfo mirrors DOCHOSTUIINFO.
type docHostUIInfo struct {
cbSize uint32
dwFlags uint32
dwDoubleClick uint32
pchHostCss *uint16
pchHostNS *uint16
}
const (
dochostuiflagNo3DBorder = 0x00000004
dochostuiflagScrollNo = 0x00000040 // turns off scrollbars (content is clipped, not scrollable)
)
// Vtables are built once and kept alive for the lifetime of the process.
var (
clientSiteVtbl []uintptr
inPlaceSiteVtbl []uintptr
inPlaceFrameVtbl []uintptr
docHostVtbl []uintptr
)
func init() {
buildVtables()
buildExternalVtbl()
}
func cb(fn interface{}) uintptr { return syscall.NewCallback(fn) }
func buildVtables() {
// IOleClientSite : IUnknown
clientSiteVtbl = []uintptr{
cb(csQueryInterface),
cb(addRef),
cb(siteRelease),
cb(func(this uintptr) uintptr { return eNotImpl }), // SaveObject
cb(func(this, a, b, c uintptr) uintptr { return eNotImpl }), // GetMoniker
cb(func(this, ppContainer uintptr) uintptr {
*(*uintptr)(unsafe.Pointer(ppContainer)) = 0
return eNoInterface
}), // GetContainer
cb(func(this uintptr) uintptr { return sOK }), // ShowObject
cb(func(this, fShow uintptr) uintptr { return sOK }), // OnShowWindow
cb(func(this uintptr) uintptr { return eNotImpl }), // RequestNewObjectLayout
}
// IOleInPlaceSite : IOleWindow : IUnknown
inPlaceSiteVtbl = []uintptr{
cb(ipsQueryInterface),
cb(addRef),
cb(siteRelease),
cb(ipsGetWindow), // GetWindow
cb(func(this, b uintptr) uintptr { return sOK }), // ContextSensitiveHelp
cb(func(this uintptr) uintptr { return sOK }), // CanInPlaceActivate
cb(func(this uintptr) uintptr { return sOK }), // OnInPlaceActivate
cb(func(this uintptr) uintptr { return sOK }), // OnUIActivate
cb(ipsGetWindowContext), // GetWindowContext
cb(func(this, cx, cy uintptr) uintptr { return sOK }), // Scroll (SIZE passed by value = 2 dwords)
cb(func(this, a uintptr) uintptr { return sOK }), // OnUIDeactivate
cb(func(this uintptr) uintptr { return sOK }), // OnInPlaceDeactivate
cb(func(this uintptr) uintptr { return sOK }), // DiscardUndoState
cb(func(this uintptr) uintptr { return sOK }), // DeactivateAndUndo
cb(func(this, a uintptr) uintptr { return sOK }), // OnPosRectChange
}
// IOleInPlaceFrame : IOleInPlaceUIWindow : IOleWindow : IUnknown
inPlaceFrameVtbl = []uintptr{
cb(ipfQueryInterface),
cb(addRef),
cb(siteRelease),
cb(ipfGetWindow), // GetWindow
cb(func(this, b uintptr) uintptr { return sOK }), // ContextSensitiveHelp
cb(func(this, a uintptr) uintptr { return eNotImpl }), // GetBorder
cb(func(this, a uintptr) uintptr { return eNotImpl }), // RequestBorderSpace
cb(func(this, a uintptr) uintptr { return sOK }), // SetBorderSpace
cb(func(this, a, b uintptr) uintptr { return sOK }), // SetActiveObject
cb(func(this, a, b uintptr) uintptr { return sOK }), // InsertMenus
cb(func(this, a, b, c uintptr) uintptr { return sOK }), // SetMenu
cb(func(this, a uintptr) uintptr { return sOK }), // RemoveMenus
cb(func(this, a uintptr) uintptr { return sOK }), // SetStatusText
cb(func(this, a uintptr) uintptr { return sOK }), // EnableModeless
cb(func(this, a, b uintptr) uintptr { return sFalse }), // TranslateAccelerator
}
// IDocHostUIHandler : IUnknown
docHostVtbl = []uintptr{
cb(duiQueryInterface),
cb(addRef),
cb(siteRelease),
cb(duiShowContextMenu), // ShowContextMenu (this + 4)
cb(duiGetHostInfo), // GetHostInfo (this + 1)
cb(func(this, a, b, c, d, e uintptr) uintptr { return sOK }), // ShowUI (this + 5)
cb(func(this uintptr) uintptr { return sOK }), // HideUI
cb(func(this uintptr) uintptr { return sOK }), // UpdateUI
cb(func(this, a uintptr) uintptr { return sOK }), // EnableModeless
cb(func(this, a uintptr) uintptr { return sOK }), // OnDocWindowActivate
cb(func(this, a uintptr) uintptr { return sOK }), // OnFrameWindowActivate
cb(func(this, a, b, c uintptr) uintptr { return sOK }), // ResizeBorder (this + 3)
cb(func(this, a, b, c uintptr) uintptr { return sFalse }), // TranslateAccelerator (this + 3)
cb(func(this, a, b uintptr) uintptr { return sFalse }), // GetOptionKeyPath (this + 2)
cb(func(this, a, b uintptr) uintptr { return eNotImpl }), // GetDropTarget (this + 2)
cb(duiGetExternal), // GetExternal (this + 1)
cb(duiTranslateUrl), // TranslateUrl (this + 3)
cb(func(this, a, b uintptr) uintptr { *(*uintptr)(unsafe.Pointer(b)) = 0; return sFalse }), // FilterDataObject (this + 2)
}
}
// --- shared IUnknown callbacks ---
func addRef(this uintptr) uintptr { return 1 }
func siteRelease(this uintptr) uintptr { return 1 }
func vtblAddr(s []uintptr) uintptr { return uintptr(unsafe.Pointer(&s[0])) }
// queryInterface implements the shared logic: every one of our host objects can
// hand back any of the interfaces the browser asks for, all rooted at the owning
// Window's site objects.
func queryInterface(w *Window, riid, ppv uintptr) uintptr {
iid := (*GUID)(unsafe.Pointer(riid))
out := (*uintptr)(unsafe.Pointer(ppv))
switch {
case guidEqual(iid, &IID_IUnknown), guidEqual(iid, &IID_IOleClientSite):
*out = uintptr(unsafe.Pointer(w.clientSite))
case guidEqual(iid, &IID_IOleInPlaceSite):
*out = uintptr(unsafe.Pointer(w.inPlaceSite))
case guidEqual(iid, &IID_IOleInPlaceFrame):
*out = uintptr(unsafe.Pointer(w.inPlaceFrame))
case guidEqual(iid, &IID_IDocHostUIHandler):
*out = uintptr(unsafe.Pointer(w.docHost))
default:
*out = 0
return eNoInterface
}
return sOK
}
func csQueryInterface(this, riid, ppv uintptr) uintptr {
return queryInterface((*oleClientSite)(unsafe.Pointer(this)).w, riid, ppv)
}
func ipsQueryInterface(this, riid, ppv uintptr) uintptr {
return queryInterface((*oleInPlaceSite)(unsafe.Pointer(this)).w, riid, ppv)
}
func ipfQueryInterface(this, riid, ppv uintptr) uintptr {
return queryInterface((*oleInPlaceFrame)(unsafe.Pointer(this)).w, riid, ppv)
}
func duiQueryInterface(this, riid, ppv uintptr) uintptr {
return queryInterface((*docHostUIHandler)(unsafe.Pointer(this)).w, riid, ppv)
}
// --- IOleInPlaceSite ---
func ipsGetWindow(this, phwnd uintptr) uintptr {
w := (*oleInPlaceSite)(unsafe.Pointer(this)).w
*(*uintptr)(unsafe.Pointer(phwnd)) = w.hwnd
return sOK
}
func ipsGetWindowContext(this, ppFrame, ppDoc, lprcPosRect, lprcClipRect, pInfo uintptr) uintptr {
w := (*oleInPlaceSite)(unsafe.Pointer(this)).w
*(*uintptr)(unsafe.Pointer(ppFrame)) = uintptr(unsafe.Pointer(w.inPlaceFrame))
*(*uintptr)(unsafe.Pointer(ppDoc)) = 0
rc := getClientRect(w.hwnd)
if lprcPosRect != 0 {
*(*rect)(unsafe.Pointer(lprcPosRect)) = rc
}
if lprcClipRect != 0 {
*(*rect)(unsafe.Pointer(lprcClipRect)) = rc
}
if pInfo != 0 {
info := (*oleInPlaceFrameInfo)(unsafe.Pointer(pInfo))
info.cb = uint32(unsafe.Sizeof(oleInPlaceFrameInfo{}))
info.fMDIApp = 0
info.hwndFrame = w.hwnd
info.haccel = 0
info.cAccelEntries = 0
}
return sOK
}
// --- IOleInPlaceFrame ---
func ipfGetWindow(this, phwnd uintptr) uintptr {
w := (*oleInPlaceFrame)(unsafe.Pointer(this)).w
*(*uintptr)(unsafe.Pointer(phwnd)) = w.hwnd
return sOK
}
// --- IDocHostUIHandler ---
func duiShowContextMenu(this, dwID, ppt, pcmdReserved, pdispReserved uintptr) uintptr {
w := (*docHostUIHandler)(unsafe.Pointer(this)).w
if w.disableMenu {
return sOK // we "handled" it -> default IE context menu is suppressed
}
return sFalse // let the browser show its own menu
}
func duiGetHostInfo(this, pInfo uintptr) uintptr {
w := (*docHostUIHandler)(unsafe.Pointer(this)).w
info := (*docHostUIInfo)(unsafe.Pointer(pInfo))
info.cbSize = uint32(unsafe.Sizeof(docHostUIInfo{}))
info.dwFlags = dochostuiflagNo3DBorder
if w.hideScroll {
info.dwFlags |= dochostuiflagScrollNo
}
info.dwDoubleClick = 0
return sOK
}
func duiTranslateUrl(this, dwTranslate, pchURLIn, ppchURLOut uintptr) uintptr {
*(*uintptr)(unsafe.Pointer(ppchURLOut)) = 0
return sFalse // do not modify the URL
}
func duiGetExternal(this, ppDisp uintptr) uintptr {
w := (*docHostUIHandler)(unsafe.Pointer(this)).w
if w.external != nil {
*(*uintptr)(unsafe.Pointer(ppDisp)) = uintptr(unsafe.Pointer(w.external))
return sOK
}
*(*uintptr)(unsafe.Pointer(ppDisp)) = 0
return sOK
}

View File

@@ -0,0 +1,153 @@
package webview
import (
"syscall"
"unsafe"
)
// external.go implements a minimal IDispatch object that the hosted page can
// reach as window.external.<name>(...). Go functions are registered with
// Window.Bind; the browser resolves a name to a DISPID via GetIDsOfNames and
// then calls Invoke, which we route to the bound Go function.
// VARIANT change/clear helpers (oleaut32).
var (
procVariantInit = oleaut32.NewProc("VariantInit")
procVariantClear = oleaut32.NewProc("VariantClear")
procVariantChangeType = oleaut32.NewProc("VariantChangeType")
)
const (
vtBSTR = 8
dispErrUnknownName = 0x80020006
dispErrMemberNotFound = 0x80020003
)
// externalDispatch is the COM object handed to the browser via GetExternal.
type externalDispatch struct {
vtbl uintptr
w *Window
}
var externalVtbl []uintptr
func buildExternalVtbl() {
externalVtbl = []uintptr{
cb(extQueryInterface), // 0 QueryInterface
cb(addRef), // 1 AddRef
cb(siteRelease), // 2 Release
cb(func(this, pctinfo uintptr) uintptr { // 3 GetTypeInfoCount
*(*uint32)(unsafe.Pointer(pctinfo)) = 0
return sOK
}),
cb(func(this, a, b, c uintptr) uintptr { return eNotImpl }), // 4 GetTypeInfo
cb(extGetIDsOfNames), // 5 GetIDsOfNames
cb(extInvoke), // 6 Invoke
}
}
func extQueryInterface(this, riid, ppv uintptr) uintptr {
iid := (*GUID)(unsafe.Pointer(riid))
if guidEqual(iid, &IID_IUnknown) || guidEqual(iid, &IID_IDispatch) {
*(*uintptr)(unsafe.Pointer(ppv)) = this
return sOK
}
*(*uintptr)(unsafe.Pointer(ppv)) = 0
return eNoInterface
}
func extGetIDsOfNames(this, riid, rgszNames, cNames, lcid, rgDispId uintptr) uintptr {
w := (*externalDispatch)(unsafe.Pointer(this)).w
name := utf16PtrToString(*(*uintptr)(unsafe.Pointer(rgszNames)))
w.mu.Lock()
id, ok := w.bindIDs[name]
w.mu.Unlock()
if !ok {
*(*int32)(unsafe.Pointer(rgDispId)) = -1
return dispErrUnknownName
}
*(*int32)(unsafe.Pointer(rgDispId)) = id
return sOK
}
func extInvoke(this, dispid, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr uintptr) uintptr {
w := (*externalDispatch)(unsafe.Pointer(this)).w
id := int32(dispid)
w.mu.Lock()
fn := w.bindings[w.idNames[id]]
w.mu.Unlock()
if fn == nil {
return dispErrMemberNotFound
}
var args []string
if pDispParams != 0 {
dp := (*dispParams)(unsafe.Pointer(pDispParams))
n := dp.cArgs
args = make([]string, n)
// Arguments arrive in reverse order in rgvarg.
for i := uint32(0); i < n; i++ {
vptr := dp.rgvarg + uintptr(n-1-i)*unsafe.Sizeof(variant{})
args[i] = variantToString(vptr)
}
}
res := fn(args)
if pVarResult != 0 {
vr := (*variant)(unsafe.Pointer(pVarResult))
vr.vt = vtBSTR
vr.val = sysAllocString(res)
}
return sOK
}
// variantToString coerces a VARIANT to a Go string.
func variantToString(v uintptr) string {
src := (*variant)(unsafe.Pointer(v))
if src.vt == vtBSTR {
return bstrToString(src.val)
}
var dest variant
procVariantInit.Call(uintptr(unsafe.Pointer(&dest)))
hr, _, _ := procVariantChangeType.Call(uintptr(unsafe.Pointer(&dest)), v, 0, vtBSTR)
if hr != 0 {
return ""
}
s := bstrToString(dest.val)
procVariantClear.Call(uintptr(unsafe.Pointer(&dest)))
return s
}
func bstrToString(b uintptr) string {
if b == 0 {
return ""
}
n := *(*uint32)(unsafe.Pointer(b - 4)) / 2 // byte length prefix -> char count
if n == 0 {
return ""
}
buf := make([]uint16, n)
for i := uint32(0); i < n; i++ {
buf[i] = *(*uint16)(unsafe.Pointer(b + uintptr(i)*2))
}
return syscall.UTF16ToString(buf)
}
func utf16PtrToString(p uintptr) string {
if p == 0 {
return ""
}
var buf []uint16
for i := uintptr(0); ; i += 2 {
c := *(*uint16)(unsafe.Pointer(p + i))
if c == 0 {
break
}
buf = append(buf, c)
}
return syscall.UTF16ToString(buf)
}

View File

@@ -0,0 +1,43 @@
package webview
// GUID is the Win32 GUID / IID / CLSID structure.
type GUID struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
func newGUID(d1 uint32, d2, d3 uint16, b0, b1, b2, b3, b4, b5, b6, b7 byte) GUID {
return GUID{d1, d2, d3, [8]byte{b0, b1, b2, b3, b4, b5, b6, b7}}
}
func guidEqual(a, b *GUID) bool {
if a.Data1 != b.Data1 || a.Data2 != b.Data2 || a.Data3 != b.Data3 {
return false
}
for i := 0; i < 8; i++ {
if a.Data4[i] != b.Data4[i] {
return false
}
}
return true
}
// COM identifiers needed to host the Internet Explorer WebBrowser control.
var (
CLSID_WebBrowser = newGUID(0x8856F961, 0x340A, 0x11D0, 0xA9, 0x6B, 0x00, 0xC0, 0x4F, 0xD7, 0x05, 0xA2)
IID_IUnknown = newGUID(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IClassFactory = newGUID(0x00000001, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IOleObject = newGUID(0x00000112, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IDispatch = newGUID(0x00020400, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IOleClientSite = newGUID(0x00000118, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IOleInPlaceSite = newGUID(0x00000119, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IOleInPlaceFrame = newGUID(0x00000116, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IOleInPlaceObject = newGUID(0x00000113, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
IID_IDocHostUIHandler = newGUID(0xBD3F23C0, 0xD43E, 0x11CF, 0x89, 0x3B, 0x00, 0xAA, 0x00, 0xBD, 0xCE, 0x1A)
IID_IWebBrowser2 = newGUID(0xD30C1661, 0xCDAF, 0x11D0, 0x8A, 0x3E, 0x00, 0xC0, 0x4F, 0xC9, 0xE2, 0x6E)
IID_IInternetProtocolRoot = newGUID(0x79EAC9E3, 0xBAF9, 0x11CE, 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B)
IID_IInternetProtocol = newGUID(0x79EAC9E4, 0xBAF9, 0x11CE, 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B)
)

View File

@@ -0,0 +1,249 @@
package webview
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"sync"
"syscall"
"unsafe"
)
// oleaut32 is used for BSTR allocation.
var (
oleaut32 = syscall.NewLazyDLL("oleaut32.dll")
procSysAllocString = oleaut32.NewProc("SysAllocString")
procSysFreeString = oleaut32.NewProc("SysFreeString")
)
// IOleObject vtable indices.
const (
idxSetClientSite = 3
idxSetHostNames = 5
idxOleClose = 6
idxDoVerb = 11
)
// IWebBrowser2 vtable indices used for sizing (property setters).
const (
idxWBPutLeft = 22
idxWBPutTop = 24
idxWBPutWidth = 26
idxWBPutHeight = 28
)
const oleCloseNoSave = 0
var htmlCounter struct {
sync.Mutex
n int
}
// embed creates the WebBrowser OLE object and activates it in-place inside the
// window. Port of EmbedBrowserObject() from embed-browser.c.
func (w *Window) embed() error {
var browser uintptr
hr, _, _ := procCoCreateInstance.Call(
uintptr(unsafe.Pointer(&CLSID_WebBrowser)),
0,
clsctxInprocServer,
uintptr(unsafe.Pointer(&IID_IOleObject)),
uintptr(unsafe.Pointer(&browser)),
)
if hr != 0 || browser == 0 {
return errors.New("webview: CoCreateInstance(WebBrowser) failed: 0x" + strconv.FormatUint(uint64(hr), 16))
}
w.oleObject = browser
comCall(browser, idxSetClientSite, uintptr(unsafe.Pointer(w.clientSite)))
comCall(browser, idxSetHostNames, uintptr(unsafe.Pointer(utf16("Go WebView"))), 0)
procOleSetContainedObject.Call(browser, 1)
rc := getClientRect(w.hwnd)
comCall(browser, idxDoVerb,
oleiVerbShow, // iVerb = OLEIVERB_SHOW (-1)
0, // lpmsg
uintptr(unsafe.Pointer(w.clientSite)), // active site
^uintptr(0), // lindex = -1
w.hwnd, // hwndParent
uintptr(unsafe.Pointer(&rc)), // lprcPosRect
)
var wb uintptr
comCall(browser, 0 /*QueryInterface*/, uintptr(unsafe.Pointer(&IID_IWebBrowser2)), uintptr(unsafe.Pointer(&wb)))
if wb == 0 {
w.unembed()
return errors.New("webview: QueryInterface(IWebBrowser2) failed")
}
w.browser = wb
w.resizeBrowser(int(rc.right), int(rc.bottom))
return nil
}
// unembed releases the browser. Port of UnEmbedBrowserObject().
func (w *Window) unembed() {
if w.browser != 0 {
release(w.browser)
w.browser = 0
}
if w.oleObject != 0 {
comCall(w.oleObject, idxOleClose, oleCloseNoSave)
comCall(w.oleObject, idxSetClientSite, 0)
release(w.oleObject)
w.oleObject = 0
}
}
// resizeBrowser stretches the browser over the whole client area. Port of
// ResizeBrowser().
func (w *Window) resizeBrowser(width, height int) {
if w.browser == 0 {
return
}
comCall(w.browser, idxWBPutLeft, 0)
comCall(w.browser, idxWBPutTop, 0)
comCall(w.browser, idxWBPutWidth, uintptr(int32(width)))
comCall(w.browser, idxWBPutHeight, uintptr(int32(height)))
}
func sysAllocString(s string) uintptr {
b, _, _ := procSysAllocString.Call(uintptr(unsafe.Pointer(utf16(s))))
return b
}
func sysFreeString(b uintptr) {
if b != 0 {
procSysFreeString.Call(b)
}
}
// Navigate loads the given URL. Port of the Navigate path of DoPageAction().
func (w *Window) Navigate(url string) error {
if w.browser == 0 {
return errors.New("webview: browser not initialised")
}
return w.invoke(dispidNavigate, dispatchMethod, url)
}
// variant mirrors the Win32 VARIANT structure. Two trailing machine words after
// the 8-byte header make it 16 bytes on 386 and 24 bytes on amd64, matching the
// platform VARIANT size.
type variant struct {
vt uint16
wReserved1 uint16
wReserved2 uint16
wReserved3 uint16
val uintptr
_ uintptr
}
type dispParams struct {
rgvarg uintptr
rgdispidNamedArgs uintptr
cArgs uint32
cNamedArgs uint32
}
// IDispatch / IWebBrowser DISPIDs.
const (
idxInvoke = 6 // IDispatch::Invoke
dispatchMethod = 1
dispidGoBack int32 = 100
dispidGoForward int32 = 101
dispidRefresh int32 = -550 // DISPID_REFRESH
dispidStop int32 = 106
dispidNavigate int32 = 104
)
var iidNull GUID
// invoke calls a method on the IWebBrowser2 IDispatch by DISPID. If a single
// string argument is supplied it is passed as a BSTR VARIANT. This is layout
// independent and avoids relying on exact IWebBrowser2 vtable slot ordering.
func (w *Window) invoke(dispid int32, flags uint16, strArg ...string) error {
if w.browser == 0 {
return errors.New("webview: browser not initialised")
}
var dp dispParams
var arg variant
if len(strArg) > 0 {
b := sysAllocString(strArg[0])
defer sysFreeString(b)
arg.vt = 8 // VT_BSTR
arg.val = b
dp.rgvarg = uintptr(unsafe.Pointer(&arg))
dp.cArgs = 1
}
comCall(w.browser, idxInvoke,
uintptr(uint32(dispid)),
uintptr(unsafe.Pointer(&iidNull)),
0, // lcid
uintptr(flags),
uintptr(unsafe.Pointer(&dp)),
0, // pVarResult
0, // pExcepInfo
0, // puArgErr
)
return nil
}
// LoadHTML displays an HTML string. It writes the markup to a temporary file
// and navigates to it, which is robust against the asynchronous nature of the
// IE control (more reliable than the in-memory document.write path used by
// DisplayHTMLStr in the C example).
func (w *Window) LoadHTML(html string) error {
htmlCounter.Lock()
htmlCounter.n++
n := htmlCounter.n
htmlCounter.Unlock()
// DOCHOSTUIFLAG_SCROLL_NO is honored on the XP/IE target but ignored by some
// newer IE control builds, so for self-authored content we also enforce it
// with CSS, which every engine respects.
if w.hideScroll {
html = "<style>html,body{overflow:hidden !important;}</style>" + html
}
name := filepath.Join(os.TempDir(), "gowebview_"+strconv.Itoa(os.Getpid())+"_"+strconv.Itoa(n)+".html")
if err := ioutil.WriteFile(name, []byte(html), 0644); err != nil {
return err
}
w.tempFiles = append(w.tempFiles, name)
return w.Navigate("file:///" + filepath.ToSlash(name))
}
// LoadFiles registers an in-process URLMon protocol backed by the supplied
// bundle and navigates to entry. Nothing is written to disk, no socket is
// opened, and relative references between bundled files are preserved.
func (w *Window) LoadFiles(files map[string][]byte, entry string) error {
registration, bundleURL, err := registerBundleProtocol(files, entry, w.hideScroll)
if err != nil {
return err
}
if err := w.Navigate(bundleURL); err != nil {
registration.close()
return err
}
w.protocols = append(w.protocols, registration)
return nil
}
// GoBack navigates to the previous page.
func (w *Window) GoBack() { w.invoke(dispidGoBack, dispatchMethod) }
// GoForward navigates to the next page.
func (w *Window) GoForward() { w.invoke(dispidGoForward, dispatchMethod) }
// Refresh reloads the current page.
func (w *Window) Refresh() { w.invoke(dispidRefresh, dispatchMethod) }
// Stop aborts the current navigation.
func (w *Window) Stop() { w.invoke(dispidStop, dispatchMethod) }

View File

@@ -0,0 +1,401 @@
package webview
// This file implements an asynchronous pluggable protocol (URLMon APP) backed
// by Go memory. All APIs used here are present on Windows XP.
import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"net/url"
"path"
"strings"
"sync"
"sync/atomic"
"unsafe"
)
const (
bindingStatusMIMETypeAvailable = 13
bscfFirstDataNotification = 0x01
bscfLastDataNotification = 0x04
bscfDataFullyAvailable = 0x08
)
type protocolRegistration struct {
session uintptr
factory *protocolFactory
scheme string
closed bool
}
type protocolFactory struct {
vtbl uintptr
classID GUID
scheme string
files map[string][]byte
mu sync.Mutex
instances map[uintptr]*internetProtocol
}
type internetProtocol struct {
vtbl uintptr
factory *protocolFactory
refs int32
mu sync.Mutex
data []byte
position int
sink uintptr
finished bool
}
var (
protocolFactoryVtbl []uintptr
internetProtocolVtbl []uintptr
)
func init() {
protocolFactoryVtbl = []uintptr{
cb(factoryQueryInterface),
cb(factoryAddRef),
cb(factoryRelease),
cb(factoryCreateInstance),
cb(factoryLockServer),
}
internetProtocolVtbl = []uintptr{
cb(protocolQueryInterface),
cb(protocolAddRef),
cb(protocolRelease),
cb(protocolStart),
cb(protocolContinue),
cb(protocolAbort),
cb(protocolTerminate),
cb(protocolSuspend),
cb(protocolResume),
cb(protocolRead),
protocolSeekCallback(),
cb(protocolLockRequest),
cb(protocolUnlockRequest),
}
}
func registerBundleProtocol(files map[string][]byte, entry string, hideScroll bool) (*protocolRegistration, string, error) {
entry, ok := cleanBundlePath(entry)
if !ok {
return nil, "", errors.New("webview: invalid bundle entry path")
}
bundle := make(map[string][]byte, len(files))
for name, data := range files {
clean, valid := cleanBundlePath(name)
if !valid {
return nil, "", errors.New("webview: invalid file path in bundle")
}
copied := append([]byte(nil), data...)
if hideScroll && clean == entry {
copied = append([]byte("<style>html,body{overflow:hidden !important;}</style>"), copied...)
}
bundle[clean] = copied
}
if _, exists := bundle[entry]; !exists {
return nil, "", errors.New("webview: bundle entry not found")
}
random := make([]byte, 16)
if _, err := rand.Read(random); err != nil {
return nil, "", err
}
// URLMon limits protocol scheme names to INTERNET_MAX_SCHEME_LENGTH.
// Eight random bytes keep the complete name short enough for XP/IE.
scheme := "nati" + hex.EncodeToString(random[:8])
classID := GUID{
Data1: binary.LittleEndian.Uint32(random[0:4]),
Data2: binary.LittleEndian.Uint16(random[4:6]),
Data3: binary.LittleEndian.Uint16(random[6:8]),
}
copy(classID.Data4[:], random[8:16])
factory := &protocolFactory{
vtbl: vtblAddr(protocolFactoryVtbl),
classID: classID,
scheme: scheme,
files: bundle,
instances: make(map[uintptr]*internetProtocol),
}
var session uintptr
hr, _, _ := procCoInternetGetSession.Call(0, uintptr(unsafe.Pointer(&session)), 0)
if hresultFailed(hr) || session == 0 {
return nil, "", errors.New("webview: CoInternetGetSession failed")
}
hr = comCall(session, 3,
uintptr(unsafe.Pointer(factory)),
uintptr(unsafe.Pointer(&factory.classID)),
uintptr(unsafe.Pointer(utf16(scheme))),
0, 0, 0,
)
if hresultFailed(hr) {
release(session)
return nil, "", errors.New("webview: RegisterNameSpace failed")
}
registration := &protocolRegistration{session: session, factory: factory, scheme: scheme}
return registration, scheme + "://app/" + escapeBundlePath(entry), nil
}
func (r *protocolRegistration) close() {
if r == nil || r.closed {
return
}
r.closed = true
if r.session != 0 && r.factory != nil {
comCall(r.session, 4,
uintptr(unsafe.Pointer(r.factory)),
uintptr(unsafe.Pointer(utf16(r.scheme))),
)
}
release(r.session)
r.session = 0
r.factory = nil
}
func factoryQueryInterface(this, riid, ppv uintptr) uintptr {
if ppv == 0 {
return ePointer
}
*(*uintptr)(unsafe.Pointer(ppv)) = 0
iid := (*GUID)(unsafe.Pointer(riid))
if guidEqual(iid, &IID_IUnknown) || guidEqual(iid, &IID_IClassFactory) {
*(*uintptr)(unsafe.Pointer(ppv)) = this
return sOK
}
return eNoInterface
}
func factoryAddRef(this uintptr) uintptr { return 2 }
func factoryRelease(this uintptr) uintptr { return 1 }
func factoryLockServer(this, lock uintptr) uintptr { return sOK }
func factoryCreateInstance(this, outer, riid, ppv uintptr) uintptr {
if ppv == 0 {
return ePointer
}
*(*uintptr)(unsafe.Pointer(ppv)) = 0
if outer != 0 {
return classENoAggregation
}
iid := (*GUID)(unsafe.Pointer(riid))
if !guidEqual(iid, &IID_IUnknown) && !guidEqual(iid, &IID_IInternetProtocolRoot) && !guidEqual(iid, &IID_IInternetProtocol) {
return eNoInterface
}
factory := (*protocolFactory)(unsafe.Pointer(this))
protocol := &internetProtocol{
vtbl: vtblAddr(internetProtocolVtbl),
factory: factory,
refs: 1,
}
pointer := uintptr(unsafe.Pointer(protocol))
factory.mu.Lock()
factory.instances[pointer] = protocol
factory.mu.Unlock()
*(*uintptr)(unsafe.Pointer(ppv)) = pointer
return sOK
}
func protocolQueryInterface(this, riid, ppv uintptr) uintptr {
if ppv == 0 {
return ePointer
}
*(*uintptr)(unsafe.Pointer(ppv)) = 0
iid := (*GUID)(unsafe.Pointer(riid))
if guidEqual(iid, &IID_IUnknown) || guidEqual(iid, &IID_IInternetProtocolRoot) || guidEqual(iid, &IID_IInternetProtocol) {
*(*uintptr)(unsafe.Pointer(ppv)) = this
protocolAddRef(this)
return sOK
}
return eNoInterface
}
func protocolAddRef(this uintptr) uintptr {
protocol := (*internetProtocol)(unsafe.Pointer(this))
return uintptr(atomic.AddInt32(&protocol.refs, 1))
}
func protocolRelease(this uintptr) uintptr {
protocol := (*internetProtocol)(unsafe.Pointer(this))
refs := atomic.AddInt32(&protocol.refs, -1)
if refs <= 0 {
protocol.releaseSink()
factory := protocol.factory
factory.mu.Lock()
delete(factory.instances, this)
factory.mu.Unlock()
return 0
}
return uintptr(refs)
}
func protocolStart(this, rawURL, sink, bindInfo, flags, reserved uintptr) uintptr {
protocol := (*internetProtocol)(unsafe.Pointer(this))
parsed, err := url.Parse(utf16PtrToString(rawURL))
if err != nil || parsed.Scheme != protocol.factory.scheme || !strings.EqualFold(parsed.Host, "app") {
protocol.reportFailure(sink, inetEResourceNotFound)
return inetEResourceNotFound
}
name, valid := cleanBundlePath(strings.TrimPrefix(parsed.Path, "/"))
data, exists := protocol.factory.files[name]
if !valid || !exists {
protocol.reportFailure(sink, inetEResourceNotFound)
return inetEResourceNotFound
}
protocol.mu.Lock()
protocol.data = data
protocol.position = 0
protocol.finished = false
protocol.sink = sink
if sink != 0 {
comCall(sink, 1)
}
protocol.mu.Unlock()
if sink != 0 {
contentType := bundleContentType(name)
comCall(sink, 4, bindingStatusMIMETypeAvailable, uintptr(unsafe.Pointer(utf16(contentType))))
size := uintptr(len(data))
comCall(sink, 5, bscfFirstDataNotification|bscfLastDataNotification|bscfDataFullyAvailable, size, size)
}
return sOK
}
func protocolContinue(this, protocolData uintptr) uintptr { return eNotImpl }
func protocolAbort(this, reason, options uintptr) uintptr {
protocol := (*internetProtocol)(unsafe.Pointer(this))
protocol.finish(reason)
return sOK
}
func protocolTerminate(this, options uintptr) uintptr {
protocol := (*internetProtocol)(unsafe.Pointer(this))
protocol.releaseSink()
protocol.mu.Lock()
protocol.data = nil
protocol.position = 0
protocol.mu.Unlock()
return sOK
}
func protocolSuspend(this uintptr) uintptr { return eNotImpl }
func protocolResume(this uintptr) uintptr { return eNotImpl }
func protocolRead(this, destination, count, read uintptr) uintptr {
if read != 0 {
*(*uint32)(unsafe.Pointer(read)) = 0
}
protocol := (*internetProtocol)(unsafe.Pointer(this))
protocol.mu.Lock()
remaining := len(protocol.data) - protocol.position
if remaining <= 0 {
protocol.mu.Unlock()
protocol.finish(sOK)
return sFalse
}
n := int(count)
if n > remaining {
n = remaining
}
for i := 0; i < n; i++ {
*(*byte)(unsafe.Pointer(destination + uintptr(i))) = protocol.data[protocol.position+i]
}
protocol.position += n
protocol.mu.Unlock()
if read != 0 {
*(*uint32)(unsafe.Pointer(read)) = uint32(n)
}
return sOK
}
func protocolLockRequest(this, options uintptr) uintptr { return sOK }
func protocolUnlockRequest(this uintptr) uintptr { return sOK }
func (p *internetProtocol) finish(result uintptr) {
p.mu.Lock()
if p.finished {
p.mu.Unlock()
return
}
p.finished = true
sink := p.sink
p.mu.Unlock()
if sink != 0 {
comCall(sink, 6, result, 0, 0)
}
}
func (p *internetProtocol) reportFailure(sink, result uintptr) {
if sink != 0 {
comCall(sink, 6, result, 0, 0)
}
}
func (p *internetProtocol) releaseSink() {
p.mu.Lock()
sink := p.sink
p.sink = 0
p.mu.Unlock()
release(sink)
}
func cleanBundlePath(name string) (string, bool) {
name = strings.Replace(name, "\\", "/", -1)
if name == "" || strings.HasPrefix(name, "/") || strings.IndexByte(name, 0) >= 0 {
return "", false
}
clean := path.Clean(name)
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
return "", false
}
return clean, true
}
func escapeBundlePath(name string) string {
parts := strings.Split(name, "/")
for i, part := range parts {
parts[i] = url.PathEscape(part)
}
return strings.Join(parts, "/")
}
func bundleContentType(name string) string {
switch strings.ToLower(path.Ext(name)) {
case ".html", ".htm":
return "text/html; charset=utf-8"
case ".css":
return "text/css; charset=utf-8"
case ".js":
return "text/javascript; charset=utf-8"
case ".json":
return "application/json; charset=utf-8"
case ".svg":
return "image/svg+xml"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".ico":
return "image/x-icon"
case ".woff":
return "font/woff"
case ".woff2":
return "font/woff2"
default:
return "application/octet-stream"
}
}
func hresultFailed(result uintptr) bool { return int32(result) < 0 }

View File

@@ -0,0 +1,8 @@
package webview
func protocolSeekCallback() uintptr { return cb(protocolSeek) }
// LARGE_INTEGER is passed as two machine words on 386.
func protocolSeek(this, moveLow, moveHigh, origin, newPosition uintptr) uintptr {
return eNotImpl
}

View File

@@ -0,0 +1,8 @@
package webview
func protocolSeekCallback() uintptr { return cb(protocolSeek) }
// LARGE_INTEGER fits in one machine word on amd64.
func protocolSeek(this, move, origin, newPosition uintptr) uintptr {
return eNotImpl
}

View File

@@ -0,0 +1,144 @@
package webview
import (
"syscall"
"unsafe"
)
// Low-level WinAPI bindings. Pure stdlib syscall, no external dependencies, so
// the package builds on Go 1.10.8 (last toolchain supporting Windows XP).
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
ole32 = syscall.NewLazyDLL("ole32.dll")
urlmon = syscall.NewLazyDLL("urlmon.dll")
procRegisterClassExW = user32.NewProc("RegisterClassExW")
procCreateWindowExW = user32.NewProc("CreateWindowExW")
procDestroyWindow = user32.NewProc("DestroyWindow")
procDefWindowProcW = user32.NewProc("DefWindowProcW")
procShowWindow = user32.NewProc("ShowWindow")
procUpdateWindow = user32.NewProc("UpdateWindow")
procGetMessageW = user32.NewProc("GetMessageW")
procTranslateMessage = user32.NewProc("TranslateMessage")
procDispatchMessageW = user32.NewProc("DispatchMessageW")
procPostQuitMessage = user32.NewProc("PostQuitMessage")
procPostMessageW = user32.NewProc("PostMessageW")
procGetClientRect = user32.NewProc("GetClientRect")
procMoveWindow = user32.NewProc("MoveWindow")
procSetWindowTextW = user32.NewProc("SetWindowTextW")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procSetWindowPos = user32.NewProc("SetWindowPos")
procLoadCursorW = user32.NewProc("LoadCursorW")
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
procOleInitialize = ole32.NewProc("OleInitialize")
procOleUninitialize = ole32.NewProc("OleUninitialize")
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
procOleSetContainedObject = ole32.NewProc("OleSetContainedObject")
procCoInternetGetSession = urlmon.NewProc("CoInternetGetSession")
)
// Win32 constants.
const (
wsOverlappedWindow = 0x00CF0000
wsThickFrame = 0x00040000
wsMaximizeBox = 0x00010000
wsVisible = 0x10000000
wsClipChildren = 0x02000000
swShow = 5
swHide = 0
wmDestroy = 0x0002
wmSize = 0x0005
wmClose = 0x0010
cwUseDefault = 0x80000000
smCXScreen = 0
smCYScreen = 1
idcArrow = 32512
csHRedraw = 0x0002
csVRedraw = 0x0001
swpNoZOrder = 0x0004
swpNoActivate = 0x0010
swpNoMove = 0x0002
clsctxInprocServer = 0x1
clsctxInprocAll = 0x3
oleiVerbShow = ^uintptr(0) // OLEIVERB_SHOW = -1
)
// wndClassExW mirrors the Win32 WNDCLASSEXW structure.
type wndClassExW struct {
cbSize uint32
style uint32
lpfnWndProc uintptr
cbClsExtra int32
cbWndExtra int32
hInstance uintptr
hIcon uintptr
hCursor uintptr
hbrBackground uintptr
lpszMenuName *uint16
lpszClassName *uint16
hIconSm uintptr
}
type msg struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt point
}
type point struct {
x, y int32
}
type rect struct {
left, top, right, bottom int32
}
func getModuleHandle() uintptr {
r, _, _ := procGetModuleHandleW.Call(0)
return r
}
func loadCursor(id uintptr) uintptr {
r, _, _ := procLoadCursorW.Call(0, id)
return r
}
func defWindowProc(hwnd uintptr, m uint32, w, l uintptr) uintptr {
r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(m), w, l)
return r
}
func getClientRect(hwnd uintptr) rect {
var rc rect
procGetClientRect.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
return rc
}
func getSystemMetric(index int) int {
r, _, _ := procGetSystemMetrics.Call(uintptr(index))
return int(int32(r))
}
func utf16(s string) *uint16 {
p, err := syscall.UTF16PtrFromString(s)
if err != nil {
return nil
}
return p
}

View File

@@ -0,0 +1,263 @@
// Package webview embeds the Internet Explorer WebBrowser control in a native
// Win32 window using nothing but the standard library's syscall package. It is
// a Go port of the classic embed-browser.c example and targets Go 1.10.8 /
// Windows XP (build with GOARCH=386).
//
// The window is a "solid" webview: the browser fills the entire client area and
// there are no separate Win32 child controls.
package webview
import (
"errors"
"os"
"runtime"
"sync"
"unsafe"
)
// Options configures a Window created by New.
type Options struct {
Title string // window caption
Width int // client width; 0 -> 1024
Height int // client height; 0 -> 768
X, Y int // top-left position; 0,0 -> centered on screen
URL string // if set, navigated to on creation
HTML string // if set, displayed on creation (takes precedence over URL)
Resizable bool // allow the user to resize the window
DisableMenu bool // suppress the default IE right-click context menu
HideScrollbars bool // hide the page scrollbars (content is clipped, not scrollable)
}
// Window is a native window hosting the WebBrowser control.
type Window struct {
hwnd uintptr
oleObject uintptr // IOleObject*
browser uintptr // IWebBrowser2*
clientSite *oleClientSite
inPlaceSite *oleInPlaceSite
inPlaceFrame *oleInPlaceFrame
docHost *docHostUIHandler
external *externalDispatch
disableMenu bool
hideScroll bool
tempFiles []string
protocols []*protocolRegistration
mu sync.Mutex
bindings map[string]func([]string) string
bindIDs map[string]int32
idNames map[int32]string
nextID int32
}
// Bind exposes a Go function to JavaScript as window.external.<name>(...).
// Arguments from JS are passed as strings; the returned string is given back to
// JS as the call result. Bind before navigating to a page that uses it.
func (w *Window) Bind(name string, fn func(args []string) string) {
w.mu.Lock()
defer w.mu.Unlock()
id, ok := w.bindIDs[name]
if !ok {
w.nextID++
id = w.nextID
w.bindIDs[name] = id
w.idNames[id] = name
}
w.bindings[name] = fn
}
const className = "GoWebViewWindow"
var (
registryMu sync.Mutex
windows = make(map[uintptr]*Window)
classOnce sync.Once
wndProcPtr uintptr
oleOnce sync.Once
oleOK bool
)
func loword(v uintptr) int { return int(v & 0xffff) }
func hiword(v uintptr) int { return int((v >> 16) & 0xffff) }
func wndProc(hwnd, m, wParam, lParam uintptr) uintptr {
switch uint32(m) {
case wmSize:
registryMu.Lock()
w := windows[hwnd]
registryMu.Unlock()
if w != nil {
w.resizeBrowser(loword(lParam), hiword(lParam))
}
return 0
case wmDestroy:
registryMu.Lock()
w := windows[hwnd]
delete(windows, hwnd)
registryMu.Unlock()
if w != nil {
w.unembed()
w.cleanup()
}
procPostQuitMessage.Call(0)
return 0
}
return defWindowProc(hwnd, uint32(m), wParam, lParam)
}
func registerClass() {
wndProcPtr = cb(wndProc)
wc := wndClassExW{
style: csHRedraw | csVRedraw,
lpfnWndProc: wndProcPtr,
hInstance: getModuleHandle(),
hCursor: loadCursor(idcArrow),
hbrBackground: 6, // COLOR_WINDOW+1
lpszClassName: utf16(className),
}
wc.cbSize = uint32(unsafe.Sizeof(wc))
procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
}
// New creates a window, embeds the browser, and (optionally) navigates. It must
// be called from the same goroutine that will later call Run, because the IE
// control requires a single-threaded COM apartment bound to one OS thread.
func New(opts Options) (*Window, error) {
runtime.LockOSThread()
oleOnce.Do(func() {
hr, _, _ := procOleInitialize.Call(0)
oleOK = hr == 0 || hr == 1 // S_OK or S_FALSE (already initialised)
})
if !oleOK {
return nil, errors.New("webview: OleInitialize failed")
}
classOnce.Do(registerClass)
if opts.Width <= 0 {
opts.Width = 1024
}
if opts.Height <= 0 {
opts.Height = 768
}
style := uintptr(wsOverlappedWindow | wsClipChildren)
if !opts.Resizable {
style &^= wsThickFrame | wsMaximizeBox
}
x, y := uintptr(opts.X), uintptr(opts.Y)
if opts.X == 0 && opts.Y == 0 {
x = uintptr(int32((getSystemMetric(smCXScreen) - opts.Width) / 2))
y = uintptr(int32((getSystemMetric(smCYScreen) - opts.Height) / 2))
}
w := &Window{
disableMenu: opts.DisableMenu,
hideScroll: opts.HideScrollbars,
bindings: make(map[string]func([]string) string),
bindIDs: make(map[string]int32),
idNames: make(map[int32]string),
}
w.clientSite = &oleClientSite{vtbl: vtblAddr(clientSiteVtbl), w: w}
w.inPlaceSite = &oleInPlaceSite{vtbl: vtblAddr(inPlaceSiteVtbl), w: w}
w.inPlaceFrame = &oleInPlaceFrame{vtbl: vtblAddr(inPlaceFrameVtbl), w: w}
w.docHost = &docHostUIHandler{vtbl: vtblAddr(docHostVtbl), w: w}
w.external = &externalDispatch{vtbl: vtblAddr(externalVtbl), w: w}
hwnd, _, _ := procCreateWindowExW.Call(
0,
uintptr(unsafe.Pointer(utf16(className))),
uintptr(unsafe.Pointer(utf16(opts.Title))),
style,
x, y,
uintptr(int32(opts.Width)), uintptr(int32(opts.Height)),
0, 0,
getModuleHandle(),
0,
)
if hwnd == 0 {
return nil, errors.New("webview: CreateWindowEx failed")
}
w.hwnd = hwnd
registryMu.Lock()
windows[hwnd] = w
registryMu.Unlock()
if err := w.embed(); err != nil {
procDestroyWindow.Call(hwnd)
return nil, err
}
if opts.HTML != "" {
w.LoadHTML(opts.HTML)
} else if opts.URL != "" {
w.Navigate(opts.URL)
}
return w, nil
}
// Run pumps the Win32 message loop until the window is closed. Blocks the
// calling goroutine.
func (w *Window) Run() {
var m msg
for {
r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
if int32(r) <= 0 { // 0 = WM_QUIT, -1 = error
break
}
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}
}
// Show makes the window visible.
func (w *Window) Show() {
procShowWindow.Call(w.hwnd, swShow)
procUpdateWindow.Call(w.hwnd)
}
// Hide hides the window.
func (w *Window) Hide() { procShowWindow.Call(w.hwnd, swHide) }
// SetTitle changes the window caption.
func (w *Window) SetTitle(title string) {
procSetWindowTextW.Call(w.hwnd, uintptr(unsafe.Pointer(utf16(title))))
}
// Resize sets the window size (in pixels).
func (w *Window) Resize(width, height int) {
procSetWindowPos.Call(w.hwnd, 0, 0, 0, uintptr(int32(width)), uintptr(int32(height)), swpNoZOrder|swpNoMove|swpNoActivate)
}
// Center positions the window in the middle of the screen.
func (w *Window) Center() {
rc := getClientRect(w.hwnd)
cw, ch := int(rc.right), int(rc.bottom)
x := (getSystemMetric(smCXScreen) - cw) / 2
y := (getSystemMetric(smCYScreen) - ch) / 2
procSetWindowPos.Call(w.hwnd, 0, uintptr(int32(x)), uintptr(int32(y)), 0, 0, swpNoZOrder|swpNoActivate|0x0001 /*SWP_NOSIZE*/)
}
// Close requests the window to close (triggers WM_DESTROY and ends Run).
func (w *Window) Close() {
procPostMessageW.Call(w.hwnd, wmClose, 0, 0)
}
func (w *Window) cleanup() {
for _, f := range w.tempFiles {
os.Remove(f)
}
for _, registration := range w.protocols {
registration.close()
}
w.tempFiles = nil
w.protocols = nil
}