ReviewOS

also looking at this

craft-native/craft

fix(windows): initialise COM before creating the WebView2 environment

#348
Closed glennmichael123 wants to merge fix/windows-com-init into main
1 file +99 -0

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/zig/src/windows.zigmodified+55-17
Changes to packages/zig/src/windows.zig
@@ -198,6 +198,7 @@ pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ?
198198pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.c) void;
199199pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.c) void;
200200pub extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: DWORD) callconv(.c) HRESULT;
201pub extern "ole32" fn CoUninitialize() callconv(.c) void;
201202
202203// ============================================================================
203204// WebView2 COM vtable interfaces
@@ -1076,34 +1077,67 @@ const CreateCoreWebView2EnvironmentWithOptionsFn = *const fn (
10761077
10771078var webview2_create_fn: ?CreateCoreWebView2EnvironmentWithOptionsFn = null;
10781079
1079/// Whether the call below has already put this thread in an apartment.
1080threadlocal var com_initialized = false;
1080/// Whether an apartment is known to exist on this thread, however it got there.
1081threadlocal var com_apartment_present = false;
1082
1083/// Whether *this* code created it, which is the only case it may undo.
1084///
1085/// Separate from the flag above because the two differ in exactly the case
1086/// that matters: `RPC_E_CHANGED_MODE` says an apartment exists and that this
1087/// call did not create it, so treating the two as one would eventually balance
1088/// a `CoUninitialize` against a `CoInitializeEx` that never took a reference.
1089threadlocal var com_initialized_here = false;
10811090
10821091/// Put the calling thread in a COM apartment, which WebView2 requires.
10831092///
10841093/// `CreateCoreWebView2EnvironmentWithOptions` fails with `CO_E_NOTINITIALIZED`
1085/// (0x800401f0) when the calling thread has no apartment, and nothing in craft
1086/// used to create one: `ole32` was linked for `CoTaskMemFree` alone. So on
1087/// Windows the environment never came up, and the process exited before a
1088/// window existed.
1094/// (0x800401f0) when the calling thread has no apartment, and craft used to
1095/// create none: `ole32` was linked for `CoTaskMemFree` alone. An embedder that
1096/// spawns craft with `CREATE_NO_WINDOW` gets no console, nothing else
1097/// initialises COM on its behalf, and no window is ever created.
10891098///
1090/// An STA, because WebView2 hosts UI on the thread that pumps messages and
1091/// that is the apartment Edge expects. Two non-`S_OK` results are still fine:
1092/// `S_FALSE` means the thread already had an apartment, and
1093/// `RPC_E_CHANGED_MODE` means an embedder got here first and chose an MTA,
1094/// which is theirs to choose and which WebView2 can still work in.
1099/// An STA, because WebView2 hosts UI on the thread that pumps messages. The
1100/// three outcomes are not the same thing and are not treated as such:
10951101///
1096/// There is deliberately no matching `CoUninitialize`. The apartment has to
1097/// outlive every webview created on the thread, and the only point at which
1098/// that is true is process exit, which tears it down regardless.
1102/// - `S_OK` — this call created the apartment.
1103/// - `S_FALSE` — the thread already had one in the same mode and this call
1104/// took a reference on it. Ours to release, like `S_OK`.
1105/// - `RPC_E_CHANGED_MODE` — the thread is already in an apartment with a
1106/// different concurrency model, so an embedder chose an MTA before craft
1107/// got here. Nothing was initialised and nothing may be released, but COM
1108/// is usable and the error above cannot occur, so this is not a failure
1109/// and craft does not override a choice that is the embedder's to make.
1110/// It is reported, because WebView2 expects an STA and that is where to
1111/// start looking if it misbehaves.
10991112fn ensureComInitialized() void {
1100 if (com_initialized) return;
1113 if (com_apartment_present) return;
1114
11011115 const hr = CoInitializeEx(null, COINIT_APARTMENTTHREADED);
1102 if (!succeeded(hr) and hr != RPC_E_CHANGED_MODE) {
1116 if (hr == RPC_E_CHANGED_MODE) {
1117 std.debug.print("[WebView2] COM is already initialised on this thread as an MTA; WebView2 expects an STA\n", .{});
1118 com_apartment_present = true;
1119 return;
1120 }
1121 if (!succeeded(hr)) {
11031122 std.debug.print("[WebView2] CoInitializeEx failed: 0x{x}\n", .{@as(u32, @bitCast(hr))});
11041123 return;
11051124 }
1106 com_initialized = true;
1125
1126 com_apartment_present = true;
1127 com_initialized_here = true;
1128}
1129
1130/// Release the apartment this thread took, if it took one.
1131///
1132/// Paired with the call above and run when the message loop ends, which is the
1133/// last moment the thread does anything with COM. Guarded by the thread-local,
1134/// so a thread that only inherited someone else's apartment — or that never
1135/// created a window — releases nothing.
1136fn releaseComIfOurs() void {
1137 if (!com_initialized_here) return;
1138 CoUninitialize();
1139 com_initialized_here = false;
1140 com_apartment_present = false;
11071141}
11081142
11091143/// Dynamically load WebView2Loader.dll and resolve CreateCoreWebView2EnvironmentWithOptions.
@@ -2248,6 +2282,10 @@ pub const App = struct {
22482282 pub fn run() !void {
22492283 app_running = true;
22502284 defer app_running = false;
2285 // Balances the CoInitializeEx that window creation performs on this
2286 // thread. After the loop returns the thread pumps nothing further, so
2287 // this is the last point at which it is still the apartment's owner.
2288 defer releaseComIfOurs();
22512289 var msg: MSG = undefined;
22522290
22532291 while (GetMessageW(&msg, null, 0, 0) != 0) {