From 17b4f5e2f796c4a08b968becf15324aefafa32f7 Mon Sep 17 00:00:00 2001 From: macovedj Date: Tue, 13 Oct 2026 14:31:45 -0500 Subject: [PATCH 1/3] rust: avoid rerunning constructors after WASI startup Register a startup marker that sets the existing run-once flag when constructors are invoked by _initialize or _start. Preserve the older toolchain fallback and add a constructor-counting component test covering the first and second export calls. --- crates/guest-rust/src/rt/mod.rs | 12 +++++++++++ tests/runtime/rust/ctors-run-once/runner.rs | 14 +++++++++++++ tests/runtime/rust/ctors-run-once/test.rs | 23 +++++++++++++++++++++ tests/runtime/rust/ctors-run-once/test.wit | 14 +++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 tests/runtime/rust/ctors-run-once/runner.rs create mode 100644 tests/runtime/rust/ctors-run-once/test.rs create mode 100644 tests/runtime/rust/ctors-run-once/test.wit diff --git a/crates/guest-rust/src/rt/mod.rs b/crates/guest-rust/src/rt/mod.rs index 27e233695..e250b811e 100644 --- a/crates/guest-rust/src/rt/mod.rs +++ b/crates/guest-rust/src/rt/mod.rs @@ -198,9 +198,21 @@ pub unsafe fn cabi_realloc( /// wit-bindgen-rust generates a call to this function at the start of all /// component export functions. Importantly, it is not called as part of /// `cabi_realloc`, which is a *core* export func, but should not execute ctors. +/// Constructors already invoked by `_initialize` or `_start` do not need to be +/// run again when an export is called. #[cfg(target_arch = "wasm32")] pub fn run_ctors_once() { static mut RUN: bool = false; + + extern "C" fn mark_run() { + unsafe { RUN = true } + } + + // Record startup regardless of who calls `__wasm_call_ctors`. + #[used] + #[unsafe(link_section = ".init_array")] + static MARK_RUN: extern "C" fn() = mark_run; + unsafe { if !RUN { // This function is synthesized by `wasm-ld` to run all static diff --git a/tests/runtime/rust/ctors-run-once/runner.rs b/tests/runtime/rust/ctors-run-once/runner.rs new file mode 100644 index 000000000..3d34844e3 --- /dev/null +++ b/tests/runtime/rust/ctors-run-once/runner.rs @@ -0,0 +1,14 @@ +include!(env!("BINDINGS")); + +struct Component; + +export!(Component); + +impl Guest for Component { + fn run() { + // Startup may have run the constructors at instantiation or at the + // first export call. Neither export call should run them again. + assert_eq!(the::test::i::ctor_runs(), 1); + assert_eq!(the::test::i::ctor_runs(), 1); + } +} diff --git a/tests/runtime/rust/ctors-run-once/test.rs b/tests/runtime/rust/ctors-run-once/test.rs new file mode 100644 index 000000000..b73434cda --- /dev/null +++ b/tests/runtime/rust/ctors-run-once/test.rs @@ -0,0 +1,23 @@ +include!(env!("BINDINGS")); + +use std::sync::atomic::{AtomicU32, Ordering::Relaxed}; + +static CTOR_RUNS: AtomicU32 = AtomicU32::new(0); + +extern "C" fn ctor() { + CTOR_RUNS.fetch_add(1, Relaxed); +} + +#[used] +#[unsafe(link_section = ".init_array")] +static CTOR: extern "C" fn() = ctor; + +struct Test; + +export!(Test); + +impl exports::the::test::i::Guest for Test { + fn ctor_runs() -> u32 { + CTOR_RUNS.load(Relaxed) + } +} diff --git a/tests/runtime/rust/ctors-run-once/test.wit b/tests/runtime/rust/ctors-run-once/test.wit new file mode 100644 index 000000000..aa7ff0d72 --- /dev/null +++ b/tests/runtime/rust/ctors-run-once/test.wit @@ -0,0 +1,14 @@ +package the:test; + +interface i { + ctor-runs: func() -> u32; +} + +world runner { + import i; + export run: func(); +} + +world test { + export i; +} From 18c2d113253d0762882f88c293710fed5c4539a0 Mon Sep 17 00:00:00 2001 From: macovedj Date: Tue, 20 Oct 2026 08:17:39 -0500 Subject: [PATCH 2/3] rust: disable constructor workaround for WASI on Rust 1.100+ Detect rustc in the runtime build script and make run_ctors_once a no-op when WASI startup provides _initialize. Retain the existing helper for older Rust and non-WASI targets, including when version detection fails. --- Cargo.lock | 1 + crates/guest-rust/Cargo.toml | 3 +++ crates/guest-rust/build.rs | 9 ++++++++ crates/guest-rust/src/rt/mod.rs | 40 +++++++++++++-------------------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d93a5990d..db6e8f0b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1505,6 +1505,7 @@ dependencies = [ "futures", "rustc-std-workspace-alloc", "rustc-std-workspace-core", + "version_check", "wit-bindgen-rust-macro", ] diff --git a/crates/guest-rust/Cargo.toml b/crates/guest-rust/Cargo.toml index 7f1d12449..a19c8ce4d 100644 --- a/crates/guest-rust/Cargo.toml +++ b/crates/guest-rust/Cargo.toml @@ -16,6 +16,9 @@ workspace = true [package.metadata.docs.rs] all-features = true +[build-dependencies] +version_check = "0.9.5" + [dependencies] wit-bindgen-rust-macro = { path = "./macro", optional = true, default-features = false, version = "0.62.0" } bitflags = { workspace = true, optional = true } diff --git a/crates/guest-rust/build.rs b/crates/guest-rust/build.rs index 0793c250d..41c4f79e1 100644 --- a/crates/guest-rust/build.rs +++ b/crates/guest-rust/build.rs @@ -2,6 +2,15 @@ use std::env; use std::path::PathBuf; fn main() { + println!("cargo:rustc-check-cfg=cfg(wasi_has_initialize)"); + + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + // Rust 1.100 supplies `_initialize` for WASI cdylibs: + // https://github.com/rust-lang/rust/pull/161421 + // Remove the version check once the MSRV reaches 1.100. + if target_os == "wasi" && version_check::is_min_version("1.100.0") == Some(true) { + println!("cargo:rustc-cfg=wasi_has_initialize"); + } let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or(String::new()); let target_family = env::var("CARGO_CFG_TARGET_FAMILY").unwrap_or(String::new()); let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or(String::new()); diff --git a/crates/guest-rust/src/rt/mod.rs b/crates/guest-rust/src/rt/mod.rs index e250b811e..df7141fe9 100644 --- a/crates/guest-rust/src/rt/mod.rs +++ b/crates/guest-rust/src/rt/mod.rs @@ -198,34 +198,26 @@ pub unsafe fn cabi_realloc( /// wit-bindgen-rust generates a call to this function at the start of all /// component export functions. Importantly, it is not called as part of /// `cabi_realloc`, which is a *core* export func, but should not execute ctors. -/// Constructors already invoked by `_initialize` or `_start` do not need to be -/// run again when an export is called. #[cfg(target_arch = "wasm32")] pub fn run_ctors_once() { - static mut RUN: bool = false; - - extern "C" fn mark_run() { - unsafe { RUN = true } - } + #[cfg(not(wasi_has_initialize))] + { + static mut RUN: bool = false; - // Record startup regardless of who calls `__wasm_call_ctors`. - #[used] - #[unsafe(link_section = ".init_array")] - static MARK_RUN: extern "C" fn() = mark_run; - - unsafe { - if !RUN { - // This function is synthesized by `wasm-ld` to run all static - // constructors. wasm-ld will either provide an implementation - // of this symbol, or synthesize a wrapper around each - // exported function to (unconditionally) run ctors. By using - // this function, the linked module is opting into "manually" - // running ctors. - unsafe extern "C" { - fn __wasm_call_ctors(); + unsafe { + if !RUN { + // This function is synthesized by `wasm-ld` to run all static + // constructors. wasm-ld will either provide an implementation + // of this symbol, or synthesize a wrapper around each + // exported function to (unconditionally) run ctors. By using + // this function, the linked module is opting into "manually" + // running ctors. + unsafe extern "C" { + fn __wasm_call_ctors(); + } + __wasm_call_ctors(); + RUN = true; } - __wasm_call_ctors(); - RUN = true; } } } From f5dc5c0d2a209d71982d7129724dcdc38985e8e9 Mon Sep 17 00:00:00 2001 From: macovedj Date: Tue, 20 Oct 2026 12:18:48 -0500 Subject: [PATCH 3/3] rust: check compiler version without an extra dependency Read the compiler minor version directly in the runtime build script, following the wasmparser pattern. Preserve the constructor fallback when compiler detection fails. --- Cargo.lock | 1 - crates/guest-rust/Cargo.toml | 3 --- crates/guest-rust/build.rs | 14 +++++++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db6e8f0b8..d93a5990d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1505,7 +1505,6 @@ dependencies = [ "futures", "rustc-std-workspace-alloc", "rustc-std-workspace-core", - "version_check", "wit-bindgen-rust-macro", ] diff --git a/crates/guest-rust/Cargo.toml b/crates/guest-rust/Cargo.toml index a19c8ce4d..7f1d12449 100644 --- a/crates/guest-rust/Cargo.toml +++ b/crates/guest-rust/Cargo.toml @@ -16,9 +16,6 @@ workspace = true [package.metadata.docs.rs] all-features = true -[build-dependencies] -version_check = "0.9.5" - [dependencies] wit-bindgen-rust-macro = { path = "./macro", optional = true, default-features = false, version = "0.62.0" } bitflags = { workspace = true, optional = true } diff --git a/crates/guest-rust/build.rs b/crates/guest-rust/build.rs index 41c4f79e1..b7f0f94c5 100644 --- a/crates/guest-rust/build.rs +++ b/crates/guest-rust/build.rs @@ -1,5 +1,6 @@ use std::env; use std::path::PathBuf; +use std::process::Command; fn main() { println!("cargo:rustc-check-cfg=cfg(wasi_has_initialize)"); @@ -8,7 +9,7 @@ fn main() { // Rust 1.100 supplies `_initialize` for WASI cdylibs: // https://github.com/rust-lang/rust/pull/161421 // Remove the version check once the MSRV reaches 1.100. - if target_os == "wasi" && version_check::is_min_version("1.100.0") == Some(true) { + if target_os == "wasi" && rustc_minor_version().unwrap_or(0) >= 100 { println!("cargo:rustc-cfg=wasi_has_initialize"); } let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or(String::new()); @@ -35,6 +36,17 @@ fn main() { } } +fn rustc_minor_version() -> Option { + let rustc = env::var_os("RUSTC")?; + let output = Command::new(rustc).arg("--version").output().ok()?; + let version = std::str::from_utf8(&output.stdout).ok()?; + let mut pieces = version.split('.'); + if pieces.next() != Some("rustc 1") { + return None; + } + pieces.next()?.parse().ok() +} + fn link_lib(name: &str) { let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); let mut src = env::current_dir().unwrap();