Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
45 changes: 45 additions & 0 deletions .github/workflows/webview2-verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Verify webview2 generator

on:
pull_request:
paths:
- 'webview2/**'
- '.github/workflows/webview2-verify.yml'
push:
branches: [master]
paths:
- 'webview2/**'

jobs:
verify:
runs-on: ubuntu-latest
env:
GOWORK: "off"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Generator tests
working-directory: webview2/scripts
run: go test -race ./...

- name: Verify generated bindings are up to date
working-directory: webview2/scripts
run: |
go run ./cmd/webview2gen verify
# capabilities.go is also generator-output; regenerate it from the
# cached release-notes snapshot and fail if it changes.
go run ./cmd/webview2gen capabilities --source test.md
if ! git diff --exit-code -- ../pkg/webview2/capabilities.go; then
echo "::error::pkg/webview2/capabilities.go is out of date — run 'webview2gen capabilities'"
exit 1
fi

- name: Cross-compile generated bindings for Windows
working-directory: webview2
run: GOOS=windows go vet ./pkg/webview2/

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +15 to +45
10 changes: 10 additions & 0 deletions webview2/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Force LF line endings for all webview2 sources, generated bindings, and
# generator fixtures so Windows checkouts with core.autocrlf=true don't
# silently convert templates to CRLF — which makes text/template emit
# CRLF in the bindings, which then diverges from the LF golden files
# committed in scripts/generator/testfiles/.
* text=auto eol=lf
*.tmpl text eol=lf
*.idl text eol=lf
*.go text eol=lf
*.go.txt text eol=lf
3 changes: 3 additions & 0 deletions webview2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Compiled webview2gen binary — never commit, build artifact only.
/scripts/webview2gen
/scripts/cmd/webview2gen/webview2gen
152 changes: 152 additions & 0 deletions webview2/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# wails/webview2 — Architecture

This document records the design decisions behind the v2 WebView2 binding
generator. It is the reference for "why was this done this way?" questions.

## Goals

1. **Correct.** Generated COM bindings must respect the Windows ABI —
no caller-stack pointer writes, no missing release methods, no truncated
reference counts.
2. **Reproducible.** Given a frozen IDL + release-notes file, two runs of
`webview2gen full` on different machines must produce byte-identical
output. CI gates on this via `webview2gen verify`.
3. **Capability-aware.** The Wails runtime sees a stable Go API, but at
runtime degrades gracefully when the installed WebView2 lacks an
interface (we ship for a wide range of runtime versions).
4. **Single command refresh.** Refreshing the bindings for a new SDK
release must be one CLI invocation, not a sequence of hand edits.

## High-level pipeline

```
Microsoft NuGet MicrosoftDocs/edge-developer
│ │
│ .nupkg (zip of headers + WebView2.idl) │ index.md
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────┐
│ internal/idl.Fetcher │ │ internal/notes.Fetch │
│ extracts WebView2.idl │ │ scrapes release notes │
│ caches in scripts/*.idl │ │ │
└──────────────┬───────────┘ └────────────┬────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────┐
│ generator.ParseIDL │ │ notes.Parse │
│ participle/v2 grammar │ │ extract per-release │
│ types/typemap (17 pat.) │ │ interface mentions │
│ emits []GeneratedFile │ │ │
└──────────────┬───────────┘ └────────────┬────────────┘
│ │
▼ ▼
pkg/webview2/*.go (306 files) pkg/webview2/capabilities.go
```

Each box is one Go package under `webview2/scripts/`; `webview2gen` (the
CLI) wires them together.

## Design decisions

### Template-driven emitter (kept)

The generator emits Go source by composing `text/template` files in
`scripts/generator/types/templates/`. The spec considered moving to a
fully programmatic emitter (`fmt.Fprintf` + `bytes.Buffer`), but the
existing template surface is small (12 files), debuggable, and already
covers all the patterns we need. The 6 bug fixes were all expressible as
template edits; a rewrite would have been a much larger churn for the
same correctness outcome.

**Trade-off.** Templates are weakly typed and easy to break with whitespace
changes. Mitigated by the snapshot-style `testfiles/` fixtures: every
generator change re-runs the existing fixtures and diffs them.

### File-per-interface output

`pkg/webview2/` has 306 files because each interface and each enum lands
in its own file. Diffs from an SDK refresh stay readable, and parallel
editing of related but separate interfaces doesn't cause merge churn.

