base webview project
This commit is contained in:
170
README_webview.md
Normal file
170
README_webview.md
Normal 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
57
build_xp.bat
Normal 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
|
||||
9
src/nati-launch/cmd/nati-launch/frontend_gen.go
Normal file
9
src/nati-launch/cmd/nati-launch/frontend_gen.go
Normal 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 ↔ 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\"> </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"),
|
||||
}
|
||||
46
src/nati-launch/cmd/nati-launch/main.go
Normal file
46
src/nati-launch/cmd/nati-launch/main.go
Normal 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()
|
||||
}
|
||||
18
src/nati-launch/frontend/app.js
Normal file
18
src/nati-launch/frontend/app.js
Normal 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;
|
||||
}
|
||||
}
|
||||
19
src/nati-launch/frontend/index.html
Normal file
19
src/nati-launch/frontend/index.html
Normal 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 ↔ 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"> </div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
26
src/nati-launch/frontend/styles.css
Normal file
26
src/nati-launch/frontend/styles.css
Normal 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;
|
||||
}
|
||||
83
src/nati-launch/tools/frontendgen/main.go
Normal file
83
src/nati-launch/tools/frontendgen/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
320
src/nati-launch/webview/com.go
Normal file
320
src/nati-launch/webview/com.go
Normal 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
|
||||
}
|
||||
153
src/nati-launch/webview/external.go
Normal file
153
src/nati-launch/webview/external.go
Normal 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)
|
||||
}
|
||||
43
src/nati-launch/webview/guids.go
Normal file
43
src/nati-launch/webview/guids.go
Normal 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)
|
||||
)
|
||||
249
src/nati-launch/webview/host.go
Normal file
249
src/nati-launch/webview/host.go
Normal 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) }
|
||||
401
src/nati-launch/webview/protocol.go
Normal file
401
src/nati-launch/webview/protocol.go
Normal 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 }
|
||||
8
src/nati-launch/webview/protocol_386.go
Normal file
8
src/nati-launch/webview/protocol_386.go
Normal 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
|
||||
}
|
||||
8
src/nati-launch/webview/protocol_amd64.go
Normal file
8
src/nati-launch/webview/protocol_amd64.go
Normal 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
|
||||
}
|
||||
144
src/nati-launch/webview/syscalls.go
Normal file
144
src/nati-launch/webview/syscalls.go
Normal 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
|
||||
}
|
||||
263
src/nati-launch/webview/webview.go
Normal file
263
src/nati-launch/webview/webview.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user