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.
This commit is contained in:
Your Name
2026-02-07 02:32:54 +02:00
parent 37d0d638d5
commit e9021e51e5
3 changed files with 170 additions and 0 deletions

View File

@@ -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
),
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
mod config;
#[cfg(all(windows, target_env = "gnu"))]
mod msvc_compat;
mod stats;
mod timer;

View File

@@ -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);
}
}