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
packages/zig/src/windows.zigmodified+99-0
Changes to packages/zig/src/windows.zig
@@ -109,6 +109,9 @@ pub const EventRegistrationToken = extern struct {
109109
110110// Constants
111111pub const S_OK: HRESULT = 0;
112/// Another apartment model was already chosen on this thread.
113pub const RPC_E_CHANGED_MODE: HRESULT = @as(i32, @bitCast(@as(u32, 0x8001_0106)));
114pub const COINIT_APARTMENTTHREADED: DWORD = 0x2;
112115pub const WS_OVERLAPPEDWINDOW: DWORD = 0x00CF0000;
113116pub const WS_VISIBLE: DWORD = 0x10000000;
114117pub const WS_POPUP: DWORD = 0x80000000;
@@ -194,6 +197,8 @@ pub extern "user32" fn GetMonitorInfoW(hMonitor: *anyopaque, lpmi: *MONITORINFO)
194197pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ?HINSTANCE;
195198pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.c) void;
196199pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.c) void;
200pub extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: DWORD) callconv(.c) HRESULT;
201pub extern "ole32" fn CoUninitialize() callconv(.c) void;
197202
198203// ============================================================================
199204// WebView2 COM vtable interfaces
@@ -1072,6 +1077,90 @@ const CreateCoreWebView2EnvironmentWithOptionsFn = *const fn (
10721077
10731078var webview2_create_fn: ?CreateCoreWebView2EnvironmentWithOptionsFn = null;
10741079
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;
1090
1091/// Put the calling thread in a single-threaded apartment, which WebView2 needs.
1092///
1093/// Craft used to create none — `ole32` was linked for `CoTaskMemFree` alone —
1094/// and an embedder that spawns it with `CREATE_NO_WINDOW` gets no console,
1095/// nothing else initialises COM on its behalf, and
1096/// `CreateCoreWebView2EnvironmentWithOptions` fails with
1097/// `CO_E_NOTINITIALIZED` before any window exists.
1098///
1099/// The apartment model is not a preference here. Microsoft's reference for
1100/// that function is explicit:
1101///
1102/// > The WebView2 environment and all other WebView2 objects are single
1103/// > threaded and have dependencies on Windows components that require COM
1104/// > to be initialized for a single-threaded apartment. The app is expected
1105/// > to run `CoInitializeEx` before running
1106/// > `CreateCoreWebView2EnvironmentWithOptions`.
1107///
1108/// > If `CoInitializeEx` did not run or previously ran with
1109/// > `COINIT_MULTITHREADED`, `CreateCoreWebView2EnvironmentWithOptions`
1110/// > fails with one of the following errors.
1111/// > CO_E_NOTINITIALIZED — if CoInitializeEx was not called
1112/// > RPC_E_CHANGED_MODE — if CoInitializeEx was previously called with
1113/// > COINIT_MULTITHREADED
1114///
1115/// So each result gets the treatment it deserves:
1116///
1117/// - `S_OK` — this call created the apartment.
1118/// - `S_FALSE` — the thread already had one in the same mode and this call
1119/// took a reference on it. Ours to release, like `S_OK`.
1120/// - `RPC_E_CHANGED_MODE` — the thread is in a multithreaded apartment.
1121/// Nothing was initialised and nothing may be released, and crucially
1122/// WebView2 will *not* work here: environment creation fails with this
1123/// same code. An earlier version of this carried on and warned, which only
1124/// moved the failure later and made it harder to read. It now refuses, so
1125/// the thread that is wrong is named at the point it is wrong. Hosting the
1126/// webview on a dedicated STA thread would also answer this, and is a
1127/// larger change than one call site.
1128/// - anything else — initialisation genuinely failed. Refused for the same
1129/// reason: continuing only produces `CO_E_NOTINITIALIZED` further along,
1130/// where it says less about what went wrong.
1131fn ensureComInitialized() !void {
1132 if (com_apartment_present) return;
1133
1134 const hr = CoInitializeEx(null, COINIT_APARTMENTTHREADED);
1135 if (hr == RPC_E_CHANGED_MODE) {
1136 std.debug.print(
1137 "[WebView2] This thread is in a multithreaded apartment. WebView2 requires a single-threaded apartment and would fail with RPC_E_CHANGED_MODE; create the window on a thread that is an STA, or one craft can initialise.\n",
1138 .{},
1139 );
1140 return error.ComApartmentIsMultithreaded;
1141 }
1142 if (!succeeded(hr)) {
1143 std.debug.print("[WebView2] CoInitializeEx failed: 0x{x}\n", .{@as(u32, @bitCast(hr))});
1144 return error.ComInitializationFailed;
1145 }
1146
1147 com_apartment_present = true;
1148 com_initialized_here = true;
1149}
1150
1151/// Release the apartment this thread took, if it took one.
1152///
1153/// Paired with the call above and run when the message loop ends, which is the
1154/// last moment the thread does anything with COM. Guarded by the thread-local,
1155/// so a thread that only inherited someone else's apartment — or that never
1156/// created a window — releases nothing.
1157fn releaseComIfOurs() void {
1158 if (!com_initialized_here) return;
1159 CoUninitialize();
1160 com_initialized_here = false;
1161 com_apartment_present = false;
1162}
1163
10751164/// Dynamically load WebView2Loader.dll and resolve CreateCoreWebView2EnvironmentWithOptions.
10761165/// Returns null if WebView2 runtime is not installed.
10771166fn loadWebView2() ?CreateCoreWebView2EnvironmentWithOptionsFn {
@@ -1762,6 +1851,12 @@ pub const Window = struct {
17621851 y: i32,
17631852
17641853 pub fn create(options: .WindowOptions) !Window {
1854 // Before anything asks WebView2 for an environment, and fatal when it
1855 // cannot be done: every reason it fails is also a reason environment
1856 // creation would fail, so carrying on would only report the same
1857 // problem later and less clearly.
1858 try ensureComInitialized();
1859
17651860 const hInstance = GetModuleHandleW(null) orelse return error.WindowCreationFailed;
17661861
17671862 // Register window class if not already done
@@ -2209,6 +2304,10 @@ pub const App = struct {
22092304 pub fn run() !void {
22102305 app_running = true;
22112306 defer app_running = false;
2307 // Balances the CoInitializeEx that window creation performs on this
2308 // thread. After the loop returns the thread pumps nothing further, so
2309 // this is the last point at which it is still the apartment's owner.
2310 defer releaseComIfOurs();
22122311 var msg: MSG = undefined;
22132312
22142313 while (GetMessageW(&msg, null, 0, 0) != 0) {