also looking at this
fix(windows): initialise COM before creating the WebView2 environment
#348
1 file
+99
-0
| @@ -109,6 +109,9 @@ pub const EventRegistrationToken = extern struct { | ||
| 109 | 109 | |
| 110 | 110 | // Constants |
| 111 | 111 | pub const S_OK: HRESULT = 0; |
| 112 | /// Another apartment model was already chosen on this thread. | |
| 113 | pub const RPC_E_CHANGED_MODE: HRESULT = @as(i32, @bitCast(@as(u32, 0x8001_0106))); | |
| 114 | pub const COINIT_APARTMENTTHREADED: DWORD = 0x2; | |
| 112 | 115 | pub const WS_OVERLAPPEDWINDOW: DWORD = 0x00CF0000; |
| 113 | 116 | pub const WS_VISIBLE: DWORD = 0x10000000; |
| 114 | 117 | pub const WS_POPUP: DWORD = 0x80000000; |
| @@ -194,6 +197,8 @@ pub extern "user32" fn GetMonitorInfoW(hMonitor: *anyopaque, lpmi: *MONITORINFO) | ||
| 194 | 197 | pub extern "kernel32" fn GetModuleHandleW(lpModuleName: ?LPCWSTR) callconv(.c) ?HINSTANCE; |
| 195 | 198 | pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.c) void; |
| 196 | 199 | pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.c) void; |
| 200 | pub extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: DWORD) callconv(.c) HRESULT; | |
| 201 | pub extern "ole32" fn CoUninitialize() callconv(.c) void; | |
| 197 | 202 | |
| 198 | 203 | // ============================================================================ |
| 199 | 204 | // WebView2 COM vtable interfaces |
| @@ -1072,6 +1077,90 @@ const CreateCoreWebView2EnvironmentWithOptionsFn = *const fn ( | ||
| 1072 | 1077 | |
| 1073 | 1078 | var webview2_create_fn: ?CreateCoreWebView2EnvironmentWithOptionsFn = null; |
| 1074 | 1079 | |
| 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; | |
| 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. | |
| 1131 | fn 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. | |
| 1157 | fn releaseComIfOurs() void { | |
| 1158 | if (!com_initialized_here) return; | |
| 1159 | CoUninitialize(); | |
| 1160 | com_initialized_here = false; | |
| 1161 | com_apartment_present = false; | |
| 1162 | } | |
| 1163 | ||
| 1075 | 1164 | /// Dynamically load WebView2Loader.dll and resolve CreateCoreWebView2EnvironmentWithOptions. |
| 1076 | 1165 | /// Returns null if WebView2 runtime is not installed. |
| 1077 | 1166 | fn loadWebView2() ?CreateCoreWebView2EnvironmentWithOptionsFn { |
| @@ -1762,6 +1851,12 @@ pub const Window = struct { | ||
| 1762 | 1851 | y: i32, |
| 1763 | 1852 | |
| 1764 | 1853 | 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 | ||
| 1765 | 1860 | const hInstance = GetModuleHandleW(null) orelse return error.WindowCreationFailed; |
| 1766 | 1861 | |
| 1767 | 1862 | // Register window class if not already done |
| @@ -2209,6 +2304,10 @@ pub const App = struct { | ||
| 2209 | 2304 | pub fn run() !void { |
| 2210 | 2305 | app_running = true; |
| 2211 | 2306 | 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(); | |
| 2212 | 2311 | var msg: MSG = undefined; |
| 2213 | 2312 | |
| 2214 | 2313 | while (GetMessageW(&msg, null, 0, 0) != 0) { |