**Trade-off.** `ls pkg/webview2 | wc -l` is intimidating. Mitigated by
`doc.go` (the only hand-written file) carrying the "do not edit" notice
and the regeneration recipe.

### IDL inlined upfront

The fetcher pulls `WebView2.idl` directly out of the NuGet `.nupkg`. The
file is self-contained — it inlines the COM/OLE types it depends on, so
we do not need a preprocessor that resolves `import "objidl.idl"` etc.
If a future SDK switches to multi-file IDL, `internal/idl` is where the
inliner will live.

### Capabilities from release notes

The mapping "ICoreWebView2_N → minimum SDK version" comes from scraping
the Markdown release notes (`MicrosoftDocs/edge-developer`). The IDL
itself does not record which SDK introduced which interface; the release
notes do (as the bullet "Added the `ICoreWebView2_N` interface" under a
versioned heading).

The scrape is regex-based and walks the notes oldest-first so the
earliest mention wins. When Microsoft changes the notes format we will
see test failures and update the regex; the table is committed at
`pkg/webview2/capabilities.go` so a broken scrape can be fixed without
forcing every consumer to wait.

**Trade-off.** A maintenance burden (every few months Microsoft may
restructure the notes). Mitigated by the test suite covering the parser
against the cached `test.md` snapshot.

### `pkg/edge/` is kept, not deleted

The legacy `pkg/edge/` package is what the Wails v3 runtime actually
imports today. Removing it would break the runtime in the same PR that
ships the new generator. The migration plan:

1. This branch lands the generator + tested `pkg/webview2/`.
2. A follow-up PR refactors `v3/pkg/application/...` (chromium.go) to
import `pkg/webview2`.
3. After that lands, `pkg/edge/` is moved or removed.

`pkg/edge/` is therefore actively used and must not be touched by
generator runs. The generator only writes to `pkg/webview2/`.

### Two go modules in one repo

`webview2/go.mod` is the runtime module (`github.com/wailsapp/wails/webview2`).
`webview2/scripts/go.mod` is a dev-time module (`module updater`, a
legacy name kept to avoid a noisy rename). The split:

- Lets `scripts/` depend on developer-only packages (NuGet fetcher, HTTP
client, participle/v2) without forcing those deps on every consumer.
- Lets `scripts/` use a separate, sometimes-older Go version (1.20) than
the runtime module (1.24) without conflict.

If we ever want a single module, the rename is straightforward but
out-of-scope for this branch.

### Rejected approaches

- **Hand-maintain the bindings.** ~109 out-pointer methods to audit, and
one new SDK per six weeks. Untenable. The whole point of v2 is
automation.
- **`go-ole` or other COM libraries.** Adds a third-party dependency
that we then need to track for the same set of fixes. The thin
template-based wrapper is simpler to audit.
- **Monolithic `webview2.go`.** A single file would be 30k+ lines.
Reviewing diffs for a new SDK release would be impractical.

## Future considerations

- **Generics for type-safe vtable calls.** Today every vtable invocation
is `uintptr(unsafe.Pointer(...))`. Go generics could express the call
shape per parameter type. Out of scope for v2; tracked as a follow-up.
- **Automatic version detection at startup.** `webviewloader` already
reports the runtime version. We could populate a process-wide cache
so `SupportsInterface` calls don't need to re-pass the string.
- **Cross-platform compile checks.** `pkg/webview2/` is `//go:build
windows` so non-Windows CI can't catch regressions. Could add a
`linux` shim package that satisfies the same surface using build tags.
- **Windows VM CI.** The generator unit tests run on Linux. Smoke tests
for the COM bindings need a real WebView2 runtime — tracked as
WAI-297; not in this branch.
122 changes: 122 additions & 0 deletions webview2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# wails/webview2

Generated Go bindings for the Microsoft Edge WebView2 SDK and the tooling
that produces them. This module is consumed by Wails on Windows.

## Package layout

| Path | Purpose |
|------|---------|
| `pkg/webview2/` | **Generated** Go COM bindings for ICoreWebView2 (v1 through v27 at the latest SDK). Every `.go` file except `doc.go` is regenerated from `WebView2.idl`. |
| `pkg/edge/` | **Legacy** hand-maintained bindings for ICoreWebView2 v1–v4. Still used by `v3/pkg/application` via `chromium.go`; kept until the runtime is migrated. |
| `pkg/combridge/` | Generic COM bridge for implementing Go-side COM objects (event handlers, callbacks). |
| `webviewloader/` | Runtime DLL finder and version comparator (`CompareBrowserVersions`). |
| `internal/w32/` | Win32 syscall stubs used by the loader. |
| `scripts/` | The IDL cache (`WebView2.*.idl`), the `webview2gen` CLI, the parser, the emitter, and the internal helper packages. `module updater`. |

