From e9021e51e5cbb63a514015fcce81dbdbd7106b7e Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 7 Feb 2026 02:32:54 +0200 Subject: [PATCH] Statically link WebView2Loader - single exe, no DLL needed build.rs swaps the dynamic import library with the static archive from webview2-com-sys, so the WebView2 loader code is baked into the exe. msvc_compat.rs provides the MSVC CRT symbols (security cookie, thread-safe init, C++ operators) that the MSVC-compiled static library expects. --- src-tauri/build.rs | 70 ++++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 + src-tauri/src/msvc_compat.rs | 98 ++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 src-tauri/src/msvc_compat.rs diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 2c92123..678f9ae 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -7,5 +7,75 @@ fn main() { std::env::set_var("PATH", mingw_bin); } + // On GNU targets, replace the WebView2Loader import library with the static + // library so the loader is baked into the exe — no DLL to ship. + #[cfg(target_env = "gnu")] + swap_webview2_to_static(); + tauri_build::build() } + +/// Replace `WebView2Loader.dll.lib` (dynamic import lib) with the contents of +/// `WebView2LoaderStatic.lib` (static archive) in the webview2-com-sys build +/// output. The linker then statically links the WebView2 loader code, removing +/// the runtime dependency on WebView2Loader.dll. +#[cfg(target_env = "gnu")] +fn swap_webview2_to_static() { + use std::fs; + use std::path::PathBuf; + + let out_dir = std::env::var("OUT_DIR").unwrap_or_default(); + // OUT_DIR = target/.../build/core-cooldown-HASH/out + // We need: target/.../build/ (two levels up) + let build_dir = PathBuf::from(&out_dir) + .parent() // core-cooldown-HASH + .and_then(|p| p.parent()) // build/ + .map(|p| p.to_path_buf()); + + let build_dir = match build_dir { + Some(d) => d, + None => return, + }; + + let target_arch = match std::env::var("CARGO_CFG_TARGET_ARCH") + .unwrap_or_default() + .as_str() + { + "x86_64" => "x64", + "x86" => "x86", + "aarch64" => "arm64", + _ => return, + }; + + let entries = match fs::read_dir(&build_dir) { + Ok(e) => e, + Err(_) => return, + }; + + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + if !name.starts_with("webview2-com-sys-") { + continue; + } + + let lib_dir = entry.path().join("out").join(target_arch); + let import_lib = lib_dir.join("WebView2Loader.dll.lib"); + let static_lib = lib_dir.join("WebView2LoaderStatic.lib"); + + if static_lib.exists() && import_lib.exists() { + if let Ok(static_bytes) = fs::read(&static_lib) { + match fs::write(&import_lib, &static_bytes) { + Ok(_) => println!( + "cargo:warning=Swapped WebView2Loader to static linking ({})", + lib_dir.display() + ), + Err(e) => println!( + "cargo:warning=Failed to swap WebView2Loader lib: {}", + e + ), + } + } + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3b8d3e1..5bd9d3e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,6 @@ mod config; +#[cfg(all(windows, target_env = "gnu"))] +mod msvc_compat; mod stats; mod timer; diff --git a/src-tauri/src/msvc_compat.rs b/src-tauri/src/msvc_compat.rs new file mode 100644 index 0000000..5710516 --- /dev/null +++ b/src-tauri/src/msvc_compat.rs @@ -0,0 +1,98 @@ +//! Compatibility shims for MSVC CRT symbols required by WebView2LoaderStatic.lib. +//! +//! When statically linking the WebView2 loader on GNU/MinGW, the MSVC-compiled +//! object code references these symbols. We provide minimal implementations so +//! the linker can resolve them. + +use std::sync::atomic::{AtomicI32, Ordering}; + +// ── MSVC Buffer Security Check (/GS) ──────────────────────────────────────── +// +// MSVC's /GS flag instruments functions with stack canaries. These two symbols +// implement the canary check. The cookie value is arbitrary — real MSVC CRT +// randomises it at startup, but for a statically-linked helper library this +// fixed sentinel is sufficient. + +#[no_mangle] +pub static __security_cookie: u64 = 0x00002B992DDFA232; + +#[no_mangle] +pub unsafe extern "C" fn __security_check_cookie(cookie: u64) { + if cookie != __security_cookie { + std::process::abort(); + } +} + +// ── MSVC Thread-Safe Static Initialisation ─────────────────────────────────── +// +// C++11 guarantees that function-local statics are initialised exactly once, +// even under concurrent access. MSVC implements this with an epoch counter and +// a set of helper functions. The WebView2 loader uses a few statics internally. +// +// Simplified implementation: uses an atomic spin for the guard. This is safe +// because WebView2 initialisation runs on the main thread in practice. + +#[no_mangle] +pub static _Init_thread_epoch: AtomicI32 = AtomicI32::new(0); + +#[no_mangle] +pub unsafe extern "C" fn _Init_thread_header(guard: *mut i32) { + if guard.is_null() { + return; + } + // Spin until we can claim the guard (-1 = uninitialized, 0 = done, 1 = in progress) + loop { + let val = guard.read_volatile(); + if val == 0 { + // Already initialised — tell caller to skip + return; + } + if val == -1 { + // Not yet initialised — try to claim it + guard.write_volatile(1); + return; + } + // val == 1: another thread is initialising — yield and retry + std::thread::yield_now(); + } +} + +#[no_mangle] +pub unsafe extern "C" fn _Init_thread_footer(guard: *mut i32) { + if !guard.is_null() { + guard.write_volatile(0); // Mark initialisation complete + _Init_thread_epoch.fetch_add(1, Ordering::Release); + } +} + +// ── MSVC C++ Runtime Operators (mangled names) ─────────────────────────────── +// +// The static library is compiled with MSVC, which uses its own C++ name mangling. +// MinGW's libstdc++ exports the same operators but with GCC/Itanium mangling, +// so the linker can't match them. We provide the MSVC-mangled versions here. + +/// `std::nothrow` — MSVC-mangled `?nothrow@std@@3Unothrow_t@1@B` +/// An empty struct constant used as a tag for nothrow `new`. +#[export_name = "?nothrow@std@@3Unothrow_t@1@B"] +pub static MSVC_STD_NOTHROW: u8 = 0; + +/// `operator new(size_t, const std::nothrow_t&)` — nothrow allocation +/// MSVC-mangled: `??2@YAPEAX_KAEBUnothrow_t@std@@@Z` +#[export_name = "??2@YAPEAX_KAEBUnothrow_t@std@@@Z"] +pub unsafe extern "C" fn msvc_operator_new_nothrow(size: usize, _nothrow: *const u8) -> *mut u8 { + let size = if size == 0 { 1 } else { size }; + let layout = std::alloc::Layout::from_size_align_unchecked(size, 8); + let ptr = std::alloc::alloc(layout); + ptr // null on failure — nothrow semantics +} + +/// `operator delete(void*, size_t)` — sized deallocation +/// MSVC-mangled: `??3@YAXPEAX_K@Z` +#[export_name = "??3@YAXPEAX_K@Z"] +pub unsafe extern "C" fn msvc_operator_delete_sized(ptr: *mut u8, size: usize) { + if !ptr.is_null() { + let size = if size == 0 { 1 } else { size }; + let layout = std::alloc::Layout::from_size_align_unchecked(size, 8); + std::alloc::dealloc(ptr, layout); + } +}