also looking at this
fix(windows): initialise COM before creating the WebView2 environment
#348
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.
| @@ -198,6 +198,7 @@ pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ? | ||
| 198 | 198 | pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.c) void; |
| 199 | 199 | pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.c) void; |
| 200 | 200 | pub extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: DWORD) callconv(.c) HRESULT; |
| 201 | pub extern "ole32" fn CoUninitialize() callconv(.c) void; | |
| 201 | 202 | |
| 202 | 203 | // ============================================================================ |
| 203 | 204 | // WebView2 COM vtable interfaces |
| @@ -1076,34 +1077,67 @@ const CreateCoreWebView2EnvironmentWithOptionsFn = *const fn ( | ||
| 1076 | 1077 | |
| 1077 | 1078 | var webview2_create_fn: ?CreateCoreWebView2EnvironmentWithOptionsFn = null; |
| 1078 | 1079 | |
| 1079 | /// Whether the call below has already put this thread in an apartment. | |
| 1080 | threadlocal var com_initialized = false; | |
| 1080 | /// Whether an apartment is known to exist on this thread, however it got there. | |
| 1081 | threadlocal 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. | |
| 1089 | threadlocal var com_initialized_here = false; | |
| 1081 | 1090 | |
| 1082 | 1091 | /// Put the calling thread in a COM apartment, which WebView2 requires. |
| 1083 | 1092 | /// |
| 1084 | 1093 | /// `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. | |
| 1089 | 1098 | /// |
| 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: | |
| 1095 | 1101 | /// |
| 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. | |
| 1099 | 1112 | fn ensureComInitialized() void { |
| 1100 | if (com_initialized) return; | |
| 1113 | if (com_apartment_present) return; | |
| 1114 | ||
| 1101 | 1115 | 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)) { | |
| 1103 | 1122 | std.debug.print("[WebView2] CoInitializeEx failed: 0x{x}\n", .{@as(u32, @bitCast(hr))}); |
| 1104 | 1123 | return; |
| 1105 | 1124 | } |
| 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. | |
| 1136 | fn releaseComIfOurs() void { | |
| 1137 | if (!com_initialized_here) return; | |
| 1138 | CoUninitialize(); | |
| 1139 | com_initialized_here = false; | |
| 1140 | com_apartment_present = false; | |
| 1107 | 1141 | } |
| 1108 | 1142 | |
| 1109 | 1143 | /// Dynamically load WebView2Loader.dll and resolve CreateCoreWebView2EnvironmentWithOptions. |
| @@ -2248,6 +2282,10 @@ pub const App = struct { | ||
| 2248 | 2282 | pub fn run() !void { |
| 2249 | 2283 | app_running = true; |
| 2250 | 2284 | 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(); | |
| 2251 | 2289 | var msg: MSG = undefined; |
| 2252 | 2290 | |
| 2253 | 2291 | while (GetMessageW(&msg, null, 0, 0) != 0) { |