## webview2gen — the binding generator

`webview2gen` is the single entry point for refreshing `pkg/webview2`
against a new SDK release.

```sh
cd webview2/scripts

# Download a specific SDK version (or no flag for the latest cached version).
go run ./cmd/webview2gen download --version 1.0.2903.40

# Parse the cached IDL and emit pkg/webview2/*.go.
go run ./cmd/webview2gen generate

# Build pkg/webview2/capabilities.go from the SDK release notes.
go run ./cmd/webview2gen capabilities # fetches notes from GitHub
go run ./cmd/webview2gen capabilities --source test.md # use a local copy

# Regenerate everything and fail if the working tree differs from the
# committed output — wire this into CI so hand-edits cannot land.
go run ./cmd/webview2gen verify

# Run download → generate → capabilities → verify in sequence.
go run ./cmd/webview2gen full
```

Run `go generate ./...` from `webview2/` to invoke `full` via the
`//go:generate` directive in `pkg/webview2/doc.go`.

### Subcommand flags

Every subcommand accepts `--help`. Most useful defaults:

- `download --dir .` — where to store cached IDLs (relative to `scripts/`).
- `generate --version <v>` — pin to a specific cached IDL (default: newest).
- `generate --out ../pkg/webview2` — where to write generated files.
- `capabilities --source <path>` — skip the network fetch.
- `capabilities --json <path>` — also dump the interface→version map as JSON.

## Capabilities — runtime feature detection

The WebView2 SDK adds interfaces (ICoreWebView2_1 → ICoreWebView2_27 and
counting). A given runtime supports a subset; calling a method on an
interface the runtime doesn't know about crashes the process. To gate
features safely:

```go
import "github.com/wailsapp/wails/webview2/pkg/webview2"

runtime := webview2runtime.GetAvailableCoreWebView2BrowserVersionString() // e.g. "121.0.2277.83"

ok, required, err := webview2.SupportsInterface(runtime, "ICoreWebView2_22")
if err != nil { ... }
if !ok {
log.Printf("feature disabled — needs WebView2 SDK %s, runtime reports %s", required, runtime)
return
}
// safe to call ICoreWebView2_22 methods
```

For a higher-level gate, declare a `Capability` once and check it everywhere:

```go
var screenCapture = webview2.Capability{
Name: "screen-capture",
Description: "ScreenCaptureStarting event support",
Interface: "ICoreWebView2_22",
}
if ok, _ := webview2.HasCapability(runtime, screenCapture); ok { ... }
```

`AllCapabilities` enumerates every interface as a named gate, suitable for
boot-time logging or diagnostics.

## Bugs fixed by the v2 generator

| # | Severity | Pattern | Fix |
|---|----------|---------|-----|
| 1 | Critical | `[out] LPWSTR* name` produced `uintptr(unsafe.Pointer(_name))` — COM wrote the string pointer into the caller's stack | Now produces `uintptr(unsafe.Pointer(&_name))` so the local `*uint16` receives the string. Affects ~109 methods. |
| 2 | Moderate | `[in] LPWSTR*` was treated as a single `string` | Maps to `[]string`; the new `inputStringArraySetup.tmpl` marshals each element to `*uint16`. |
| 3 | Moderate | Generated interfaces lacked `Release()`, leaking refs | `Release() uint32` is emitted alongside `AddRef()` on every interface. |
| 4 | Minor | `QueryInterface` swallowed `HRESULT`, returning `nil` on failure | Returns `(*T, error)`; non-zero HRESULTs are surfaced. |
| 5 | Minor | `VARIANT` was a `uintptr` (8 bytes) instead of the 16-byte Windows struct | `type VARIANT struct { VT uint16; … Val [8]byte }`. |
| 6 | Minor | `AddRef/Release` returned `uintptr` instead of the COM `ULONG` (uint32) | Both return `uint32`; callback wrappers cast to `uintptr` at the syscall boundary. |

## Testing

```sh
cd webview2/scripts
go test ./... # generator + internal pkgs (Linux-fine)
GOOS=windows go vet ../pkg/... # cross-vet the generated bindings
```

