Reopens the change from #342 on a branch in this repository. That PR closed itself when the fork it came from was deleted — nothing to do with its contents, and its comments are still the record of how this was measured.
What is wrong
On Windows, CreateCoreWebView2EnvironmentWithOptions returns
CO_E_NOTINITIALIZED (0x800401f0) when the calling thread is in no COM
apartment, and craft never puts it in one: ole32 is linked for
CoTaskMemFree alone, with no CoInitializeEx, CoInitialize or
OleInitialize anywhere in the tree. So craft depends on something else in the
process having created an apartment.
That dependency is satisfied when craft is spawned with a console, which is why
windows-multi-window passes. It is not satisfied under CREATE_NO_WINDOW,
which is what an embedder passes so Windows does not put a console window
beside the app — and then no window opens at all.
The change
Window.create calls CoInitializeEx(null, COINIT_APARTMENTTHREADED) before
anything touches WebView2. It is the single funnel every window goes through,
and a thread-local guard keeps later windows from repeating it. S_FALSE (an
apartment already existed) and RPC_E_CHANGED_MODE (an embedder chose an MTA
first) are both accepted rather than treated as failures. There is no matching
CoUninitialize: the apartment has to outlive every webview on the thread, and
the only point at which that holds is process exit. No new link dependency —
build.zig already links ole32.
Measured, not argued
Three rounds on windows-latest, WebView2 runtime 153.0.4234.48, against the
shipped craft-windows-x64.zip and then against builds from this source.
The invocation is innocent. Six argument lists, each with a page that
fetches a URL when it loads, so a hit proves the environment was created. All
six produced a webview on v0.0.100 and v0.0.142 alike — including
--persistent-storage and a positional URL.
CREATE_NO_WINDOW is the whole difference. Same binary, arguments held
fixed, varying only the spawn:
| spawn | result |
|---|---|
piped stdio, no windowsHide | loaded in 490ms |
piped stdio + windowsHide | no webview — 0x800401f0 |
inherit stdio + windowsHide | no webview — 0x800401f0 |
inherit stdio, no windowsHide | loaded in 467ms |
GUI-subsystem parent + windowsHide | no webview — 0x800401f0 |
stdio makes no difference, the parent's subsystem makes no difference, and renaming the binary reproduces the table exactly.
This patch clears it. Two builds on one runner from commits one apart —
this change and its parent — same Zig, same -Doptimize=ReleaseSafe
-Dcpu=baseline, the pinned zig-js/zig-regex/zig-gc revisions, and the
WebView2Loader.dll from the release archive. git diff --stat between them is
1 file changed, 39 insertions(+); the parent's craft.exe does not reference
CoInitializeEx and this one does.
| spawn | parent commit | this branch |
|---|---|---|
piped stdio, no windowsHide | loaded in 2184ms | loaded in 812ms |
piped stdio + windowsHide | no webview — 0x800401f0 | loaded in 1163ms |
inherit stdio + windowsHide | no webview — exit 1 | loaded in 654ms |
inherit stdio, no windowsHide | loaded in 514ms | loaded in 466ms |
GUI-subsystem parent + windowsHide | no webview — 0x800401f0 | loaded in 519ms |
Every CREATE_NO_WINDOW failure becomes a load, the three spawns that already
worked still work and got no slower, and the last row is the real case: a
GUI-subsystem launcher spawning craft with inherited stdio and
CREATE_NO_WINDOW, which is what stacksjs/system-cleaner's desktop bundle does
and what reported WebView2InitFailed every time.
Harness and runs:
spawn probe,
workflow,
A/B run.
The first two are permalinks into a deleted experiment branch; the spawn probe
now lives on main as a
CI regression guard.
One thing worth fixing separately
The Zig this repository's Windows CI pins cannot be installed any more:
HTTP 404 downloading https://ziglang.org/builds/zig-x86_64-windows-0.17.0-dev.1963+e00c6c439.zipziglang.org keeps only the current master build, and pantry's registry mirrors
no windows-x86-64 dev build to outlive that pruning. windows-multi-window is
presumably green on a cached toolchain; a fresh runner would hit this. The A/B
above built with tagged 0.17.0 from ziglang.org, which compiled craft
without changes and satisfies minimum_zig_version.
🤖 Generated with Claude Code