CI must gate on `webview2gen verify` and `go test ./...`. The generator
contains regression tests for each of the 17 typed IDL patterns and for
all six known bug classes.

Windows VM integration tests (COM smoke, property getters, event handlers)
are tracked separately on WAI-297 — they require an actual WebView2 runtime.

## See also

- `ARCHITECTURE.md` for the design rationale (programmatic emitter,
file-per-interface, capability sources, etc.).
- Microsoft SDK release notes: <https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes>
- WebView2 IDL on NuGet: <https://www.nuget.org/packages/Microsoft.Web.WebView2>
1 change: 1 addition & 0 deletions webview2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
2 changes: 1 addition & 1 deletion webview2/pkg/webview2/COREWEBVIEW2_BOUNDS_MODE.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ package webview2
type COREWEBVIEW2_BOUNDS_MODE uint32

const (
COREWEBVIEW2_BOUNDS_MODE_USE_RAW_PIXELS = 0
COREWEBVIEW2_BOUNDS_MODE_USE_RAW_PIXELS = 0
COREWEBVIEW2_BOUNDS_MODE_USE_RASTERIZATION_SCALE = 1
)
30 changes: 15 additions & 15 deletions webview2/pkg/webview2/COREWEBVIEW2_BROWSING_DATA_KINDS.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ package webview2
type COREWEBVIEW2_BROWSING_DATA_KINDS uint32

const (
COREWEBVIEW2_BROWSING_DATA_KINDS_FILE_SYSTEMS = 0x1
COREWEBVIEW2_BROWSING_DATA_KINDS_INDEXED_DB = 0x2
COREWEBVIEW2_BROWSING_DATA_KINDS_LOCAL_STORAGE = 0x4
COREWEBVIEW2_BROWSING_DATA_KINDS_WEB_SQL = 0x8
COREWEBVIEW2_BROWSING_DATA_KINDS_CACHE_STORAGE = 0x10
COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_DOM_STORAGE = 0x20
COREWEBVIEW2_BROWSING_DATA_KINDS_COOKIES = 0x40
COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_SITE = 0x80
COREWEBVIEW2_BROWSING_DATA_KINDS_DISK_CACHE = 0x100
COREWEBVIEW2_BROWSING_DATA_KINDS_DOWNLOAD_HISTORY = 0x200
COREWEBVIEW2_BROWSING_DATA_KINDS_GENERAL_AUTOFILL = 0x400
COREWEBVIEW2_BROWSING_DATA_KINDS_FILE_SYSTEMS = 0x1
COREWEBVIEW2_BROWSING_DATA_KINDS_INDEXED_DB = 0x2
COREWEBVIEW2_BROWSING_DATA_KINDS_LOCAL_STORAGE = 0x4
COREWEBVIEW2_BROWSING_DATA_KINDS_WEB_SQL = 0x8
COREWEBVIEW2_BROWSING_DATA_KINDS_CACHE_STORAGE = 0x10
COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_DOM_STORAGE = 0x20
COREWEBVIEW2_BROWSING_DATA_KINDS_COOKIES = 0x40
COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_SITE = 0x80
COREWEBVIEW2_BROWSING_DATA_KINDS_DISK_CACHE = 0x100
COREWEBVIEW2_BROWSING_DATA_KINDS_DOWNLOAD_HISTORY = 0x200
COREWEBVIEW2_BROWSING_DATA_KINDS_GENERAL_AUTOFILL = 0x400
COREWEBVIEW2_BROWSING_DATA_KINDS_PASSWORD_AUTOSAVE = 0x800
COREWEBVIEW2_BROWSING_DATA_KINDS_BROWSING_HISTORY = 0x1000
COREWEBVIEW2_BROWSING_DATA_KINDS_SETTINGS = 0x2000
COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_PROFILE = 0x4000
COREWEBVIEW2_BROWSING_DATA_KINDS_SERVICE_WORKERS = 0x8000
COREWEBVIEW2_BROWSING_DATA_KINDS_BROWSING_HISTORY = 0x1000
COREWEBVIEW2_BROWSING_DATA_KINDS_SETTINGS = 0x2000
COREWEBVIEW2_BROWSING_DATA_KINDS_ALL_PROFILE = 0x4000
COREWEBVIEW2_BROWSING_DATA_KINDS_SERVICE_WORKERS = 0x8000
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ package webview2
type COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT uint32

const (
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG = 0
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG = 0
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_JPEG = 1
)
Loading
Loading