From f35c0cb649869dbbe9c46f1c2a0435aeca7e2ed2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 13:53:19 -0700 Subject: [PATCH 01/32] Small fixes for the SDK --- lib/api/src/backend/js/entities/module.rs | 26 ++++++++++++++----- lib/c-api-imports/Cargo.toml | 7 +++++ lib/c-api-imports/src/lib.rs | 8 ++++++ lib/virtual-fs/src/mount_fs.rs | 5 +++- lib/wasix/src/state/handles/thread_local.rs | 7 ++--- .../src/syscalls/wasix/callback_signal.rs | 8 +++++- 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 74926a736eaa..235df72b7ba0 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -40,6 +40,8 @@ pub struct ModuleTypeHints { pub struct Module { module: JsHandle, name: Option, + #[cfg(feature = "wasm-types-polyfill")] + info: ModuleInfo, // WebAssembly type hints type_hints: Option, #[cfg(feature = "js-serializable-module")] @@ -91,23 +93,24 @@ impl Module { // The module is now validated, so we can safely parse it's types #[cfg(feature = "wasm-types-polyfill")] - let (type_hints, name) = { - let info = crate::polyfill::translate_module(&binary[..]).unwrap(); + let (type_hints, name, module_info) = { + let translated = crate::polyfill::translate_module(&binary[..]).unwrap(); ( Some(ModuleTypeHints { - imports: info + imports: translated .info .imports() .map(|import| import.ty().clone()) .collect::>(), - exports: info + exports: translated .info .exports() .map(|export| export.ty().clone()) .collect::>(), }), - info.info.name, + translated.info.name.clone(), + translated.info, ) }; #[cfg(not(feature = "wasm-types-polyfill"))] @@ -117,6 +120,8 @@ impl Module { module: JsHandle::new(module), type_hints, name, + #[cfg(feature = "wasm-types-polyfill")] + info: module_info, #[cfg(feature = "js-serializable-module")] raw_bytes: Some(binary), } @@ -458,7 +463,14 @@ impl Module { } pub(crate) fn info(&self) -> &ModuleInfo { - unimplemented!() + #[cfg(feature = "wasm-types-polyfill")] + { + &self.info + } + #[cfg(not(feature = "wasm-types-polyfill"))] + { + unimplemented!("module info requires the wasm-types-polyfill feature") + } } } @@ -469,6 +481,8 @@ impl From for Module { module: JsHandle::new(module), name: None, type_hints: None, + #[cfg(feature = "wasm-types-polyfill")] + info: ModuleInfo::default(), #[cfg(feature = "js-serializable-module")] raw_bytes: None, } diff --git a/lib/c-api-imports/Cargo.toml b/lib/c-api-imports/Cargo.toml index 1cab0fac44c8..43358e0f95d3 100644 --- a/lib/c-api-imports/Cargo.toml +++ b/lib/c-api-imports/Cargo.toml @@ -16,10 +16,17 @@ anyhow.workspace = true wasmer-api = { version = "=7.2.1", path = "../api", default-features = false, package = "wasmer" } # Only needed to implement `wasmer_wasix::runtime::InstantiationHook` for the # hooks, so WASIX embedders can register them directly. + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] wasmer-wasix = { version = "=0.702.1", path = "../wasix", optional = true, features = [ "sys", ], default-features = false } +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasmer-wasix = { version = "=0.702.1", path = "../wasix", optional = true, features = [ + "js", +], default-features = false } + [dev-dependencies] tokio = { workspace = true, features = [ "rt", diff --git a/lib/c-api-imports/src/lib.rs b/lib/c-api-imports/src/lib.rs index 860aa252ad72..5a0a95f4df02 100644 --- a/lib/c-api-imports/src/lib.rs +++ b/lib/c-api-imports/src/lib.rs @@ -376,6 +376,14 @@ struct WasmCapiEnv { func_env: Option>, } +// The JavaScript backend executes each WASIX segment on one worker at a time +// and explicitly transfers its WebAssembly module and shared memory before the +// segment runs. wasm-bindgen correctly marks raw JS handles as !Send, but the +// worker scheduler provides the stronger single-owner invariant required by +// `FunctionEnv`. +#[cfg(target_arch = "wasm32")] +unsafe impl Send for WasmCapiEnv {} + // ABI discriminants and layout constants copied from the WebAssembly C API // headers (`wasm.h`). const WASM_I32: u8 = 0; diff --git a/lib/virtual-fs/src/mount_fs.rs b/lib/virtual-fs/src/mount_fs.rs index 39b4d2c06b5b..7dd8c9ea14cb 100644 --- a/lib/virtual-fs/src/mount_fs.rs +++ b/lib/virtual-fs/src/mount_fs.rs @@ -4,14 +4,17 @@ use crate::*; +#[cfg(not(feature = "js"))] +use std::time::{SystemTime, UNIX_EPOCH}; use std::{ borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::OsString, path::{Path, PathBuf}, sync::{Arc, RwLock}, - time::{SystemTime, UNIX_EPOCH}, }; +#[cfg(feature = "js")] +use web_time::{SystemTime, UNIX_EPOCH}; const MIN_METADATA_TIMESTAMP: u64 = 1_000_000_000; // 1 second in nano seconds diff --git a/lib/wasix/src/state/handles/thread_local.rs b/lib/wasix/src/state/handles/thread_local.rs index fd01509bf359..5b1a504344d9 100644 --- a/lib/wasix/src/state/handles/thread_local.rs +++ b/lib/wasix/src/state/handles/thread_local.rs @@ -103,7 +103,7 @@ impl WasiInstanceHandlesPointer { THREAD_LOCAL_INSTANCE_HANDLES.with(|map| { let map = map.borrow_mut(); if let Some(inner) = map.get(&id) { - let borrow: RefMut = inner.borrow_mut(); + let borrow: RefMut = inner.try_borrow_mut().ok()?; let borrow: RefMut<'static, WasiModuleTreeHandles> = unsafe { std::mem::transmute(borrow) }; Some(WasiInstanceGuardMut { @@ -138,8 +138,9 @@ impl WasiInstanceHandlesPointer { } fn destroy(id: u64) { THREAD_LOCAL_INSTANCE_HANDLES.with(|map| { - let mut map = map.borrow_mut(); - map.remove(&id); + if let Ok(mut map) = map.try_borrow_mut() { + map.remove(&id); + } }) } } diff --git a/lib/wasix/src/syscalls/wasix/callback_signal.rs b/lib/wasix/src/syscalls/wasix/callback_signal.rs index 940b34d26deb..dc00893df58a 100644 --- a/lib/wasix/src/syscalls/wasix/callback_signal.rs +++ b/lib/wasix/src/syscalls/wasix/callback_signal.rs @@ -37,7 +37,13 @@ pub fn callback_signal( Span::current().record("funct_is_some", funct.is_some()); { - let mut env_inner = ctx.data_mut().inner_mut(); + // Signal registration can be re-entered by a guest callback while + // another syscall holds the instance handles. In that case the + // callback is already active, so leave the existing registration in + // place instead of panicking on a nested RefCell borrow. + let Some(mut env_inner) = ctx.data_mut().try_inner_mut() else { + return Ok(()); + }; let inner = env_inner.main_module_instance_handles_mut(); inner.signal = funct; inner.signal_set = true; From 82cefa1f7937a521752a8f3d55869c6fc1e2342c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 15:46:36 -0700 Subject: [PATCH 02/32] Fix panic in Wasmer when used as library in debug mode --- lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index 1ca37e499129..65cdffef66c1 100644 --- a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs +++ b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs @@ -411,7 +411,7 @@ impl CompactUnwindManager { "Personality function does not appear in GOT table!".into(), )); }; - let delta = (personality_pointer - self.dso_base) as u32; + let delta = personality_pointer.wrapping_sub(self.dso_base) as u32; self.write(delta)?; } From cb17195009b58bc05167572c5ec70e3506d1de65 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 16:01:52 -0700 Subject: [PATCH 03/32] fix(compiler): use JIT image base for macOS compact unwind Compact-unwind entries encode unsigned 32-bit offsets from the base returned by the dynamic-unwind callback. Using Wasmer's dylib base is invalid when JIT code, LSDA data, or the personality GOT slot is mapped below the dylib. This caused subtraction panics in debug builds and invalid libunwind pointers, resulting in SIGBUS during forced guest termination when Wasmer was loaded as a dynamic library. Derive the base from the actual JIT image addresses and validate that the complete address range and personality offset fit in u32. --- .../unwind/systemv/compact_unwind/mod.rs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index 65cdffef66c1..a71b77a09d46 100644 --- a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs +++ b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs @@ -201,27 +201,30 @@ impl CompactUnwindManager { return Ok(()); } - let mut info = libc::Dl_info { - dli_fname: core::ptr::null(), - dli_fbase: core::ptr::null_mut(), - dli_sname: core::ptr::null(), - dli_saddr: core::ptr::null_mut(), - }; - - unsafe { - /* xxx: Must find a better way to find a dso_base */ - if let Some(personality) = self.personalities.first() { - _ = libc::dladdr(*personality as *const _, &mut info as *mut _); - } - - if info.dli_fbase.is_null() { - _ = libc::dladdr( - wasmer_vm::libcalls::wasmer_eh_personality as *const _, - &mut info as *mut _, - ); + let personality = self.maybe_eh_personality_addr_in_got.ok_or_else(|| { + CompileError::Codegen("Personality function does not appear in GOT table!".into()) + })?; + + // Compact-unwind addresses are unsigned 32-bit offsets from the base + // returned by our dynamic-unwind callback. A JIT's code, LSDA data, + // and personality GOT slot may be mapped on either side of Wasmer's + // dylib, so that dylib's Mach-O base is not a valid common base. + let mut start = personality; + let mut end = personality; + for entry in &self.compact_unwind_entries { + start = start.min(entry.function_addr); + end = end.max(entry.function_addr.saturating_add(entry.length as usize)); + if entry.lsda_addr != 0 { + start = start.min(entry.lsda_addr); + end = end.max(entry.lsda_addr); } } - self.dso_base = info.dli_fbase as usize; + if end.saturating_sub(start) > u32::MAX as usize { + return Err(CompileError::Codegen( + "compact-unwind addresses exceed the 32-bit image-relative range".into(), + )); + } + self.dso_base = start; self.write_unwind_info()?; @@ -411,7 +414,14 @@ impl CompactUnwindManager { "Personality function does not appear in GOT table!".into(), )); }; - let delta = personality_pointer.wrapping_sub(self.dso_base) as u32; + let delta = personality_pointer + .checked_sub(self.dso_base) + .and_then(|delta| u32::try_from(delta).ok()) + .ok_or_else(|| { + CompileError::Codegen( + "compact-unwind personality is outside the image-relative range".into(), + ) + })?; self.write(delta)?; } From 64b0344def9779c41d6a1c509607ae48c4768ad8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 15:46:36 -0700 Subject: [PATCH 04/32] Fix panic in Wasmer when used as library in debug mode --- lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index 1ca37e499129..65cdffef66c1 100644 --- a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs +++ b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs @@ -411,7 +411,7 @@ impl CompactUnwindManager { "Personality function does not appear in GOT table!".into(), )); }; - let delta = (personality_pointer - self.dso_base) as u32; + let delta = personality_pointer.wrapping_sub(self.dso_base) as u32; self.write(delta)?; } From fb5297cb25065b46a109cd94ef24c9d1ac037cf5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 16:01:52 -0700 Subject: [PATCH 05/32] fix(compiler): use JIT image base for macOS compact unwind Compact-unwind entries encode unsigned 32-bit offsets from the base returned by the dynamic-unwind callback. Using Wasmer's dylib base is invalid when JIT code, LSDA data, or the personality GOT slot is mapped below the dylib. This caused subtraction panics in debug builds and invalid libunwind pointers, resulting in SIGBUS during forced guest termination when Wasmer was loaded as a dynamic library. Derive the base from the actual JIT image addresses and validate that the complete address range and personality offset fit in u32. --- .../unwind/systemv/compact_unwind/mod.rs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index 65cdffef66c1..a71b77a09d46 100644 --- a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs +++ b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs @@ -201,27 +201,30 @@ impl CompactUnwindManager { return Ok(()); } - let mut info = libc::Dl_info { - dli_fname: core::ptr::null(), - dli_fbase: core::ptr::null_mut(), - dli_sname: core::ptr::null(), - dli_saddr: core::ptr::null_mut(), - }; - - unsafe { - /* xxx: Must find a better way to find a dso_base */ - if let Some(personality) = self.personalities.first() { - _ = libc::dladdr(*personality as *const _, &mut info as *mut _); - } - - if info.dli_fbase.is_null() { - _ = libc::dladdr( - wasmer_vm::libcalls::wasmer_eh_personality as *const _, - &mut info as *mut _, - ); + let personality = self.maybe_eh_personality_addr_in_got.ok_or_else(|| { + CompileError::Codegen("Personality function does not appear in GOT table!".into()) + })?; + + // Compact-unwind addresses are unsigned 32-bit offsets from the base + // returned by our dynamic-unwind callback. A JIT's code, LSDA data, + // and personality GOT slot may be mapped on either side of Wasmer's + // dylib, so that dylib's Mach-O base is not a valid common base. + let mut start = personality; + let mut end = personality; + for entry in &self.compact_unwind_entries { + start = start.min(entry.function_addr); + end = end.max(entry.function_addr.saturating_add(entry.length as usize)); + if entry.lsda_addr != 0 { + start = start.min(entry.lsda_addr); + end = end.max(entry.lsda_addr); } } - self.dso_base = info.dli_fbase as usize; + if end.saturating_sub(start) > u32::MAX as usize { + return Err(CompileError::Codegen( + "compact-unwind addresses exceed the 32-bit image-relative range".into(), + )); + } + self.dso_base = start; self.write_unwind_info()?; @@ -411,7 +414,14 @@ impl CompactUnwindManager { "Personality function does not appear in GOT table!".into(), )); }; - let delta = personality_pointer.wrapping_sub(self.dso_base) as u32; + let delta = personality_pointer + .checked_sub(self.dso_base) + .and_then(|delta| u32::try_from(delta).ok()) + .ok_or_else(|| { + CompileError::Codegen( + "compact-unwind personality is outside the image-relative range".into(), + ) + })?; self.write(delta)?; } From f5025901b6a8d4e3d3c3ddf6d8eedeadc9053916 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 15:46:36 -0700 Subject: [PATCH 06/32] Fix panic in Wasmer when used as library in debug mode --- .../unwind/systemv/compact_unwind/mod.rs | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index a71b77a09d46..65cdffef66c1 100644 --- a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs +++ b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs @@ -201,30 +201,27 @@ impl CompactUnwindManager { return Ok(()); } - let personality = self.maybe_eh_personality_addr_in_got.ok_or_else(|| { - CompileError::Codegen("Personality function does not appear in GOT table!".into()) - })?; - - // Compact-unwind addresses are unsigned 32-bit offsets from the base - // returned by our dynamic-unwind callback. A JIT's code, LSDA data, - // and personality GOT slot may be mapped on either side of Wasmer's - // dylib, so that dylib's Mach-O base is not a valid common base. - let mut start = personality; - let mut end = personality; - for entry in &self.compact_unwind_entries { - start = start.min(entry.function_addr); - end = end.max(entry.function_addr.saturating_add(entry.length as usize)); - if entry.lsda_addr != 0 { - start = start.min(entry.lsda_addr); - end = end.max(entry.lsda_addr); + let mut info = libc::Dl_info { + dli_fname: core::ptr::null(), + dli_fbase: core::ptr::null_mut(), + dli_sname: core::ptr::null(), + dli_saddr: core::ptr::null_mut(), + }; + + unsafe { + /* xxx: Must find a better way to find a dso_base */ + if let Some(personality) = self.personalities.first() { + _ = libc::dladdr(*personality as *const _, &mut info as *mut _); + } + + if info.dli_fbase.is_null() { + _ = libc::dladdr( + wasmer_vm::libcalls::wasmer_eh_personality as *const _, + &mut info as *mut _, + ); } } - if end.saturating_sub(start) > u32::MAX as usize { - return Err(CompileError::Codegen( - "compact-unwind addresses exceed the 32-bit image-relative range".into(), - )); - } - self.dso_base = start; + self.dso_base = info.dli_fbase as usize; self.write_unwind_info()?; @@ -414,14 +411,7 @@ impl CompactUnwindManager { "Personality function does not appear in GOT table!".into(), )); }; - let delta = personality_pointer - .checked_sub(self.dso_base) - .and_then(|delta| u32::try_from(delta).ok()) - .ok_or_else(|| { - CompileError::Codegen( - "compact-unwind personality is outside the image-relative range".into(), - ) - })?; + let delta = personality_pointer.wrapping_sub(self.dso_base) as u32; self.write(delta)?; } From b5718d1e5ca74f3ed58e2ae4aa5457309bc6ed68 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 28 Jul 2026 16:01:52 -0700 Subject: [PATCH 07/32] fix(compiler): use JIT image base for macOS compact unwind Compact-unwind entries encode unsigned 32-bit offsets from the base returned by the dynamic-unwind callback. Using Wasmer's dylib base is invalid when JIT code, LSDA data, or the personality GOT slot is mapped below the dylib. This caused subtraction panics in debug builds and invalid libunwind pointers, resulting in SIGBUS during forced guest termination when Wasmer was loaded as a dynamic library. Derive the base from the actual JIT image addresses and validate that the complete address range and personality offset fit in u32. --- .../unwind/systemv/compact_unwind/mod.rs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index 65cdffef66c1..a71b77a09d46 100644 --- a/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs +++ b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs @@ -201,27 +201,30 @@ impl CompactUnwindManager { return Ok(()); } - let mut info = libc::Dl_info { - dli_fname: core::ptr::null(), - dli_fbase: core::ptr::null_mut(), - dli_sname: core::ptr::null(), - dli_saddr: core::ptr::null_mut(), - }; - - unsafe { - /* xxx: Must find a better way to find a dso_base */ - if let Some(personality) = self.personalities.first() { - _ = libc::dladdr(*personality as *const _, &mut info as *mut _); - } - - if info.dli_fbase.is_null() { - _ = libc::dladdr( - wasmer_vm::libcalls::wasmer_eh_personality as *const _, - &mut info as *mut _, - ); + let personality = self.maybe_eh_personality_addr_in_got.ok_or_else(|| { + CompileError::Codegen("Personality function does not appear in GOT table!".into()) + })?; + + // Compact-unwind addresses are unsigned 32-bit offsets from the base + // returned by our dynamic-unwind callback. A JIT's code, LSDA data, + // and personality GOT slot may be mapped on either side of Wasmer's + // dylib, so that dylib's Mach-O base is not a valid common base. + let mut start = personality; + let mut end = personality; + for entry in &self.compact_unwind_entries { + start = start.min(entry.function_addr); + end = end.max(entry.function_addr.saturating_add(entry.length as usize)); + if entry.lsda_addr != 0 { + start = start.min(entry.lsda_addr); + end = end.max(entry.lsda_addr); } } - self.dso_base = info.dli_fbase as usize; + if end.saturating_sub(start) > u32::MAX as usize { + return Err(CompileError::Codegen( + "compact-unwind addresses exceed the 32-bit image-relative range".into(), + )); + } + self.dso_base = start; self.write_unwind_info()?; @@ -411,7 +414,14 @@ impl CompactUnwindManager { "Personality function does not appear in GOT table!".into(), )); }; - let delta = personality_pointer.wrapping_sub(self.dso_base) as u32; + let delta = personality_pointer + .checked_sub(self.dso_base) + .and_then(|delta| u32::try_from(delta).ok()) + .ok_or_else(|| { + CompileError::Codegen( + "compact-unwind personality is outside the image-relative range".into(), + ) + })?; self.write(delta)?; } From b5332d4b93eb6e3dcc4df805796120d2fff1e595 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 11:26:40 -0700 Subject: [PATCH 08/32] Added Module new_async function --- Cargo.lock | 1 + lib/api/Cargo.toml | 3 +- lib/api/src/backend/js/entities/module.rs | 36 +++++++++++++++++++++++ lib/api/src/entities/module/inner.rs | 22 ++++++++++++++ lib/api/src/entities/module/mod.rs | 14 +++++++++ lib/api/tests/module.rs | 21 +++++++++++++ 6 files changed, 96 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 8abc5ae77bc2..a58965036e2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7188,6 +7188,7 @@ dependencies = [ "tracing", "ureq 3.3.0", "wasm-bindgen", + "wasm-bindgen-futures", "wasm-bindgen-test", "wasmer-compiler", "wasmer-compiler-cranelift", diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index a8ef7e6de5cc..47b2723a0857 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -86,6 +86,7 @@ wasmer-types = { path = "../types", version = "=7.2.1", default-features = false "std", ] } wasm-bindgen.workspace = true +wasm-bindgen-futures = { workspace = true, optional = true } js-sys.workspace = true wasmer-derive = { path = "../derive", version = "=7.2.1" } wasmer-compiler = { path = "../compiler", version = "=7.2.1" } @@ -158,7 +159,7 @@ v8-default = ["v8", "wat"] wasm-c-api = ["wasm-types-polyfill"] # Features for `js`. -js = ["wasm-bindgen", "js-sys"] +js = ["wasm-bindgen", "wasm-bindgen-futures", "js-sys"] js-default = ["js", "std", "wasm-types-polyfill"] wasm-types-polyfill = ["wasmparser"] diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 235df72b7ba0..0cf1c12ce16c 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -3,7 +3,11 @@ use std::path::Path; use bytes::Bytes; use js_sys::{Reflect, Uint8Array, WebAssembly}; use tracing::{debug, warn}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; use wasm_bindgen::{JsValue, prelude::*}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::JsFuture; use wasmer_types::{ CompileError, DeserializeError, ExportType, ExportsIterator, ExternType, FunctionType, GlobalType, ImportType, ImportsIterator, MemoryType, ModuleInfo, Mutability, Pages, @@ -62,6 +66,38 @@ impl From for JsValue { } impl Module { + #[cfg(target_arch = "wasm32")] + pub(crate) async fn new_async( + _engine: &impl AsEngineRef, + binary: &[u8], + ) -> Result { + // Copy the bytes into JavaScript-owned memory before awaiting. A view + // into Wasm linear memory could be invalidated if that memory grows. + let js_bytes = Uint8Array::from(binary); + let module = JsFuture::from(WebAssembly::compile(&js_bytes)) + .await + .map_err(|error| { + CompileError::Validate( + error + .as_string() + .or_else(|| { + Reflect::get(&error, &JsValue::from_str("message")) + .ok() + .and_then(|message| message.as_string()) + }) + .unwrap_or_else(|| "Unknown validation error".to_owned()), + ) + })? + .dyn_into::() + .map_err(|_| { + CompileError::Validate( + "WebAssembly.compile returned an unexpected value".to_owned(), + ) + })?; + + Ok(unsafe { Self::from_js_module(module, binary) }) + } + pub(crate) fn from_binary( _engine: &impl AsEngineRef, binary: &[u8], diff --git a/lib/api/src/entities/module/inner.rs b/lib/api/src/entities/module/inner.rs index ed24347ef04c..618b52208740 100644 --- a/lib/api/src/entities/module/inner.rs +++ b/lib/api/src/entities/module/inner.rs @@ -44,6 +44,28 @@ impl BackendModule { Self::from_binary(engine, bytes.as_ref()) } + #[inline] + pub async fn new_async( + engine: &impl AsEngineRef, + bytes: impl AsRef<[u8]>, + ) -> Result { + #[cfg(all(feature = "js", target_arch = "wasm32"))] + if matches!(engine.as_engine_ref().inner.be, crate::BackendEngine::Js(_)) { + #[cfg(feature = "wat")] + let bytes = wat::parse_bytes(bytes.as_ref()).map_err(|e| { + CompileError::Wasm(WasmError::Generic(format!( + "Error when converting wat: {e}", + ))) + })?; + + return crate::backend::js::entities::module::Module::new_async(engine, bytes.as_ref()) + .await + .map(Self::Js); + } + + Self::new(engine, bytes) + } + #[inline] pub fn new_with_progress( engine: &impl AsEngineRef, diff --git a/lib/api/src/entities/module/mod.rs b/lib/api/src/entities/module/mod.rs index ebafc5b2a366..5114c7fa6b4d 100644 --- a/lib/api/src/entities/module/mod.rs +++ b/lib/api/src/entities/module/mod.rs @@ -113,6 +113,20 @@ impl Module { BackendModule::new(engine, bytes).map(Self) } + /// Asynchronously creates a new WebAssembly module. + /// + /// This is equivalent to [`Module::new`] on backends whose compilation is + /// synchronous. The JavaScript backend uses the host's asynchronous + /// WebAssembly compilation API, which avoids blocking the browser's main + /// thread and supports modules that browsers reject for synchronous + /// compilation there. + pub async fn new_async( + engine: &impl AsEngineRef, + bytes: impl AsRef<[u8]>, + ) -> Result { + BackendModule::new_async(engine, bytes).await.map(Self) + } + /// Creates a new WebAssembly module from a file path. pub fn from_file( engine: &impl AsEngineRef, diff --git a/lib/api/tests/module.rs b/lib/api/tests/module.rs index 31969694d0c1..abaccd5f7811 100644 --- a/lib/api/tests/module.rs +++ b/lib/api/tests/module.rs @@ -9,6 +9,27 @@ use std::ffi::OsStr; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; +async fn assert_module_new_async() -> Result<(), String> { + let store = Store::default(); + let module = Module::new_async(&store, "(module (func (export \"run\")))") + .await + .map_err(|error| format!("{error:?}"))?; + assert!(module.exports().any(|export| export.name() == "run")); + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn module_new_async() -> Result<(), String> { + futures::executor::block_on(assert_module_new_async()) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn module_new_async() -> Result<(), JsValue> { + assert_module_new_async().await.map_err(JsValue::from_str) +} + #[engine_test] fn module_get_name() -> Result<(), String> { let store = Store::default(); From 86f4264123b5deef071451859a8116474fc2af37 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 12:38:07 -0700 Subject: [PATCH 09/32] Improved polyfill support of table element types --- lib/api/src/backend/js/entities/module.rs | 101 ++++++++++++++++++- lib/api/src/backend/js/entities/table.rs | 3 +- lib/api/src/backend/js/vm/function.rs | 63 +++++++++++- lib/api/src/utils/polyfill.rs | 115 ++++++++++++++++++++-- 4 files changed, 268 insertions(+), 14 deletions(-) diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 0cf1c12ce16c..c1140e531513 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{collections::HashMap, path::Path}; use bytes::Bytes; use js_sys::{Reflect, Uint8Array, WebAssembly}; @@ -10,8 +10,8 @@ use wasm_bindgen::{JsValue, prelude::*}; use wasm_bindgen_futures::JsFuture; use wasmer_types::{ CompileError, DeserializeError, ExportType, ExportsIterator, ExternType, FunctionType, - GlobalType, ImportType, ImportsIterator, MemoryType, ModuleInfo, Mutability, Pages, - SerializeError, TableType, Type, + GlobalIndex, GlobalType, ImportIndex, ImportType, ImportsIterator, InitExpr, InitExprOp, + MemoryType, ModuleInfo, Mutability, Pages, SerializeError, TableIndex, TableType, Type, }; use crate::{ @@ -52,6 +52,39 @@ pub struct Module { raw_bytes: Option, } +#[cfg(feature = "wasm-types-polyfill")] +fn evaluate_i32_init_expr( + expression: &InitExpr, + globals: &HashMap, +) -> Option { + let mut stack = Vec::::new(); + for operation in expression.ops() { + match operation { + InitExprOp::GlobalGetI32(index) | InitExprOp::GlobalGetI64(index) => { + stack.push(*globals.get(index)?); + } + InitExprOp::I32Const(value) => stack.push(i64::from(*value)), + InitExprOp::I64Const(value) => stack.push(*value), + InitExprOp::I32Add | InitExprOp::I64Add => { + let rhs = stack.pop()?; + let lhs = stack.pop()?; + stack.push(lhs.checked_add(rhs)?); + } + InitExprOp::I32Sub | InitExprOp::I64Sub => { + let rhs = stack.pop()?; + let lhs = stack.pop()?; + stack.push(lhs.checked_sub(rhs)?); + } + InitExprOp::I32Mul | InitExprOp::I64Mul => { + let rhs = stack.pop()?; + let lhs = stack.pop()?; + stack.push(lhs.checked_mul(rhs)?); + } + } + } + u32::try_from(stack.pop()?).ok().filter(|_| stack.is_empty()) +} + // XXX // Do not rely on `Module` being `Send`: it will panic at runtime // if accessed from multiple threads thanks to [`JsHandle`]. @@ -246,8 +279,66 @@ impl Module { // in case the import is not found, the JS Wasm VM will handle // the error for us, so we don't need to handle it } - WebAssembly::Instance::new(&self.module, &imports_object) - .map_err(|e: JsValue| -> RuntimeError { e.into() }) + let instance = WebAssembly::Instance::new(&self.module, &imports_object) + .map_err(|e: JsValue| -> RuntimeError { e.into() })?; + #[cfg(feature = "wasm-types-polyfill")] + self.annotate_imported_table_functions(store, imports); + Ok(instance) + } + + #[cfg(feature = "wasm-types-polyfill")] + fn annotate_imported_table_functions( + &self, + store: &mut impl AsStoreMut, + imports: &Imports, + ) { + let mut tables = HashMap::::new(); + let mut globals = HashMap::::new(); + + for (key, import_index) in &self.info.imports { + let Some(extern_) = imports.get_export(&key.module, &key.field) else { + continue; + }; + match (import_index, extern_) { + (ImportIndex::Table(index), Extern::Table(table)) => { + tables.insert(*index, table); + } + (ImportIndex::Global(index), Extern::Global(global)) => { + let value = match global.get(store) { + crate::Value::I32(value) => i64::from(value), + crate::Value::I64(value) => value, + _ => continue, + }; + globals.insert(*index, value); + } + _ => {} + } + } + for initializer in &self.info.table_initializers { + let Some(table) = tables.get(&initializer.table_index) else { + continue; + }; + let Some(start) = evaluate_i32_init_expr(&initializer.offset_expr, &globals) else { + continue; + }; + for (offset, function_index) in initializer.elements.iter().enumerate() { + let Some(signature_index) = self.info.functions.get(*function_index) else { + continue; + }; + let Some(function_type) = self.info.signatures.get(*signature_index) else { + continue; + }; + let Ok(function) = table + .as_js() + .handle + .table + .get(start.saturating_add(offset as u32)) + else { + continue; + }; + crate::js::vm::VMFunction::annotate_type(&function, function_type); + } + } } pub fn name(&self) -> Option<&str> { diff --git a/lib/api/src/backend/js/entities/table.rs b/lib/api/src/backend/js/entities/table.rs index 40d224b3d2a7..91f8c562cc0f 100644 --- a/lib/api/src/backend/js/entities/table.rs +++ b/lib/api/src/backend/js/entities/table.rs @@ -72,7 +72,8 @@ impl Table { pub fn get(&self, store: &mut impl AsStoreMut, index: u32) -> Option { if let Ok(func) = self.handle.table.get(index) { - let ty = FunctionType::new(vec![], vec![]); + let ty = VMFunction::type_from_js(&func) + .unwrap_or_else(|| FunctionType::new(vec![], vec![])); let vm_function = VMFunction::new(func, ty); let function = crate::Function::from_vm_extern( store, diff --git a/lib/api/src/backend/js/vm/function.rs b/lib/api/src/backend/js/vm/function.rs index 6f50bfe28ce9..681319b53752 100644 --- a/lib/api/src/backend/js/vm/function.rs +++ b/lib/api/src/backend/js/vm/function.rs @@ -1,8 +1,45 @@ use std::any::Any; use crate::js::utils::js_handle::JsHandle; -use js_sys::Function as JsFunction; -use wasmer_types::{FunctionType, RawValue}; +use js_sys::{Array, Function as JsFunction, Reflect, Symbol}; +use wasm_bindgen::{JsCast, JsValue}; +use wasmer_types::{FunctionType, RawValue, Type}; + +fn type_key() -> Symbol { + Symbol::for_("wasmer.function-type") +} + +fn encode_types(types: &[Type]) -> Array { + Array::from_iter(types.iter().map(|ty| { + JsValue::from_f64(match ty { + Type::I32 => 0.0, + Type::I64 => 1.0, + Type::F32 => 2.0, + Type::F64 => 3.0, + Type::V128 => 4.0, + Type::ExternRef => 5.0, + Type::FuncRef => 6.0, + Type::ExceptionRef => 7.0, + }) + })) +} + +fn decode_types(types: &Array) -> Option> { + types + .iter() + .map(|value| match value.as_f64()? as u8 { + 0 => Some(Type::I32), + 1 => Some(Type::I64), + 2 => Some(Type::F32), + 3 => Some(Type::F64), + 4 => Some(Type::V128), + 5 => Some(Type::ExternRef), + 6 => Some(Type::FuncRef), + 7 => Some(Type::ExceptionRef), + _ => None, + }) + .collect() +} /// The VM Function type #[derive(Clone, Eq)] @@ -16,11 +53,33 @@ unsafe impl Sync for VMFunction {} impl VMFunction { pub(crate) fn new(function: JsFunction, ty: FunctionType) -> Self { + Self::annotate_type(&function, &ty); Self { function: JsHandle::new(function), ty, } } + + pub(crate) fn annotate_type(function: &JsFunction, ty: &FunctionType) { + let encoded = Array::of2( + &encode_types(ty.params()).into(), + &encode_types(ty.results()).into(), + ); + let _ = Reflect::set(&function, type_key().as_ref(), &encoded); + } + + pub(crate) fn type_from_js(function: &JsFunction) -> Option { + let encoded = Reflect::get(function, type_key().as_ref()) + .ok()? + .dyn_into::() + .ok()?; + let params = encoded.get(0).dyn_into::().ok()?; + let results = encoded.get(1).dyn_into::().ok()?; + Some(FunctionType::new( + decode_types(¶ms)?, + decode_types(&results)?, + )) + } } impl PartialEq for VMFunction { diff --git a/lib/api/src/utils/polyfill.rs b/lib/api/src/utils/polyfill.rs index 320c8316047b..a8acaad3535c 100644 --- a/lib/api/src/utils/polyfill.rs +++ b/lib/api/src/utils/polyfill.rs @@ -7,17 +7,19 @@ use core::convert::TryFrom; use std::vec::Vec; use wasmer_types::entity::EntityRef; +use wasmer_types::entity::packed_option::ReservedValue; use wasmer_types::{ ExportIndex, FunctionIndex, FunctionType, GlobalIndex, GlobalType, ImportIndex, MemoryIndex, - MemoryType, ModuleInfo, Pages, SignatureHash, SignatureIndex, TableIndex, TableType, TagIndex, - TagType, Type, + InitExpr, InitExprOp, MemoryType, ModuleInfo, Pages, SignatureHash, SignatureIndex, + TableIndex, TableInitializer, TableType, TagIndex, TagType, Type, }; use wasmparser::{ - self, BinaryReaderError, Export, ExportSectionReader, ExternalKind, FunctionSectionReader, - GlobalSectionReader, GlobalType as WPGlobalType, ImportSectionReader, Imports, - MemorySectionReader, MemoryType as WPMemoryType, NameSectionReader, Parser, Payload, - TableSectionReader, TagType as WPTagType, TypeRef, TypeSectionReader, + self, BinaryReaderError, ElementItems, ElementKind, ElementSectionReader, Export, + ExportSectionReader, ExternalKind, FunctionSectionReader, GlobalSectionReader, + GlobalType as WPGlobalType, ImportSectionReader, Imports, MemorySectionReader, + MemoryType as WPMemoryType, NameSectionReader, Operator, Parser, Payload, TableSectionReader, + TagType as WPTagType, TypeRef, TypeSectionReader, }; pub type WasmResult = Result; @@ -343,6 +345,10 @@ pub fn translate_module(data: &[u8]) -> WasmResult { parse_start_section(func, &mut module_info)?; } + Payload::ElementSection(elements) => { + parse_element_section(elements, &mut module_info)?; + } + Payload::TagSection(tags) => { parse_tag_section(tags, &mut module_info)?; } @@ -373,6 +379,103 @@ pub fn translate_module(data: &[u8]) -> WasmResult { Ok(module_info) } +fn parse_element_section( + elements: ElementSectionReader<'_>, + module: &mut ModuleInfoPolyfill, +) -> WasmResult<()> { + for element in elements { + let element = element.map_err(transform_err)?; + let ElementKind::Active { + table_index, + offset_expr, + } = element.kind + else { + continue; + }; + + let mut functions = Vec::new(); + match element.items { + ElementItems::Functions(items) => { + for item in items { + functions.push(FunctionIndex::from_u32(item.map_err(transform_err)?)); + } + } + ElementItems::Expressions(_, items) => { + for item in items { + let expression = item.map_err(transform_err)?; + let operator = expression + .get_operators_reader() + .read() + .map_err(transform_err)?; + match operator { + Operator::RefFunc { function_index } => { + functions.push(FunctionIndex::from_u32(function_index)); + } + Operator::RefNull { .. } => { + functions.push(FunctionIndex::reserved_value()); + } + other => { + return Err(format!( + "unsupported element expression in type polyfill: {other:?}" + )); + } + } + } + } + } + + module.info.table_initializers.push(TableInitializer { + table_index: TableIndex::from_u32(table_index.unwrap_or(0)), + offset_expr: parse_init_expr(&offset_expr, &module.info)?, + elements: functions.into_boxed_slice(), + }); + } + Ok(()) +} + +fn parse_init_expr( + expression: &wasmparser::ConstExpr<'_>, + module: &ModuleInfo, +) -> WasmResult { + let mut reader = expression.get_operators_reader(); + let mut operations = Vec::new(); + loop { + match reader.read().map_err(transform_err)? { + Operator::End => break, + Operator::I32Const { value } => operations.push(InitExprOp::I32Const(value)), + Operator::I64Const { value } => operations.push(InitExprOp::I64Const(value)), + Operator::GlobalGet { global_index } => { + let index = GlobalIndex::from_u32(global_index); + match module + .global_type(index) + .ok_or_else(|| format!("unknown global {global_index} in element offset"))? + .ty + { + Type::I32 => operations.push(InitExprOp::GlobalGetI32(index)), + Type::I64 => operations.push(InitExprOp::GlobalGetI64(index)), + other => { + return Err(format!( + "unsupported {other:?} global in element offset" + )); + } + } + } + Operator::I32Add => operations.push(InitExprOp::I32Add), + Operator::I32Sub => operations.push(InitExprOp::I32Sub), + Operator::I32Mul => operations.push(InitExprOp::I32Mul), + Operator::I64Add => operations.push(InitExprOp::I64Add), + Operator::I64Sub => operations.push(InitExprOp::I64Sub), + Operator::I64Mul => operations.push(InitExprOp::I64Mul), + other => { + return Err(format!( + "unsupported operator in element offset: {other:?}" + )); + } + } + } + Ok(InitExpr::new(operations.into_boxed_slice())) +} + fn validate_exported_types(module_info: &ModuleInfoPolyfill) -> WasmResult<()> { for (name, export) in module_info.info.exports.iter() { let uses_externref = match export { From 07a480c4624b49c97ecdda6d5af1b88ff43573c8 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 12:38:39 -0700 Subject: [PATCH 10/32] Added support for jspi --- lib/api/Cargo.toml | 1 + .../src/backend/js/entities/function/env.rs | 153 ++++++++- .../src/backend/js/entities/function/mod.rs | 309 ++++++++++++++++++ .../src/backend/js/entities/function/typed.rs | 57 ++++ lib/api/src/backend/js/jspi.rs | 81 +++++ lib/api/src/backend/js/mod.rs | 2 + lib/api/src/entities/engine/mod.rs | 4 +- lib/api/src/entities/function/env/inner.rs | 87 +++-- lib/api/src/entities/function/inner.rs | 22 +- lib/api/src/utils/native/typed_func.rs | 7 +- lib/api/tests/jspi_async.rs | 2 +- lib/api/tests/jspi_async_js.rs | 42 +++ lib/api/tests/module.rs | 47 ++- lib/api/tests/simple_greenthread.rs | 195 +++++++---- 14 files changed, 904 insertions(+), 105 deletions(-) create mode 100644 lib/api/src/backend/js/jspi.rs create mode 100644 lib/api/tests/jspi_async_js.rs diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index 47b2723a0857..ae4df557f33b 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -101,6 +101,7 @@ target-lexicon.workspace = true [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wat.workspace = true anyhow.workspace = true +futures.workspace = true wasm-bindgen-test.workspace = true macro-wasmer-engine-test = { version = "7.2.1", path = "./macro-wasmer-engine-test" } diff --git a/lib/api/src/backend/js/entities/function/env.rs b/lib/api/src/backend/js/entities/function/env.rs index deb51a2a9236..367019cbf494 100644 --- a/lib/api/src/backend/js/entities/function/env.rs +++ b/lib/api/src/backend/js/entities/function/env.rs @@ -5,6 +5,12 @@ use crate::{ js::{store::StoreHandle, vm::VMFunctionEnvironment}, store::{AsStoreMut, AsStoreRef, StoreRef}, }; +#[cfg(feature = "experimental-async")] +use crate::{ + AsStoreAsync, StoreAsync, StoreAsyncReadLock, StoreAsyncWriteLock, +}; +#[cfg(feature = "experimental-async")] +use wasmer_types::StoreId; #[derive(Debug)] #[repr(transparent)] @@ -33,7 +39,7 @@ impl FunctionEnv { /// Get the data as reference pub fn as_ref<'a>(&self, store: &'a impl AsStoreRef) -> &'a T where - T: Any + Send + 'static + Sized, + T: Any + 'static + Sized, { self.handle .get(store.as_store_ref().objects().as_js()) @@ -52,7 +58,7 @@ impl FunctionEnv { /// Get the data as mutable pub fn as_mut<'a>(&self, store: &'a mut impl AsStoreMut) -> &'a mut T where - T: Any + Send + 'static + Sized, + T: Any + 'static + Sized, { self.handle .get_mut(store.objects_mut().as_js_mut()) @@ -64,7 +70,7 @@ impl FunctionEnv { /// Convert it into a `FunctionEnvMut` pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut<'_, T> where - T: Any + Send + 'static + Sized, + T: Any + 'static + Sized, { FunctionEnvMut { store_mut: store.as_store_mut(), @@ -146,6 +152,11 @@ impl FunctionEnvMut<'_, T> { let data = unsafe { &mut *data }; (data, self.store_mut.as_store_mut()) } + + #[cfg(feature = "experimental-async")] + pub fn as_store_async(&self) -> Option { + self.store_mut.as_store_async() + } } impl AsStoreRef for FunctionEnvMut<'_, T> { @@ -205,3 +216,139 @@ impl From> for crate::FunctionEnv { Self(crate::BackendFunctionEnv::Js(value)) } } + +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvMut { + pub(crate) store: StoreAsync, + pub(crate) func_env: FunctionEnv, +} + +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandle { + read_lock: StoreAsyncReadLock, + pub(crate) func_env: FunctionEnv, +} + +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandleMut { + write_lock: StoreAsyncWriteLock, + pub(crate) func_env: FunctionEnv, +} + +#[cfg(feature = "experimental-async")] +impl Clone for AsyncFunctionEnvMut { + fn clone(&self) -> Self { + Self { + store: StoreAsync { + id: self.store.id, + inner: self.store.inner.clone(), + }, + func_env: self.func_env.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl Debug for AsyncFunctionEnvMut +where + T: Send + Debug + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.store.inner.try_read() { + Some(read_lock) => self.func_env.as_ref(&read_lock).fmt(f), + None => write!(f, "AsyncFunctionEnvMut {{ }}"), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvMut { + pub(crate) fn store_id(&self) -> StoreId { + self.store.id + } + + pub async fn read(&self) -> AsyncFunctionEnvHandle { + AsyncFunctionEnvHandle { + read_lock: self.store.read_lock().await, + func_env: self.func_env.clone(), + } + } + + pub async fn write(&self) -> AsyncFunctionEnvHandleMut { + AsyncFunctionEnvHandleMut { + write_lock: self.store.write_lock().await, + func_env: self.func_env.clone(), + } + } + + pub fn as_ref(&self) -> FunctionEnv { + self.func_env.clone() + } + + pub fn as_mut(&mut self) -> Self { + self.clone() + } + + pub fn as_store_async(&self) -> impl AsStoreAsync + 'static { + StoreAsync { + id: self.store.id, + inner: self.store.inner.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandle { + pub fn data(&self) -> &T { + self.func_env.as_ref(&self.read_lock) + } + + pub fn data_and_store(&self) -> (&T, &impl AsStoreRef) { + (self.data(), &self.read_lock) + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for AsyncFunctionEnvHandle { + fn as_store_ref(&self) -> StoreRef<'_> { + self.read_lock.as_store_ref() + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandleMut { + pub fn data_mut(&mut self) -> &mut T { + self.func_env.as_mut(&mut self.write_lock) + } + + pub fn data_and_store_mut(&mut self) -> (&mut T, &mut impl AsStoreMut) { + let data = self.data_mut() as *mut T; + let data = unsafe { &mut *data }; + (data, &mut self.write_lock) + } + + pub fn as_function_env_mut(&mut self) -> FunctionEnvMut<'_, T> { + FunctionEnvMut { + store_mut: self.write_lock.as_store_mut(), + func_env: self.func_env.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for AsyncFunctionEnvHandleMut { + fn as_store_ref(&self) -> StoreRef<'_> { + self.write_lock.as_store_ref() + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreMut for AsyncFunctionEnvHandleMut { + fn as_store_mut(&mut self) -> StoreMut<'_> { + self.write_lock.as_store_mut() + } + + fn objects_mut(&mut self) -> &mut crate::StoreObjects { + self.write_lock.objects_mut() + } +} diff --git a/lib/api/src/backend/js/entities/function/mod.rs b/lib/api/src/backend/js/entities/function/mod.rs index 4d442fae57ad..1dd70c27f003 100644 --- a/lib/api/src/backend/js/entities/function/mod.rs +++ b/lib/api/src/backend/js/entities/function/mod.rs @@ -1,11 +1,17 @@ pub(crate) mod env; pub(crate) mod typed; use std::marker::PhantomData; +#[cfg(feature = "experimental-async")] +use std::{future::Future, pin::Pin, rc::Rc, sync::Arc}; pub(crate) use typed::*; use js_sys::{Array, Function as JsFunction}; +#[cfg(feature = "experimental-async")] +use js_sys::{Promise, Reflect}; use wasm_bindgen::{JsCast, prelude::*}; +#[cfg(feature = "experimental-async")] +use wasm_bindgen_futures::{JsFuture, future_to_promise}; use wasmer_types::{FunctionType, RawValue}; use crate::{ @@ -19,6 +25,12 @@ use crate::{ }, vm::{VMExtern, VMExternFunction}, }; +#[cfg(feature = "experimental-async")] +use crate::{ + AsStoreAsync, AsyncFunctionEnvMut, BackendAsyncFunctionEnvMut, StoreAsync, StoreContext, + entities::function::async_host::{AsyncFunctionEnv, AsyncHostFunction}, + js::{function::env::AsyncFunctionEnvMut as JsAsyncFunctionEnvMut, jspi}, +}; use std::panic::{self, AssertUnwindSafe}; @@ -50,6 +62,210 @@ impl Function { )) } + #[cfg(feature = "experimental-async")] + pub(crate) fn new_async( + store: &mut impl AsStoreMut, + ty: FT, + func: F, + ) -> Self + where + FT: Into, + F: Fn(&[Value]) -> Fut + 'static, + Fut: Future, RuntimeError>> + 'static, + { + let env = FunctionEnv::new(store, ()); + Self::new_with_env_async(store, &env, ty, move |_env, values| func(values)) + } + + #[cfg(feature = "experimental-async")] + pub(crate) fn new_with_env_async( + store: &mut impl AsStoreMut, + env: &FunctionEnv, + ty: FT, + func: F, + ) -> Self + where + FT: Into, + F: Fn(AsyncFunctionEnvMut, &[Value]) -> Fut + 'static, + Fut: Future, RuntimeError>> + 'static, + T: 'static, + { + assert!( + jspi::is_supported(), + "the JavaScript host does not support WebAssembly JSPI" + ); + + let function_type = ty.into(); + let func_ty = function_type.clone(); + let store_id = store.objects_mut().id(); + let raw_env = env.as_js().clone(); + let func = Rc::new(func); + let wrapped_func = Closure::wrap(Box::new(move |args: &Array| -> Promise { + let Some(async_store) = jspi::active_store(store_id) else { + return Promise::reject(&JsValue::from_str( + "an async host function was called outside Function::call_async", + )); + }; + let callback_store = async_store.store(); + let js_env = JsAsyncFunctionEnvMut { + store: async_store, + func_env: raw_env.clone(), + }; + let env_mut = + AsyncFunctionEnvMut(BackendAsyncFunctionEnvMut::Js(js_env)); + let values = function_type + .params() + .iter() + .enumerate() + .map(|(index, ty)| js_value_to_wasmer(ty, &args.get(index as u32))) + .collect::>(); + let result_types = function_type.results().to_vec(); + let func = Rc::clone(&func); + + future_to_promise(async move { + let write_lock = callback_store.write_lock().await; + let store_context = StoreContext::install_async(write_lock.inner); + let future = func(env_mut, &values); + drop(store_context); + + let results = future.await.map_err(JsValue::from)?; + match result_types.len() { + 0 => Ok(JsValue::UNDEFINED), + 1 => Ok(wasmer_value_to_js(&results[0])), + _ => Ok(wasmer_array_to_js_array(&results).into()), + } + }) + }) as Box Promise>) + .into_js_value(); + + let variadic = + JsFunction::new_with_args("f", "return f(Array.prototype.slice.call(arguments, 1))"); + let function = variadic + .bind1(&JsValue::UNDEFINED, &wrapped_func) + .unchecked_into::(); + let function = match jspi::suspending(&function) { + Ok(function) => function, + Err(error) => wasm_bindgen::throw_val(error), + }; + let vm_function = VMFunction::new(function, func_ty); + Self::from_vm_extern( + &mut store.as_store_mut(), + VMExternFunction::Js(vm_function), + ) + } + + #[cfg(feature = "experimental-async")] + pub(crate) fn new_typed_async( + store: &mut impl AsStoreMut, + func: F, + ) -> Self + where + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, + F: AsyncHostFunction<(), Args, Rets, WithoutEnv> + 'static, + { + let env = FunctionEnv::new(store, ()); + let signature = FunctionType::new(Args::wasm_types(), Rets::wasm_types()); + let args_sig = Arc::new(signature.clone()); + let results_sig = Arc::new(signature.clone()); + let func = Arc::new(func); + Self::new_with_env_async( + store, + &env, + signature, + move |mut env_mut, values| -> Pin< + Box, RuntimeError>>>, + > { + let js_env = match env_mut.0 { + BackendAsyncFunctionEnvMut::Js(ref mut js_env) => js_env, + _ => panic!("Not a js backend"), + }; + let mut store_wrapper = unsafe { StoreContext::get_current(js_env.store_id()) }; + let mut store_mut = store_wrapper.as_mut(); + let args = match typed_args_from_values::( + &mut store_mut, + args_sig.as_ref(), + values, + ) { + Ok(args) => args, + Err(error) => return Box::pin(async { Err(error) }), + }; + drop(store_wrapper); + let func = Arc::clone(&func); + let results_sig = Arc::clone(&results_sig); + let future = func + .as_ref() + .call_async(AsyncFunctionEnv::new(), args); + Box::pin(async move { + let typed_result = future.await?; + let mut store_mut = env_mut.write().await; + typed_results_to_values::( + &mut store_mut.as_store_mut(), + results_sig.as_ref(), + typed_result, + ) + }) + }, + ) + } + + #[cfg(feature = "experimental-async")] + pub(crate) fn new_typed_with_env_async( + store: &mut impl AsStoreMut, + env: &FunctionEnv, + func: F, + ) -> Self + where + T: 'static, + F: AsyncHostFunction + 'static, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, + { + let signature = FunctionType::new(Args::wasm_types(), Rets::wasm_types()); + let args_sig = Arc::new(signature.clone()); + let results_sig = Arc::new(signature.clone()); + let func = Arc::new(func); + Self::new_with_env_async( + store, + env, + signature, + move |mut env_mut, values| -> Pin< + Box, RuntimeError>>>, + > { + let js_env = match env_mut.0 { + BackendAsyncFunctionEnvMut::Js(ref mut js_env) => js_env, + _ => panic!("Not a js backend"), + }; + let mut store_wrapper = unsafe { StoreContext::get_current(js_env.store_id()) }; + let mut store_mut = store_wrapper.as_mut(); + let args = match typed_args_from_values::( + &mut store_mut, + args_sig.as_ref(), + values, + ) { + Ok(args) => args, + Err(error) => return Box::pin(async { Err(error) }), + }; + drop(store_wrapper); + let env_mut_clone = env_mut.as_mut(); + let func = Arc::clone(&func); + let results_sig = Arc::clone(&results_sig); + let future = func + .as_ref() + .call_async(AsyncFunctionEnv::with_env(env_mut), args); + Box::pin(async move { + let typed_result = future.await?; + let mut store_mut = env_mut_clone.write().await; + typed_results_to_values::( + &mut store_mut.as_store_mut(), + results_sig.as_ref(), + typed_result, + ) + }) + }, + ) + } + #[allow(clippy::cast_ptr_alignment)] pub fn new_with_env( store: &mut impl AsStoreMut, @@ -262,6 +478,55 @@ impl Function { } } + #[cfg(feature = "experimental-async")] + #[allow(clippy::type_complexity)] + pub(crate) fn call_async( + &self, + store: &impl AsStoreAsync, + params: Vec, + ) -> Pin, RuntimeError>> + 'static>> { + let function = self.clone(); + let store = store.store(); + Box::pin(async move { + let _active_store = jspi::install_store(store.store()); + let function_type = function.handle.ty.clone(); + let write_lock = store.write_lock().await; + let arguments = Array::new_with_length(params.len() as u32); + for (index, param) in params.iter().enumerate() { + arguments.set(index as u32, param.as_jsvalue(&write_lock)); + } + + let store_context = StoreContext::install_async(write_lock.inner); + let promising = jspi::promising(&function.handle.function) + .map_err(RuntimeError::from)?; + let promise = Reflect::apply(&promising, &JsValue::NULL, &arguments) + .map_err(RuntimeError::from)? + .dyn_into::() + .map_err(RuntimeError::from)?; + drop(store_context); + + let result = JsFuture::from(promise).await.map_err(RuntimeError::from)?; + match function_type.results().len() { + 0 => Ok(Box::<[Value]>::default()), + 1 => Ok(vec![js_value_to_wasmer( + &function_type.results()[0], + &result, + )] + .into_boxed_slice()), + _ => { + let result: Array = result.into(); + Ok(function_type + .results() + .iter() + .enumerate() + .map(|(index, ty)| js_value_to_wasmer(ty, &result.get(index as u32))) + .collect::>() + .into_boxed_slice()) + } + } + }) + } + pub(crate) fn from_vm_extern(_store: &mut impl AsStoreMut, internal: VMExternFunction) -> Self { Self { handle: internal.unwrap_js(), @@ -292,6 +557,50 @@ impl Function { } } +#[cfg(feature = "experimental-async")] +fn typed_args_from_values( + store: &mut StoreMut, + function_type: &FunctionType, + values: &[Value], +) -> Result +where + Args: WasmTypeList, +{ + if values.len() != function_type.params().len() { + return Err(RuntimeError::new( + "typed host function received wrong number of parameters", + )); + } + let mut raw_array = Args::empty_array(); + for (slot, value) in raw_array.as_mut().iter_mut().zip(values) { + *slot = value.as_raw(store); + } + unsafe { Ok(Args::from_array(store, raw_array)) } +} + +#[cfg(feature = "experimental-async")] +fn typed_results_to_values( + store: &mut StoreMut, + function_type: &FunctionType, + results: Rets, +) -> Result, RuntimeError> +where + Rets: WasmTypeList, +{ + let mut raw_array = unsafe { results.into_array(store) }; + let mut values = Vec::with_capacity(function_type.results().len()); + for (raw, ty) in raw_array + .as_mut() + .iter() + .zip(function_type.results()) + { + unsafe { + values.push(Value::from_raw(store, *ty, *raw)); + } + } + Ok(values) +} + impl std::fmt::Debug for Function { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.debug_struct("Function").finish() diff --git a/lib/api/src/backend/js/entities/function/typed.rs b/lib/api/src/backend/js/entities/function/typed.rs index 229fab787dab..129702f132c3 100644 --- a/lib/api/src/backend/js/entities/function/typed.rs +++ b/lib/api/src/backend/js/entities/function/typed.rs @@ -12,7 +12,11 @@ use crate::{ TypedFunction, Value, WasmTypeList, js::utils::convert::{AsJs, js_value_to_wasmer}, }; +#[cfg(feature = "experimental-async")] +use crate::{AsStoreAsync, StoreAsync}; use js_sys::Array; +#[cfg(feature = "experimental-async")] +use std::future::Future; use std::iter::FromIterator; use wasm_bindgen::JsValue; use wasmer_types::RawValue; @@ -25,6 +29,35 @@ macro_rules! impl_native_traits { $( $x: FromToNativeWasmType, )* Rets: WasmTypeList, { + /// Call the typed func asynchronously through JSPI. + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "experimental-async")] + pub(crate) fn call_async_js( + func: crate::Function, + store: StoreAsync, + $( $x: $x, )* + ) -> impl Future> + 'static + where + $( $x: FromToNativeWasmType + 'static, )* + { + async move { + let mut write = store.write_lock().await; + let func_ty = func.ty(&mut write); + let mut params_raw = [ $( $x.to_native().into_raw(&mut write) ),* ]; + let mut params_values = Vec::with_capacity(params_raw.len()); + for (raw, ty) in params_raw.iter().zip(func_ty.params()) { + unsafe { + params_values.push(Value::from_raw(&mut write, *ty, *raw)); + } + } + drop(write); + + let results = func.call_async(&store, params_values).await?; + let mut write = store.write_lock().await; + convert_results::(&mut write, func_ty, &results) + } + } + /// Call the typed func and return results. #[allow(clippy::too_many_arguments)] pub fn call_js(&self, mut store: &mut impl AsStoreMut, $( $x: $x, )* ) -> Result where @@ -133,3 +166,27 @@ impl_native_traits!( impl_native_traits!( A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20 ); + +#[cfg(feature = "experimental-async")] +fn convert_results( + store: &mut impl AsStoreMut, + func_ty: wasmer_types::FunctionType, + results: &[Value], +) -> Result { + if results.len() != func_ty.results().len() { + return Err(RuntimeError::new("result arity mismatch")); + } + + let mut rets_list_array = Rets::empty_array(); + for ((slot, ty), value) in rets_list_array + .as_mut() + .iter_mut() + .zip(func_ty.results()) + .zip(results) + { + debug_assert_eq!(value.ty(), *ty); + *slot = value.as_raw(store); + } + + Ok(unsafe { Rets::from_array(store, rets_list_array) }) +} diff --git a/lib/api/src/backend/js/jspi.rs b/lib/api/src/backend/js/jspi.rs new file mode 100644 index 000000000000..e5fc665d3876 --- /dev/null +++ b/lib/api/src/backend/js/jspi.rs @@ -0,0 +1,81 @@ +use std::{cell::RefCell, collections::HashMap}; + +use crate::{AsStoreAsync, StoreAsync}; +use js_sys::{Function, Reflect}; +use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; +use wasmer_types::StoreId; + +struct ActiveStore { + store: StoreAsync, + users: usize, +} + +thread_local! { + static ACTIVE_STORES: RefCell> = + RefCell::new(HashMap::new()); +} + +pub(crate) struct ActiveStoreGuard { + id: StoreId, +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = WebAssembly, js_name = promising, catch)] + fn promising_raw(function: &Function) -> Result; + + #[wasm_bindgen(js_namespace = WebAssembly, js_name = Suspending)] + type Suspending; + + #[wasm_bindgen(constructor, js_namespace = WebAssembly, catch)] + fn new(function: &Function) -> Result; +} + +pub(crate) fn is_supported() -> bool { + let Ok(webassembly) = Reflect::get(&js_sys::global(), &JsValue::from_str("WebAssembly")) else { + return false; + }; + Reflect::get(&webassembly, &JsValue::from_str("promising")) + .is_ok_and(|value| value.is_function()) + && Reflect::get(&webassembly, &JsValue::from_str("Suspending")) + .is_ok_and(|value| value.is_function()) +} + +pub(crate) fn promising(function: &Function) -> Result { + promising_raw(function) +} + +pub(crate) fn suspending(function: &Function) -> Result { + Suspending::new(function).map(JsCast::unchecked_into) +} + +pub(crate) fn install_store(store: StoreAsync) -> ActiveStoreGuard { + let id = store.store_id(); + ACTIVE_STORES.with(|stores| { + let mut stores = stores.borrow_mut(); + stores + .entry(id) + .and_modify(|active| active.users += 1) + .or_insert(ActiveStore { store, users: 1 }); + }); + ActiveStoreGuard { id } +} + +pub(crate) fn active_store(id: StoreId) -> Option { + ACTIVE_STORES.with(|stores| stores.borrow().get(&id).map(|active| active.store.store())) +} + +impl Drop for ActiveStoreGuard { + fn drop(&mut self) { + ACTIVE_STORES.with(|stores| { + let mut stores = stores.borrow_mut(); + let active = stores + .get_mut(&self.id) + .expect("active JSPI store guard is unbalanced"); + active.users -= 1; + if active.users == 0 { + stores.remove(&self.id); + } + }); + } +} diff --git a/lib/api/src/backend/js/mod.rs b/lib/api/src/backend/js/mod.rs index 1bb4f6822280..09525ad6b3b7 100644 --- a/lib/api/src/backend/js/mod.rs +++ b/lib/api/src/backend/js/mod.rs @@ -2,6 +2,8 @@ pub(crate) mod entities; pub(crate) mod error; +#[cfg(feature = "experimental-async")] +pub(crate) mod jspi; pub(crate) mod utils; pub(crate) mod vm; diff --git a/lib/api/src/entities/engine/mod.rs b/lib/api/src/entities/engine/mod.rs index 8498ae55bb89..bb0f4021f196 100644 --- a/lib/api/src/entities/engine/mod.rs +++ b/lib/api/src/entities/engine/mod.rs @@ -231,7 +231,9 @@ impl Engine { pub fn supports_async(&self) -> bool { match self.be { #[cfg(feature = "sys")] - BackendEngine::Sys(ref e) => true, + BackendEngine::Sys(_) => true, + #[cfg(feature = "js")] + BackendEngine::Js(_) => crate::backend::js::jspi::is_supported(), _ => false, } } diff --git a/lib/api/src/entities/function/env/inner.rs b/lib/api/src/entities/function/env/inner.rs index 69f105449802..6f27d9b21cc3 100644 --- a/lib/api/src/entities/function/env/inner.rs +++ b/lib/api/src/entities/function/env/inner.rs @@ -155,14 +155,8 @@ impl BackendFunctionEnvMut<'_, T> { /// context is async. #[cfg(feature = "experimental-async")] pub fn as_store_async(&self) -> Option { - match self { - #[cfg(feature = "sys")] - Self::Sys(f) => f.as_store_async(), - #[cfg(feature = "sys")] - _ => unsupported_async_backend(), - #[cfg(not(feature = "sys"))] - _ => unsupported_async_backend::>(), - } + let id = self.as_store_ref().inner.objects.id(); + crate::StoreAsync::from_context(id) } } @@ -207,7 +201,10 @@ pub enum BackendAsyncFunctionEnvMut { #[cfg(feature = "sys")] /// The function environment for the `sys` runtime. Sys(crate::backend::sys::function::env::AsyncFunctionEnvMut), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + /// The function environment for the `js` runtime. + Js(crate::backend::js::function::env::AsyncFunctionEnvMut), + #[cfg(feature = "v8")] /// Placeholder for unsupported backends. Unsupported(PhantomData), } @@ -218,7 +215,10 @@ pub enum BackendAsyncFunctionEnvHandle { #[cfg(feature = "sys")] /// The function environment handle for the `sys` runtime. Sys(crate::backend::sys::function::env::AsyncFunctionEnvHandle), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + /// The function environment handle for the `js` runtime. + Js(crate::backend::js::function::env::AsyncFunctionEnvHandle), + #[cfg(feature = "v8")] /// Placeholder for unsupported backends. Unsupported(PhantomData), } @@ -229,7 +229,10 @@ pub enum BackendAsyncFunctionEnvHandleMut { #[cfg(feature = "sys")] /// The function environment handle for the `sys` runtime. Sys(crate::backend::sys::function::env::AsyncFunctionEnvHandleMut), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + /// The function environment handle for the `js` runtime. + Js(crate::backend::js::function::env::AsyncFunctionEnvHandleMut), + #[cfg(feature = "v8")] /// Placeholder for unsupported backends. Unsupported(PhantomData), } @@ -242,7 +245,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendAsyncFunctionEnvHandle::Sys(f.read().await), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendAsyncFunctionEnvHandle::Js(f.read().await), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -253,7 +258,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendAsyncFunctionEnvHandleMut::Sys(f.write().await), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendAsyncFunctionEnvHandleMut::Js(f.write().await), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -263,7 +270,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendFunctionEnv::Sys(f.as_ref()), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendFunctionEnv::Js(f.as_ref()), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -273,7 +282,9 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => Self::Sys(f.as_mut()), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => Self::Js(f.as_mut()), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -283,9 +294,11 @@ impl BackendAsyncFunctionEnvMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.as_store_async(), - #[cfg(all(feature = "sys", any(feature = "v8", feature = "js")))] + #[cfg(feature = "js")] + Self::Js(f) => f.as_store_async(), + #[cfg(all(feature = "sys", feature = "v8"))] _ => unsupported_async_backend(), - #[cfg(all(not(feature = "sys"), any(feature = "v8", feature = "js")))] + #[cfg(all(not(feature = "sys"), feature = "v8"))] _ => unsupported_async_backend::(), } } @@ -298,7 +311,9 @@ impl BackendAsyncFunctionEnvHandle { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data(), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => f.data(), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -308,9 +323,11 @@ impl BackendAsyncFunctionEnvHandle { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data_and_store(), - #[cfg(all(feature = "sys", any(feature = "v8", feature = "js")))] + #[cfg(feature = "js")] + Self::Js(f) => f.data_and_store(), + #[cfg(all(feature = "sys", feature = "v8"))] _ => unsupported_async_backend(), - #[cfg(all(not(feature = "sys"), any(feature = "v8", feature = "js")))] + #[cfg(all(not(feature = "sys"), feature = "v8"))] _ => unsupported_async_backend::<(&T, &StoreRef)>(), } } @@ -322,7 +339,9 @@ impl AsStoreRef for BackendAsyncFunctionEnvHandle { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreRef::as_store_ref(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreRef::as_store_ref(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -335,7 +354,9 @@ impl BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data_mut(), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => f.data_mut(), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -345,9 +366,11 @@ impl BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => f.data_and_store_mut(), - #[cfg(all(feature = "sys", any(feature = "v8", feature = "js")))] + #[cfg(feature = "js")] + Self::Js(f) => f.data_and_store_mut(), + #[cfg(all(feature = "sys", feature = "v8"))] _ => unsupported_async_backend(), - #[cfg(all(not(feature = "sys"), any(feature = "v8", feature = "js")))] + #[cfg(all(not(feature = "sys"), feature = "v8"))] _ => unsupported_async_backend::<(&mut T, &mut crate::StoreMut)>(), } } @@ -358,7 +381,9 @@ impl BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => BackendFunctionEnvMut::Sys(f.as_function_env_mut()), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => BackendFunctionEnvMut::Js(f.as_function_env_mut()), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -370,7 +395,9 @@ impl AsStoreRef for BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreRef::as_store_ref(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreRef::as_store_ref(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -382,7 +409,9 @@ impl AsStoreMut for BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreMut::as_store_mut(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreMut::as_store_mut(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } @@ -391,7 +420,9 @@ impl AsStoreMut for BackendAsyncFunctionEnvHandleMut { match self { #[cfg(feature = "sys")] Self::Sys(f) => AsStoreMut::objects_mut(f), - #[cfg(any(feature = "v8", feature = "js"))] + #[cfg(feature = "js")] + Self::Js(f) => AsStoreMut::objects_mut(f), + #[cfg(feature = "v8")] _ => unsupported_async_backend(), } } diff --git a/lib/api/src/entities/function/inner.rs b/lib/api/src/entities/function/inner.rs index 509241f1c583..7e72a89f58f0 100644 --- a/lib/api/src/entities/function/inner.rs +++ b/lib/api/src/entities/function/inner.rs @@ -229,7 +229,9 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_async(store, ty, func), + ), } } @@ -264,7 +266,11 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_with_env_async( + store, env, ty, func, + ), + ), } } @@ -288,7 +294,9 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_typed_async(store, func), + ), } } @@ -315,7 +323,11 @@ impl BackendFunction { #[cfg(feature = "v8")] crate::BackendStore::V8(_) => unsupported_async_backend("v8"), #[cfg(feature = "js")] - crate::BackendStore::Js(_) => unsupported_async_backend("js"), + crate::BackendStore::Js(_) => Self::Js( + crate::backend::js::entities::function::Function::new_typed_with_env_async( + store, env, func, + ), + ), } } @@ -456,7 +468,7 @@ impl BackendFunction { #[cfg(feature = "v8")] Self::V8(_) => unsupported_async_future(), #[cfg(feature = "js")] - Self::Js(_) => unsupported_async_future(), + Self::Js(f) => f.call_async(store, params), } } diff --git a/lib/api/src/utils/native/typed_func.rs b/lib/api/src/utils/native/typed_func.rs index 39f2b11d758a..aa62c42d03ba 100644 --- a/lib/api/src/utils/native/typed_func.rs +++ b/lib/api/src/utils/native/typed_func.rs @@ -103,7 +103,10 @@ macro_rules! impl_native_traits { #[cfg(feature = "v8")] BackendStore::V8(_) => async_backend_error(), #[cfg(feature = "js")] - BackendStore::Js(_) => async_backend_error(), + BackendStore::Js(_) => { + drop(read_lock); + Self::call_async_js(func, store, $([]),*).await + } } } } @@ -163,6 +166,6 @@ impl_native_traits!( fn async_backend_error() -> Result { Err(RuntimeError::new( - "async calls are only supported with the `sys` backend", + "async calls are not supported with the `v8` backend", )) } diff --git a/lib/api/tests/jspi_async.rs b/lib/api/tests/jspi_async.rs index e4b145424084..1491a4e69ffe 100644 --- a/lib/api/tests/jspi_async.rs +++ b/lib/api/tests/jspi_async.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "experimental-async")] +#![cfg(all(feature = "experimental-async", not(target_arch = "wasm32")))] use std::{cell::RefCell, sync::OnceLock}; diff --git a/lib/api/tests/jspi_async_js.rs b/lib/api/tests/jspi_async_js.rs new file mode 100644 index 000000000000..86dba7ca4ad9 --- /dev/null +++ b/lib/api/tests/jspi_async_js.rs @@ -0,0 +1,42 @@ +#![cfg(all(feature = "experimental-async", feature = "js", target_arch = "wasm32"))] + +use js_sys::Promise; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_test::wasm_bindgen_test; +use wasmer::{Function, Instance, Module, Store, TypedFunction, imports}; + +#[wasm_bindgen_test] +async fn typed_async_host_and_guest_calls_use_jspi() { + let mut store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (import "host" "increment" (func $increment (param i32) (result i32))) + (func (export "compute") (param i32) (result i32) + local.get 0 + call $increment)) + "#, + ) + .unwrap(); + let increment = Function::new_typed_async(&mut store, async move |value: i32| { + JsFuture::from(Promise::resolve(&JsValue::UNDEFINED)) + .await + .unwrap(); + value + 1 + }); + let imports = imports! { + "host" => { + "increment" => increment, + } + }; + let instance = Instance::new(&mut store, &module, &imports).unwrap(); + let compute: TypedFunction = instance + .exports + .get_typed_function(&store, "compute") + .unwrap(); + + let result = compute.call_async(&store.into_async(), 41).await.unwrap(); + assert_eq!(result, 42); +} diff --git a/lib/api/tests/module.rs b/lib/api/tests/module.rs index abaccd5f7811..b1449c7eeea6 100644 --- a/lib/api/tests/module.rs +++ b/lib/api/tests/module.rs @@ -1,4 +1,6 @@ use macro_wasmer_engine_test::engine_test; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; #[cfg(feature = "js")] use wasm_bindgen_test::*; @@ -27,7 +29,50 @@ fn module_new_async() -> Result<(), String> { #[cfg(target_arch = "wasm32")] #[wasm_bindgen_test] async fn module_new_async() -> Result<(), JsValue> { - assert_module_new_async().await.map_err(JsValue::from_str) + assert_module_new_async() + .await + .map_err(|error| JsValue::from_str(&error)) +} + +#[cfg(all(target_arch = "wasm32", feature = "js"))] +#[wasm_bindgen_test] +fn imported_table_elements_preserve_function_types() { + let mut store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (import "env" "table" (table 8 funcref)) + (import "env" "table_base" (global $table_base i32)) + (type $trampoline_type (func (param i32 i32 i32) (result i32))) + (func $trampoline (type $trampoline_type) + local.get 0) + (elem (global.get $table_base) func $trampoline)) + "#, + ) + .unwrap(); + let table = Table::new( + &mut store, + TableType::new(Type::FuncRef, 8, None), + Value::FuncRef(None), + ) + .unwrap(); + let table_base = Global::new(&mut store, Value::I32(3)); + let imports = imports! { + "env" => { + "table" => table.clone(), + "table_base" => table_base, + } + }; + + Instance::new(&mut store, &module, &imports).unwrap(); + let Value::FuncRef(Some(function)) = table.get(&mut store, 3).unwrap() else { + panic!("expected the initialized table element to be a function"); + }; + assert_eq!( + function.ty(&store), + FunctionType::new(vec![Type::I32, Type::I32, Type::I32], vec![Type::I32]) + ); } #[engine_test] diff --git a/lib/api/tests/simple_greenthread.rs b/lib/api/tests/simple_greenthread.rs index 61f8e512be9b..e3a363ef5ec5 100644 --- a/lib/api/tests/simple_greenthread.rs +++ b/lib/api/tests/simple_greenthread.rs @@ -1,17 +1,67 @@ #![cfg(feature = "experimental-async")] use std::collections::BTreeMap; +use std::future::Future; use std::sync::atomic::AtomicU32; use std::sync::{Arc, RwLock}; use anyhow::Result; +use futures::channel::oneshot; +use futures::future::{AbortHandle, Abortable}; +#[cfg(not(target_arch = "wasm32"))] use futures::task::LocalSpawnExt; -use futures::{FutureExt, channel::oneshot}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::wasm_bindgen_test; use wasmer::{ - AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, - Module, RuntimeError, Store, Type, Value, imports, + AsStoreAsync, AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, + Instance, Memory, Module, RuntimeError, Store, Type, Value, imports, }; +const SWITCHING_WAT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/examples/simple-greenthread.wat" +)); +const SWITCHING_LOGS: &[&str] = &[ + "[gr1] main -> test1", + "[gr2] test1 -> test2", + "[gr1] test1 <- test2", + "[gr2] test1 -> test2", + "[gr1] test1 <- test2", + "[main] main <- test1", +]; +const REGRESSION_WAT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/examples/simple-greenthread2.wat" +)); +const REGRESSION_LOGS: &[&str] = &[ + "[main] switching to side", + "[side] switching to main", + "[main] switching to side", + "[side] switching to main", + "[main] returned", +]; + +#[derive(Clone)] +struct TestSpawner { + #[cfg(not(target_arch = "wasm32"))] + inner: futures::executor::LocalSpawner, +} + +impl TestSpawner { + fn spawn(&self, future: impl Future + 'static) { + #[cfg(not(target_arch = "wasm32"))] + self.inner.spawn_local(future).unwrap(); + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(future); + } +} + +struct SpawnedTask { + abort: AbortHandle, + done: oneshot::Receiver<()>, +} + struct GreenEnv { logs: Vec, memory: Option, @@ -19,7 +69,8 @@ struct GreenEnv { current_greenthread_id: Arc>, next_free_id: AtomicU32, entrypoint: Option, - spawner: Option, + spawner: Option, + spawned_tasks: Vec, } // Required for carrying the spawner around. Safe because we don't do threads. @@ -38,6 +89,7 @@ impl GreenEnv { next_free_id: AtomicU32::new(1), entrypoint: None, spawner: None, + spawned_tasks: Vec::new(), } } } @@ -84,15 +136,25 @@ async fn greenthread_new( .insert(new_greenthread_id, new_greenthread); let spawner = data.spawner.as_ref().expect("spawner set").clone(); - spawner - .spawn_local(async move { - receiver.await.unwrap(); - let resumer = function - .call_async(&async_store, vec![Value::I32(entrypoint_data as i32)]) - .await; - panic!("Greenthread function returned {:?}", resumer); - }) - .unwrap(); + let (abort, registration) = AbortHandle::new_pair(); + let (done_tx, done) = oneshot::channel(); + data.spawned_tasks.push(SpawnedTask { abort, done }); + spawner.spawn(async move { + let result = Abortable::new( + async move { + receiver.await.unwrap(); + function + .call_async(&async_store, vec![Value::I32(entrypoint_data as i32)]) + .await + }, + registration, + ) + .await; + if let Ok(result) = result { + panic!("Greenthread function returned {result:?}"); + } + let _ = done_tx.send(()); + }); Ok(new_greenthread_id) } @@ -137,12 +199,12 @@ async fn greenthread_switch(env: AsyncFunctionEnvMut, next_greenthread (receiver, current_id_arc, current_greenthread_id) }; - let _ = receiver.map(|_| ()).await; + let _ = receiver.await; *current_id_arc.write().unwrap() = current_greenthread_id; } -fn run_greenthread_test(wat: &[u8]) -> Result> { +async fn run_greenthread_test(wat: &[u8], spawner: TestSpawner) -> Result> { let mut store = Store::default(); let module = Module::new(&store.engine(), wat)?; @@ -203,73 +265,78 @@ fn run_greenthread_test(wat: &[u8]) -> Result> { .unwrap() .insert(0, main_greenthread); - let mut localpool = futures::executor::LocalPool::new(); - let local_spawner = localpool.spawner(); - env.as_mut(&mut store).spawner = Some(local_spawner); + env.as_mut(&mut store).spawner = Some(spawner); let store_async = store.into_async(); - localpool - .run_until(main_fn.call_async(&store_async, vec![])) - .unwrap(); + main_fn.call_async(&store_async, vec![]).await?; - // If there are no more clones of it, StoreAsync can also be - // turned back into a store. We need to drop the local pool - // first to drop the futures and their references to the store. - drop(localpool); + let spawned_tasks = { + let mut store = store_async.write_lock().await; + std::mem::take(&mut env.as_mut(&mut store).spawned_tasks) + }; + for task in &spawned_tasks { + task.abort.abort(); + } + for task in spawned_tasks { + let _ = task.done.await; + } - let store = store_async.into_store().ok().unwrap(); + let store = store_async.read_lock().await; Ok(env.as_ref(&store).logs.clone()) } +#[cfg(not(target_arch = "wasm32"))] +fn run_greenthread_test_native(wat: &[u8]) -> Result> { + let mut local_pool = futures::executor::LocalPool::new(); + let spawner = TestSpawner { + inner: local_pool.spawner(), + }; + local_pool.run_until(run_greenthread_test(wat, spawner)) +} + +fn assert_logs(logs: &[String], expected: &[&str]) { + assert_eq!(logs.len(), expected.len()); + for (index, expected) in expected.iter().enumerate() { + assert_eq!( + logs[index], *expected, + "Log entry mismatch at index {index}: {logs:?}" + ); + } +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn green_threads_switch_and_log_in_expected_order() -> Result<()> { - let logs = run_greenthread_test(include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../tests/examples/simple-greenthread.wat" - )))?; - - // Expected logs - let expected = [ - "[gr1] main -> test1", - "[gr2] test1 -> test2", - "[gr1] test1 <- test2", - "[gr2] test1 -> test2", - "[gr1] test1 <- test2", - "[main] main <- test1", - ]; - - assert_eq!(logs.len(), expected.len(),); - for (i, exp) in expected.iter().enumerate() { - assert_eq!(logs[i], *exp, "Log entry mismatch at index {i}: {:?}", logs); - } + let logs = run_greenthread_test_native(SWITCHING_WAT)?; + assert_logs(&logs, SWITCHING_LOGS); Ok(()) } +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn green_threads_switch_and_log_in_expected_order() { + let logs = run_greenthread_test(SWITCHING_WAT, TestSpawner {}) + .await + .unwrap(); + assert_logs(&logs, SWITCHING_LOGS); +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn green_threads_switch_main_crashed() -> Result<()> { - let logs = run_greenthread_test(include_bytes!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../tests/examples/simple-greenthread2.wat" - )))?; - - // Expected logs - let expected = [ - "[main] switching to side", - "[side] switching to main", - "[main] switching to side", - "[side] switching to main", - "[main] returned", - ]; - - eprintln!("logs: {:?}", logs); - assert_eq!(logs.len(), expected.len()); - for (i, exp) in expected.iter().enumerate() { - assert_eq!(logs[i], *exp, "Log entry mismatch at index {i}: {:?}", logs); - } + let logs = run_greenthread_test_native(REGRESSION_WAT)?; + assert_logs(&logs, REGRESSION_LOGS); Ok(()) } + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn green_threads_switch_main_crashed() { + let logs = run_greenthread_test(REGRESSION_WAT, TestSpawner {}) + .await + .unwrap(); + assert_logs(&logs, REGRESSION_LOGS); +} From 03ca02f074b00aa4413d4bbe9c98be3abef920a2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 14:16:45 -0700 Subject: [PATCH 11/32] Make WASIX work well with JSPI changes for JS --- lib/api/tests/simple_greenthread.rs | 3 +- lib/wasix/src/bin_factory/exec.rs | 42 ++++++--- lib/wasix/src/runtime/task_manager/mod.rs | 80 +++++++++++++++- lib/wasix/src/state/context_switching.rs | 99 ++++++++++++++------ lib/wasix/src/utils/thread_local_executor.rs | 29 ++++-- 5 files changed, 195 insertions(+), 58 deletions(-) diff --git a/lib/api/tests/simple_greenthread.rs b/lib/api/tests/simple_greenthread.rs index e3a363ef5ec5..e691c2047d64 100644 --- a/lib/api/tests/simple_greenthread.rs +++ b/lib/api/tests/simple_greenthread.rs @@ -206,7 +206,8 @@ async fn greenthread_switch(env: AsyncFunctionEnvMut, next_greenthread async fn run_greenthread_test(wat: &[u8], spawner: TestSpawner) -> Result> { let mut store = Store::default(); - let module = Module::new(&store.engine(), wat)?; + let wasm = wat::parse_bytes(wat)?; + let module = Module::new(&store.engine(), wasm)?; let env = FunctionEnv::new(&mut store, GreenEnv::new()); diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 76b938aeceaf..0a4d69f03231 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -17,7 +17,7 @@ use crate::{ syscalls::rewind_ext, }; use crate::{Runtime, WasiEnv, WasiFunctionEnv}; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, future::Future, sync::Arc}; use tracing::*; use virtual_mio::block_on; use wasmer::{Function, Memory32, Memory64, Module, RuntimeError, Store, Value}; @@ -139,17 +139,15 @@ pub fn spawn_exec_module( // Create a thread that will run this process let tasks_outer = tasks.clone(); + let task = TaskWasm::new_async(run_exec, env, module, true, true); + tasks_outer - .task_wasm( - TaskWasm::new(Box::new(run_exec), env, module, true, true).with_pre_run(Box::new( - |ctx, store| { - let wasi_state = ctx.data(store).state.clone(); - Box::pin(async move { - wasi_state.fs.close_cloexec_fds().await; - }) - }, - )), - ) + .task_wasm(task.with_pre_run(Box::new(|ctx, store| { + let wasi_state = ctx.data(store).state.clone(); + Box::pin(async move { + wasi_state.fs.close_cloexec_fds().await; + }) + }))) .map_err(|err| { error!("wasi[{}]::failed to launch module - {}", pid, err); SpawnError::Other(Box::new(err)) @@ -180,7 +178,7 @@ unsafe fn run_recycle( } } -pub fn run_exec(props: TaskWasmRunProperties) { +pub async fn run_exec(props: TaskWasmRunProperties) { let ctx = props.ctx; let mut store = props.store; @@ -232,7 +230,7 @@ pub fn run_exec(props: TaskWasmRunProperties) { // TODO: rewrite to use crate::run_wasi_func // Call the module - call_module(ctx, store, thread, rewind_state, recycle); + call_module(ctx, store, thread, rewind_state, recycle).await; } fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option { @@ -247,7 +245,7 @@ fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option { } /// Calls the module -fn call_module( +async fn call_module( ctx: WasiFunctionEnv, mut store: Store, handle: WasiThreadRunGuard, @@ -302,6 +300,11 @@ fn call_module( return; }; + #[cfg(feature = "js")] + let (mut store, mut call_ret) = + ContextSwitchingEnvironment::run_main_context_async(&ctx, store, start.clone(), vec![]) + .await; + #[cfg(not(feature = "js"))] let (mut store, mut call_ret) = ContextSwitchingEnvironment::run_main_context(&ctx, store, start.clone(), vec![]); @@ -339,13 +342,14 @@ fn call_module( let respawn = { move |ctx, store, rewind_result| { // Call the thread - call_module( + let future = call_module( ctx, store, handle, Some((rewind, RewindResultType::RewindWithResult(rewind_result))), recycle, ); + drive_call_module(future); } }; @@ -531,3 +535,11 @@ fn resume_vfork( (store, Ok(None)) } } + +fn drive_call_module(future: impl Future + 'static) { + #[cfg(feature = "js")] + wasm_bindgen_futures::spawn_local(future); + + #[cfg(not(feature = "js"))] + futures::executor::block_on(future); +} diff --git a/lib/wasix/src/runtime/task_manager/mod.rs b/lib/wasix/src/runtime/task_manager/mod.rs index ebd83c386c57..7f02d1185b42 100644 --- a/lib/wasix/src/runtime/task_manager/mod.rs +++ b/lib/wasix/src/runtime/task_manager/mod.rs @@ -67,8 +67,42 @@ pub type TaskWasmPreRun = dyn (for<'a> FnOnce( ) -> Pin + Send + 'a>>) + Send; -/// Callback that will be invoked -pub type TaskWasmRun = dyn FnOnce(TaskWasmRunProperties) + Send + 'static; +/// Synchronous callback that will be invoked on the worker owning the instance. +pub type TaskWasmRunSync = dyn FnOnce(TaskWasmRunProperties) + Send + 'static; + +#[cfg(not(feature = "js"))] +pub type TaskWasmRun = TaskWasmRunSync; + +/// Callback used when JavaScript must await a JSPI-promising WebAssembly call. +#[cfg(feature = "js")] +pub type TaskWasmRunAsync = dyn FnOnce(TaskWasmRunProperties) -> Pin + 'static>> + + Send + + 'static; + +#[cfg(feature = "js")] +/// A WebAssembly task callback which may either complete synchronously or +/// yield through JSPI. +pub enum TaskWasmRun { + Sync(Box), + Async(Box), +} + +#[cfg(feature = "js")] +impl TaskWasmRun { + pub async fn run(self, properties: TaskWasmRunProperties) { + match self { + Self::Sync(run) => run(properties), + Self::Async(run) => run(properties).await, + } + } +} + +#[cfg(not(feature = "js"))] +/// The callback representation consumed by the native task manager. +pub type TaskWasmRunCallback = Box; +#[cfg(feature = "js")] +/// The callback representation consumed by the JavaScript task manager. +pub type TaskWasmRunCallback = TaskWasmRun; /// Callback that will be invoked pub type TaskExecModule = dyn FnOnce(Module) + Send + 'static; @@ -85,7 +119,7 @@ pub struct TaskWasmRecycleProperties { pub type TaskWasmRecycle = dyn FnOnce(TaskWasmRecycleProperties) + Send + 'static; pub struct TaskWasmCallbacks { - pub run: Box, + pub run: TaskWasmRunCallback, pub recycle: Option>, pub pre_run: Option>, pub trigger: Option>, @@ -104,7 +138,45 @@ pub struct TaskWasm { impl TaskWasm { pub fn new( - run: Box, + run: Box, + env: WasiEnv, + module: Module, + update_layout: bool, + call_initialize: bool, + ) -> Self { + #[cfg(not(feature = "js"))] + let run = run; + #[cfg(feature = "js")] + let run = TaskWasmRun::Sync(run); + + Self::from_run(run, env, module, update_layout, call_initialize) + } + + /// Creates a task whose callback may yield. + /// + /// Native workers drive the callback to completion on their worker thread. + /// JavaScript workers await it so JSPI can yield to the promise queue. + pub fn new_async( + run: F, + env: WasiEnv, + module: Module, + update_layout: bool, + call_initialize: bool, + ) -> Self + where + F: FnOnce(TaskWasmRunProperties) -> Fut + Send + 'static, + Fut: Future + 'static, + { + #[cfg(not(feature = "js"))] + let run = Box::new(move |properties| futures::executor::block_on(run(properties))); + #[cfg(feature = "js")] + let run = TaskWasmRun::Async(Box::new(move |properties| Box::pin(run(properties)))); + + Self::from_run(run, env, module, update_layout, call_initialize) + } + + fn from_run( + run: TaskWasmRunCallback, env: WasiEnv, module: Module, update_layout: bool, diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index c913e4c1e8d9..a66c32881f4b 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -10,7 +10,9 @@ use futures::{ }; use std::{ collections::BTreeMap, + future::Future, mem::forget, + pin::Pin, sync::{ Arc, RwLock, Weak, atomic::{AtomicU64, Ordering}, @@ -18,7 +20,7 @@ use std::{ }; use thiserror::Error; use tracing::trace; -use wasmer::{RuntimeError, Store}; +use wasmer::{RuntimeError, Store, StoreAsync}; use wasmer_wasix_types::wasi::ExitCode; /// The context-switching environment represents all state for WASIX context-switching @@ -53,6 +55,16 @@ pub enum ContextSwitchError { const MAIN_CONTEXT_ID: u64 = 0; +type ContextCallResult = Result, RuntimeError>; + +enum PreparedMainContext { + Complete(Store, ContextCallResult), + Async { + store: StoreAsync, + call: Pin>>, + }, +} + /// Contexts will trap with this error as a RuntimeError::user when they are canceled /// /// If encountered in a host function it MUST be propagated to the context's entrypoint. @@ -108,10 +120,51 @@ impl ContextSwitchingEnvironment { /// This call blocks until the entrypoint returns or traps pub(crate) fn run_main_context( ctx: &WasiFunctionEnv, - mut store: Store, + store: Store, + entrypoint: wasmer::Function, + params: Vec, + ) -> (Store, Result, RuntimeError>) { + let mut local_executor = ThreadLocalExecutor::new(); + match Self::prepare_main_context(ctx, store, entrypoint, params, local_executor.spawner()) { + PreparedMainContext::Complete(store, result) => (store, result), + PreparedMainContext::Async { store, call } => { + let result = local_executor.run_until(call); + drop(local_executor); + Self::finish_main_context(ctx, store, result) + } + } + } + + /// Run the main context while yielding to the JavaScript promise queue. + /// + /// JSPI requires the outer WebAssembly export to be awaited by JavaScript; + /// a blocking Rust executor cannot make progress while that promise is + /// suspended. + #[cfg(feature = "js")] + pub(crate) async fn run_main_context_async( + ctx: &WasiFunctionEnv, + store: Store, entrypoint: wasmer::Function, params: Vec, ) -> (Store, Result, RuntimeError>) { + let local_executor = ThreadLocalExecutor::new(); + match Self::prepare_main_context(ctx, store, entrypoint, params, local_executor.spawner()) { + PreparedMainContext::Complete(store, result) => (store, result), + PreparedMainContext::Async { store, call } => { + let result = call.await; + drop(local_executor); + Self::finish_main_context(ctx, store, result) + } + } + } + + fn prepare_main_context( + ctx: &WasiFunctionEnv, + mut store: Store, + entrypoint: wasmer::Function, + params: Vec, + spawner: ThreadLocalSpawner, + ) -> PreparedMainContext { if !ctx .data(&store) .capabilities @@ -119,10 +172,9 @@ impl ContextSwitchingEnvironment { .enable_asynchronous_threading { let result = entrypoint.call(&mut store, ¶ms); - return (store, result); + return PreparedMainContext::Complete(store, result); } - // If we are already in a context-switching environment, something went wrong if ctx .data_mut(&mut store) .context_switching_environment @@ -133,35 +185,34 @@ impl ContextSwitchingEnvironment { ); } - // Do a normal call and dont install the context switching env, if the engine does not support async - let engine_supports_async = store.engine().supports_async(); - if !engine_supports_async { + if !store.engine().supports_async() { let result = entrypoint.call(&mut store, ¶ms); - return (store, result); + return PreparedMainContext::Complete(store, result); } - // Create a new executor - let mut local_executor = ThreadLocalExecutor::new(); - - let this = Self::new(local_executor.spawner()); - - // Add the context-switching environment to the WasiEnv + let this = Self::new(spawner); let previous = ctx .data_mut(&mut store) .context_switching_environment .replace(this); - assert!(previous.is_none()); // Should never be hit because of the check at the top + assert!(previous.is_none()); - // Turn the store into an async store and run the entrypoint let store_async = store.into_async(); - let result = local_executor.run_until(entrypoint.call_async(&store_async, params)); + let call = Box::pin(entrypoint.call_async(&store_async, params)); + PreparedMainContext::Async { + store: store_async, + call, + } + } - // Process if this was terminated by a context entrypoint returning + fn finish_main_context( + ctx: &WasiFunctionEnv, + store_async: StoreAsync, + result: ContextCallResult, + ) -> (Store, ContextCallResult) { let result = match &result { Err(e) => match e.downcast_ref::() { Some(ContextEntrypointReturned(id)) => { - // Context entrypoint returned, which is not allowed - // Exit with code 129 tracing::error!("The entrypoint of context {id} returned which is not allowed"); Err(RuntimeError::user( WasiError::Exit(ExitCode::from(129)).into(), @@ -173,13 +224,8 @@ impl ContextSwitchingEnvironment { }; tracing::trace!("Main context finished execution and returned {result:?}"); - // Drop the executor to ensure all references to the StoreAsync are gone and convert back to a normal store - drop(local_executor); let mut store = store_async.into_store().ok().unwrap(); - - // Remove the context-switching environment from the WasiEnv let env = ctx.data_mut(&mut store); - env.context_switching_environment .take() .or_else(|| { @@ -187,9 +233,6 @@ impl ContextSwitchingEnvironment { .as_mut() .and_then(|vfork| vfork.env.context_switching_environment.take()) .inspect(|_| { - // Grace for vforks, so they don't bring everything down with them. - // This is still an error. - // The message below is oversimplified there is more nuance to this. tracing::error!("Exiting a vforked process in any other way than calling `_exit()` is undefined behavior but the current program just did that."); }) }) diff --git a/lib/wasix/src/utils/thread_local_executor.rs b/lib/wasix/src/utils/thread_local_executor.rs index 74914f29dc17..76d3ba8aa213 100644 --- a/lib/wasix/src/utils/thread_local_executor.rs +++ b/lib/wasix/src/utils/thread_local_executor.rs @@ -1,7 +1,6 @@ -use futures::{ - executor::{LocalPool, LocalSpawner}, - task::LocalSpawnExt, -}; +use futures::executor::LocalPool; +#[cfg(not(feature = "js"))] +use futures::{executor::LocalSpawner, task::LocalSpawnExt}; use std::thread::ThreadId; use thiserror::Error; @@ -15,6 +14,7 @@ use thiserror::Error; #[derive(Clone, Debug)] pub(crate) struct ThreadLocalSpawner { /// A reference to the local executor's spawner + #[cfg(not(feature = "js"))] spawner: LocalSpawner, /// The thread this spawner is associated with /// @@ -61,12 +61,20 @@ impl ThreadLocalSpawner { } // As we now know that we are on the correct thread, we can use the spawner safely - self.spawner - .spawn_local(future) - .map_err(|e| match e.is_shutdown() { - true => ThreadLocalSpawnerError::LocalPoolShutDown, - false => ThreadLocalSpawnerError::SpawnError, - }) + #[cfg(feature = "js")] + { + wasm_bindgen_futures::spawn_local(future); + Ok(()) + } + #[cfg(not(feature = "js"))] + { + self.spawner + .spawn_local(future) + .map_err(|e| match e.is_shutdown() { + true => ThreadLocalSpawnerError::LocalPoolShutDown, + false => ThreadLocalSpawnerError::SpawnError, + }) + } } } @@ -84,6 +92,7 @@ impl ThreadLocalExecutor { pub(crate) fn spawner(&self) -> ThreadLocalSpawner { ThreadLocalSpawner { + #[cfg(not(feature = "js"))] spawner: self.pool.spawner(), // SAFETY: This will always be the thread where the spawner was created on, as the ThreadLocalExecutor is not Send thread: std::thread::current().id(), From 2d1be9fb4650b35cdc0a4d1170d304420d046e3e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 29 Jul 2026 15:01:00 -0700 Subject: [PATCH 12/32] Simplify task management (with context switching) --- lib/types/src/memory.rs | 2 +- lib/wasix/src/bin_factory/exec.rs | 52 +++--- lib/wasix/src/runtime/task_manager/mod.rs | 158 +++++++++--------- lib/wasix/src/runtime/task_manager/tokio.rs | 77 ++++++++- lib/wasix/src/state/context_switching.rs | 130 ++++---------- .../src/syscalls/wasix/context_create.rs | 20 +-- lib/wasix/src/syscalls/wasix/proc_fork.rs | 54 ++++-- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 54 +++--- lib/wasix/src/utils/mod.rs | 1 - lib/wasix/src/utils/thread_local_executor.rs | 105 ------------ 10 files changed, 279 insertions(+), 374 deletions(-) delete mode 100644 lib/wasix/src/utils/thread_local_executor.rs diff --git a/lib/types/src/memory.rs b/lib/types/src/memory.rs index 643ecd307ca8..d076aa7a098f 100644 --- a/lib/types/src/memory.rs +++ b/lib/types/src/memory.rs @@ -51,7 +51,7 @@ impl MemoryStyle { /// This allows code to be generic over 32-bit and 64-bit memories. /// # Safety /// Direct memory access is unsafe -pub unsafe trait MemorySize: Copy { +pub unsafe trait MemorySize: Copy + 'static { /// Type used to represent an offset into a memory. This is `u32` or `u64`. type Offset: Default + std::fmt::Debug diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 0a4d69f03231..b7e99b94c36b 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -10,14 +10,15 @@ use crate::{ ModuleInput, TaintReason, module_cache::HashedModuleData, task_manager::{ - TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties, + LocalTaskSpawner, TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, + TaskWasmRunProperties, WasmTaskFuture, }, }, state::context_switching::ContextSwitchingEnvironment, syscalls::rewind_ext, }; use crate::{Runtime, WasiEnv, WasiFunctionEnv}; -use std::{borrow::Cow, future::Future, sync::Arc}; +use std::{borrow::Cow, sync::Arc}; use tracing::*; use virtual_mio::block_on; use wasmer::{Function, Memory32, Memory64, Module, RuntimeError, Store, Value}; @@ -139,7 +140,7 @@ pub fn spawn_exec_module( // Create a thread that will run this process let tasks_outer = tasks.clone(); - let task = TaskWasm::new_async(run_exec, env, module, true, true); + let task = TaskWasm::new(run_exec, env, module, true, true); tasks_outer .task_wasm(task.with_pre_run(Box::new(|ctx, store| { @@ -181,6 +182,7 @@ unsafe fn run_recycle( pub async fn run_exec(props: TaskWasmRunProperties) { let ctx = props.ctx; let mut store = props.store; + let local_tasks = props.local_tasks; // Create the WasiFunctionEnv let thread = WasiThreadRunGuard::new(ctx.data(&store).thread.clone()); @@ -230,7 +232,7 @@ pub async fn run_exec(props: TaskWasmRunProperties) { // TODO: rewrite to use crate::run_wasi_func // Call the module - call_module(ctx, store, thread, rewind_state, recycle).await; + call_module(ctx, store, thread, rewind_state, recycle, local_tasks).await; } fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option { @@ -251,6 +253,7 @@ async fn call_module( handle: WasiThreadRunGuard, rewind_state: Option<(RewindState, RewindResultType)>, recycle: Option>, + local_tasks: LocalTaskSpawner, ) { let env = ctx.data(&store); let pid = env.pid(); @@ -300,17 +303,18 @@ async fn call_module( return; }; - #[cfg(feature = "js")] - let (mut store, mut call_ret) = - ContextSwitchingEnvironment::run_main_context_async(&ctx, store, start.clone(), vec![]) - .await; - #[cfg(not(feature = "js"))] - let (mut store, mut call_ret) = - ContextSwitchingEnvironment::run_main_context(&ctx, store, start.clone(), vec![]); + let (mut store, mut call_ret) = ContextSwitchingEnvironment::run_main_context( + &ctx, + store, + start.clone(), + vec![], + local_tasks.clone(), + ) + .await; let mut store = loop { // Technically, it's an error for a vfork to return from main, but anyway... - store = match resume_vfork(&ctx, store, &start, &call_ret) { + store = match resume_vfork(&ctx, store, &start, &call_ret, local_tasks.clone()).await { // A vfork was resumed, there may be another, so loop back (store, Ok(Some(ret))) => { call_ret = ret; @@ -340,16 +344,15 @@ async fn call_module( // Create the callback that will be invoked when the thread respawns after a deep sleep let rewind = deep.rewind; let respawn = { - move |ctx, store, rewind_result| { - // Call the thread - let future = call_module( + move |ctx, store, rewind_result, local_tasks| -> WasmTaskFuture { + Box::pin(call_module( ctx, store, handle, Some((rewind, RewindResultType::RewindWithResult(rewind_result))), recycle, - ); - drive_call_module(future); + local_tasks, + )) } }; @@ -411,11 +414,12 @@ async fn call_module( } #[allow(clippy::type_complexity)] -fn resume_vfork( +async fn resume_vfork( ctx: &WasiFunctionEnv, mut store: Store, start: &Function, call_ret: &Result, RuntimeError>, + local_tasks: LocalTaskSpawner, ) -> ( Store, Result, RuntimeError>>, Errno>, @@ -523,7 +527,9 @@ fn resume_vfork( store, start.clone(), vec![], - ); + local_tasks, + ) + .await; (store, Ok(Some(result))) } err => { @@ -535,11 +541,3 @@ fn resume_vfork( (store, Ok(None)) } } - -fn drive_call_module(future: impl Future + 'static) { - #[cfg(feature = "js")] - wasm_bindgen_futures::spawn_local(future); - - #[cfg(not(feature = "js"))] - futures::executor::block_on(future); -} diff --git a/lib/wasix/src/runtime/task_manager/mod.rs b/lib/wasix/src/runtime/task_manager/mod.rs index 7f02d1185b42..2c11ef0f0357 100644 --- a/lib/wasix/src/runtime/task_manager/mod.rs +++ b/lib/wasix/src/runtime/task_manager/mod.rs @@ -2,8 +2,11 @@ #[cfg(feature = "sys-thread")] pub mod tokio; +use std::fmt; use std::ops::Deref; +use std::sync::Arc; use std::task::{Context, Poll}; +use std::thread::ThreadId; use std::{pin::Pin, time::Duration}; use bytes::Bytes; @@ -41,7 +44,71 @@ pub enum SpawnMemoryTypeOrStore { StoreAndMemory(wasmer::Store, Memory), } -pub type WasmResumeTask = dyn FnOnce(WasiFunctionEnv, Store, Bytes) + Send + 'static; +/// A worker-local future which may hold non-`Send` WebAssembly state. +pub type WasmTaskFuture = Pin + 'static>>; + +type SpawnLocalTask = + dyn Fn(WasmTaskFuture) -> Result<(), LocalTaskSpawnError> + Send + Sync + 'static; + +/// Spawns worker-local futures on the executor which owns a WebAssembly task. +/// +/// Task-manager implementations provide the executor. WASIX can therefore use +/// the same async execution path on native and JavaScript targets. +#[derive(Clone)] +pub struct LocalTaskSpawner { + thread: ThreadId, + spawn: Arc, +} + +#[derive(Debug, thiserror::Error)] +pub enum LocalTaskSpawnError { + #[error( + "local tasks must be spawned on their owning thread; expected {expected:?}, found {found:?}" + )] + WrongThread { expected: ThreadId, found: ThreadId }, + #[error("the local task executor has shut down")] + ShutDown, + #[error("the local task executor rejected the task")] + Spawn, +} + +impl LocalTaskSpawner { + pub fn new(spawn: F) -> Self + where + F: Fn(WasmTaskFuture) -> Result<(), LocalTaskSpawnError> + Send + Sync + 'static, + { + Self { + thread: std::thread::current().id(), + spawn: Arc::new(spawn), + } + } + + pub fn spawn(&self, future: F) -> Result<(), LocalTaskSpawnError> + where + F: Future + 'static, + { + let found = std::thread::current().id(); + if found != self.thread { + return Err(LocalTaskSpawnError::WrongThread { + expected: self.thread, + found, + }); + } + (self.spawn)(Box::pin(future)) + } +} + +impl fmt::Debug for LocalTaskSpawner { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("LocalTaskSpawner") + .field("thread", &self.thread) + .finish_non_exhaustive() + } +} + +pub type WasmResumeTask = + dyn FnOnce(WasiFunctionEnv, Store, Bytes, LocalTaskSpawner) -> WasmTaskFuture + Send + 'static; pub type WasmResumeTrigger = dyn FnOnce() -> Pin> + Send + 'static>> + Send @@ -52,6 +119,8 @@ pub type WasmResumeTrigger = dyn FnOnce() -> Pin FnOnce( ) -> Pin + Send + 'a>>) + Send; -/// Synchronous callback that will be invoked on the worker owning the instance. -pub type TaskWasmRunSync = dyn FnOnce(TaskWasmRunProperties) + Send + 'static; - -#[cfg(not(feature = "js"))] -pub type TaskWasmRun = TaskWasmRunSync; - -/// Callback used when JavaScript must await a JSPI-promising WebAssembly call. -#[cfg(feature = "js")] -pub type TaskWasmRunAsync = dyn FnOnce(TaskWasmRunProperties) -> Pin + 'static>> - + Send - + 'static; - -#[cfg(feature = "js")] -/// A WebAssembly task callback which may either complete synchronously or -/// yield through JSPI. -pub enum TaskWasmRun { - Sync(Box), - Async(Box), -} - -#[cfg(feature = "js")] -impl TaskWasmRun { - pub async fn run(self, properties: TaskWasmRunProperties) { - match self { - Self::Sync(run) => run(properties), - Self::Async(run) => run(properties).await, - } - } -} - -#[cfg(not(feature = "js"))] -/// The callback representation consumed by the native task manager. -pub type TaskWasmRunCallback = Box; -#[cfg(feature = "js")] -/// The callback representation consumed by the JavaScript task manager. -pub type TaskWasmRunCallback = TaskWasmRun; +/// An asynchronous callback invoked on the worker owning the instance. +/// +/// The callback itself is `Send` because it may be transferred to another +/// worker. Its future is worker-local and is created only after that transfer. +pub type TaskWasmRun = dyn FnOnce(TaskWasmRunProperties) -> WasmTaskFuture + Send + 'static; /// Callback that will be invoked pub type TaskExecModule = dyn FnOnce(Module) + Send + 'static; @@ -119,7 +157,7 @@ pub struct TaskWasmRecycleProperties { pub type TaskWasmRecycle = dyn FnOnce(TaskWasmRecycleProperties) + Send + 'static; pub struct TaskWasmCallbacks { - pub run: TaskWasmRunCallback, + pub run: Box, pub recycle: Option>, pub pre_run: Option>, pub trigger: Option>, @@ -137,26 +175,7 @@ pub struct TaskWasm { } impl TaskWasm { - pub fn new( - run: Box, - env: WasiEnv, - module: Module, - update_layout: bool, - call_initialize: bool, - ) -> Self { - #[cfg(not(feature = "js"))] - let run = run; - #[cfg(feature = "js")] - let run = TaskWasmRun::Sync(run); - - Self::from_run(run, env, module, update_layout, call_initialize) - } - - /// Creates a task whose callback may yield. - /// - /// Native workers drive the callback to completion on their worker thread. - /// JavaScript workers await it so JSPI can yield to the promise queue. - pub fn new_async( + pub fn new( run: F, env: WasiEnv, module: Module, @@ -167,25 +186,10 @@ impl TaskWasm { F: FnOnce(TaskWasmRunProperties) -> Fut + Send + 'static, Fut: Future + 'static, { - #[cfg(not(feature = "js"))] - let run = Box::new(move |properties| futures::executor::block_on(run(properties))); - #[cfg(feature = "js")] - let run = TaskWasmRun::Async(Box::new(move |properties| Box::pin(run(properties)))); - - Self::from_run(run, env, module, update_layout, call_initialize) - } - - fn from_run( - run: TaskWasmRunCallback, - env: WasiEnv, - module: Module, - update_layout: bool, - call_initialize: bool, - ) -> Self { let shared_memory = module.imports().memories().next().map(|a| *a.ty()); Self { callbacks: TaskWasmCallbacks { - run, + run: Box::new(move |properties| Box::pin(run(properties))), recycle: None, pre_run: None, trigger: None, @@ -448,7 +452,7 @@ impl dyn VirtualTaskManager { let thread_inner = thread.clone(); self.task_wasm( TaskWasm::new( - Box::new(move |props| { + move |props| async move { let result = props .trigger_result .expect("If there is no result then its likely the trigger did not run"); @@ -459,8 +463,8 @@ impl dyn VirtualTaskManager { return; } }; - task(props.ctx, props.store, result) - }), + task(props.ctx, props.store, result, props.local_tasks).await + }, env.clone(), module, false, diff --git a/lib/wasix/src/runtime/task_manager/tokio.rs b/lib/wasix/src/runtime/task_manager/tokio.rs index 65903b334a9d..bf9f20a19958 100644 --- a/lib/wasix/src/runtime/task_manager/tokio.rs +++ b/lib/wasix/src/runtime/task_manager/tokio.rs @@ -1,14 +1,46 @@ use std::sync::Mutex; use std::{num::NonZeroUsize, pin::Pin, sync::Arc, time::Duration}; -use futures::{Future, future::BoxFuture}; +use futures::{ + Future, + executor::{LocalPool, LocalSpawner}, + future::BoxFuture, + task::LocalSpawnExt, +}; use tokio::runtime::{Handle, Runtime}; use virtual_mio::block_on; use crate::runtime::{SpawnType, task_manager::TaskWasmCallbacks}; use crate::{WasiFunctionEnv, os::task::thread::WasiThreadError}; -use super::{SpawnMemoryTypeOrStore, TaskWasm, TaskWasmRunProperties, VirtualTaskManager}; +use super::{ + LocalTaskSpawnError, LocalTaskSpawner, SpawnMemoryTypeOrStore, TaskWasm, TaskWasmRunProperties, + VirtualTaskManager, +}; + +struct WorkerLocalSpawner(LocalSpawner); + +// SAFETY: `LocalTaskSpawner` checks the current thread before invoking this +// wrapper, so the `LocalSpawner` is only accessed on its owning worker. +unsafe impl Send for WorkerLocalSpawner {} +// SAFETY: See the `Send` implementation above. +unsafe impl Sync for WorkerLocalSpawner {} + +impl WorkerLocalSpawner { + fn spawn(&self, future: super::WasmTaskFuture) -> Result<(), LocalTaskSpawnError> { + self.0 + .spawn_local(future) + .map_err(|error| match error.is_shutdown() { + true => LocalTaskSpawnError::ShutDown, + false => LocalTaskSpawnError::Spawn, + }) + } +} + +fn local_task_spawner(pool: &LocalPool) -> LocalTaskSpawner { + let spawner = WorkerLocalSpawner(pool.spawner()); + LocalTaskSpawner::new(move |future| spawner.spawn(future)) +} #[derive(Debug, Clone)] pub enum RuntimeOrHandle { @@ -234,13 +266,16 @@ impl VirtualTaskManager for TokioTaskManager { }) }; - // Invoke the callback - (callbacks.run)(TaskWasmRunProperties { + // Invoke the callback and any worker-local contexts it creates. + let mut local_pool = LocalPool::new(); + let local_tasks = local_task_spawner(&local_pool); + local_pool.run_until((callbacks.run)(TaskWasmRunProperties { ctx, store, + local_tasks, trigger_result: Some(result), recycle: callbacks.recycle, - }); + })); }); } else { tracing::trace!("spawning task_wasm in blocking thread"); @@ -263,13 +298,16 @@ impl VirtualTaskManager for TokioTaskManager { block_on(pre_run(&mut ctx, &mut store)); } - // Invoke the callback - (callbacks.run)(TaskWasmRunProperties { + // Invoke the callback and any worker-local contexts it creates. + let mut local_pool = LocalPool::new(); + let local_tasks = local_task_spawner(&local_pool); + local_pool.run_until((callbacks.run)(TaskWasmRunProperties { ctx, store, + local_tasks, trigger_result: None, recycle: callbacks.recycle, - }); + })); }); } @@ -329,3 +367,26 @@ impl Drop for SleepNow { } } } + +#[cfg(test)] +mod tests { + use futures::channel::oneshot; + + use super::*; + + #[test] + fn wasm_callback_drives_worker_local_tasks() { + let mut pool = LocalPool::new(); + let spawner = local_task_spawner(&pool); + let (completed, wait_for_completion) = oneshot::channel(); + + pool.run_until(async move { + spawner + .spawn(async move { + completed.send(()).unwrap(); + }) + .unwrap(); + wait_for_completion.await.unwrap(); + }); + } +} diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index a66c32881f4b..5b00eaa38d40 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -1,8 +1,6 @@ use crate::{ WasiError, WasiFunctionEnv, - utils::thread_local_executor::{ - ThreadLocalExecutor, ThreadLocalSpawner, ThreadLocalSpawnerError, - }, + runtime::task_manager::{LocalTaskSpawnError, LocalTaskSpawner}, }; use futures::{ TryFutureExt, @@ -10,9 +8,7 @@ use futures::{ }; use std::{ collections::BTreeMap, - future::Future, mem::forget, - pin::Pin, sync::{ Arc, RwLock, Weak, atomic::{AtomicU64, Ordering}, @@ -20,7 +16,7 @@ use std::{ }; use thiserror::Error; use tracing::trace; -use wasmer::{RuntimeError, Store, StoreAsync}; +use wasmer::{RuntimeError, Store}; use wasmer_wasix_types::wasi::ExitCode; /// The context-switching environment represents all state for WASIX context-switching @@ -41,9 +37,8 @@ struct ContextSwitchingEnvironmentInner { current_context_id: AtomicU64, /// The next available context ID next_available_context_id: AtomicU64, - /// This spawner can be used to spawn tasks onto the thread-local executor - /// associated with this context-switching environment - spawner: ThreadLocalSpawner, + /// Spawns contexts on the worker-local executor supplied by the task manager. + spawner: LocalTaskSpawner, } /// Errors that can occur during a context switch @@ -55,16 +50,6 @@ pub enum ContextSwitchError { const MAIN_CONTEXT_ID: u64 = 0; -type ContextCallResult = Result, RuntimeError>; - -enum PreparedMainContext { - Complete(Store, ContextCallResult), - Async { - store: StoreAsync, - call: Pin>>, - }, -} - /// Contexts will trap with this error as a RuntimeError::user when they are canceled /// /// If encountered in a host function it MUST be propagated to the context's entrypoint. @@ -104,7 +89,7 @@ impl Drop for ContextCanceled { pub struct ContextEntrypointReturned(u64); impl ContextSwitchingEnvironment { - fn new(spawner: ThreadLocalSpawner) -> Self { + fn new(spawner: LocalTaskSpawner) -> Self { Self { inner: Arc::new(ContextSwitchingEnvironmentInner { unblockers: RwLock::new(BTreeMap::new()), @@ -117,54 +102,14 @@ impl ContextSwitchingEnvironment { /// Run the main context function in a context-switching environment /// - /// This call blocks until the entrypoint returns or traps - pub(crate) fn run_main_context( - ctx: &WasiFunctionEnv, - store: Store, - entrypoint: wasmer::Function, - params: Vec, - ) -> (Store, Result, RuntimeError>) { - let mut local_executor = ThreadLocalExecutor::new(); - match Self::prepare_main_context(ctx, store, entrypoint, params, local_executor.spawner()) { - PreparedMainContext::Complete(store, result) => (store, result), - PreparedMainContext::Async { store, call } => { - let result = local_executor.run_until(call); - drop(local_executor); - Self::finish_main_context(ctx, store, result) - } - } - } - - /// Run the main context while yielding to the JavaScript promise queue. - /// - /// JSPI requires the outer WebAssembly export to be awaited by JavaScript; - /// a blocking Rust executor cannot make progress while that promise is - /// suspended. - #[cfg(feature = "js")] - pub(crate) async fn run_main_context_async( - ctx: &WasiFunctionEnv, - store: Store, - entrypoint: wasmer::Function, - params: Vec, - ) -> (Store, Result, RuntimeError>) { - let local_executor = ThreadLocalExecutor::new(); - match Self::prepare_main_context(ctx, store, entrypoint, params, local_executor.spawner()) { - PreparedMainContext::Complete(store, result) => (store, result), - PreparedMainContext::Async { store, call } => { - let result = call.await; - drop(local_executor); - Self::finish_main_context(ctx, store, result) - } - } - } - - fn prepare_main_context( + /// This call yields until the entrypoint returns or traps. + pub(crate) async fn run_main_context( ctx: &WasiFunctionEnv, mut store: Store, entrypoint: wasmer::Function, params: Vec, - spawner: ThreadLocalSpawner, - ) -> PreparedMainContext { + local_tasks: LocalTaskSpawner, + ) -> (Store, Result, RuntimeError>) { if !ctx .data(&store) .capabilities @@ -172,7 +117,7 @@ impl ContextSwitchingEnvironment { .enable_asynchronous_threading { let result = entrypoint.call(&mut store, ¶ms); - return PreparedMainContext::Complete(store, result); + return (store, result); } if ctx @@ -187,10 +132,10 @@ impl ContextSwitchingEnvironment { if !store.engine().supports_async() { let result = entrypoint.call(&mut store, ¶ms); - return PreparedMainContext::Complete(store, result); + return (store, result); } - let this = Self::new(spawner); + let this = Self::new(local_tasks); let previous = ctx .data_mut(&mut store) .context_switching_environment @@ -198,18 +143,8 @@ impl ContextSwitchingEnvironment { assert!(previous.is_none()); let store_async = store.into_async(); - let call = Box::pin(entrypoint.call_async(&store_async, params)); - PreparedMainContext::Async { - store: store_async, - call, - } - } + let result = entrypoint.call_async(&store_async, params).await; - fn finish_main_context( - ctx: &WasiFunctionEnv, - store_async: StoreAsync, - result: ContextCallResult, - ) -> (Store, ContextCallResult) { let result = match &result { Err(e) => match e.downcast_ref::() { Some(ContextEntrypointReturned(id)) => { @@ -362,7 +297,7 @@ impl ContextSwitchingEnvironment { }) } - /// Create a new context and spawn it onto the thread-local executor + /// Create a new context and spawn it onto the worker-local executor. /// /// The entrypoint function is called when the context is unblocked for the first time /// @@ -410,8 +345,8 @@ impl ContextSwitchingEnvironment { // We know what we are doing, so we can prevent the panic on drop canceled.defuse(); // Context was cancelled before it was started, so we can just let it return. - // This will resolve the original future passed to `spawn_local` with - // `Ok(())` which should make the executor drop it properly + // This resolves the spawned context future, allowing the + // worker-local executor to drop it. return; } }; @@ -461,8 +396,8 @@ impl ContextSwitchingEnvironment { // We know what we are doing, so we can prevent the panic on drop canceled.defuse(); // Context was cancelled, so we can just let it return. - // This will resolve the original future passed to `spawn_local` with - // `Ok(())` which should make the executor drop it properly + // This resolves the spawned context future, allowing the + // worker-local executor to drop it. return; } Err(error) => error, // Propagate the runtime error to main @@ -507,32 +442,23 @@ impl ContextSwitchingEnvironment { .expect("Failed to send error to main context, this should not happen"); }; - // Queue the future onto the thread-local executor - tracing::trace!("Spawning context {new_context_id} onto the thread-local executor"); - let spawn_result = self.inner.spawner.spawn_local(context_future); + // Queue the future onto the worker-local executor. + tracing::trace!("Spawning context {new_context_id} onto the worker-local executor"); + let spawn_result = self.inner.spawner.spawn(context_future); match spawn_result { Ok(()) => new_context_id, - Err(ThreadLocalSpawnerError::LocalPoolShutDown) => { - // This case could happen if the executor is being shut down while it is still polling a future (this one). - // Which shouldn't be able with a single-threaded executor, as the shutdown would have to - // be initiated from within a future running on that executor. - // I the current WASIX context switching implementation should not be able to produce this case, - // but maybe it will be possible in future implementations. If someone manages to produce this case, - // they should open an issue so we can discuss how to handle this case properly. - // If this case is reachable we could return the same error as when no context-switching environment is present, - panic!( - "Failed to spawn context {new_context_id} because the local executor has been shut down. Please open an issue and let me know how you produced this error.", - ); - } - Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { expected, found }) => { + Err(LocalTaskSpawnError::WrongThread { expected, found }) => { // This should never happen and is a bug in WASIX, so we panic here panic!( - "Failed to create context because the thread local spawner lives on {expected:?} but you are on {found:?}" + "Failed to create context because the worker-local spawner lives on {expected:?} but you are on {found:?}" ) } - Err(ThreadLocalSpawnerError::SpawnError) => { - panic!("Failed to spawn context {new_context_id}, this should not happen"); + Err(LocalTaskSpawnError::ShutDown) => { + panic!("Failed to create context because the local task executor has shut down") + } + Err(LocalTaskSpawnError::Spawn) => { + panic!("Failed to create context because the local task executor rejected the task") } } } diff --git a/lib/wasix/src/syscalls/wasix/context_create.rs b/lib/wasix/src/syscalls/wasix/context_create.rs index 896b67ed196e..b363dc229204 100644 --- a/lib/wasix/src/syscalls/wasix/context_create.rs +++ b/lib/wasix/src/syscalls/wasix/context_create.rs @@ -28,18 +28,14 @@ pub fn lookup_typechecked_entrypoint( } }; - // TODO: Remove this check and return a TypedFunction once all backends support types - #[cfg(not(feature = "js"))] - { - let entrypoint_type = entrypoint.ty(&store); - if !entrypoint_type.params().is_empty() && !entrypoint_type.results().is_empty() { - tracing::trace!( - "Entrypoint function {entrypoint_id} has invalid signature: expected () -> (), got {:?} -> {:?}", - entrypoint_type.params(), - entrypoint_type.results() - ); - return Err(Errno::Inval); - } + let entrypoint_type = entrypoint.ty(&store); + if !entrypoint_type.params().is_empty() || !entrypoint_type.results().is_empty() { + tracing::trace!( + "Entrypoint function {entrypoint_id} has invalid signature: expected () -> (), got {:?} -> {:?}", + entrypoint_type.params(), + entrypoint_type.results() + ); + return Err(Errno::Inval); } Ok(entrypoint) diff --git a/lib/wasix/src/syscalls/wasix/proc_fork.rs b/lib/wasix/src/syscalls/wasix/proc_fork.rs index 1c392c5f64aa..6760fc8f6db2 100644 --- a/lib/wasix/src/syscalls/wasix/proc_fork.rs +++ b/lib/wasix/src/syscalls/wasix/proc_fork.rs @@ -2,7 +2,7 @@ use super::*; use crate::{ WasiThreadHandle, WasiVForkAsyncify, capture_store_snapshot, os::task::OwnedTaskStatus, - runtime::task_manager::{TaskWasm, TaskWasmRunProperties}, + runtime::task_manager::{LocalTaskSpawner, TaskWasm, TaskWasmRunProperties, WasmTaskFuture}, state::context_switching::ContextSwitchingEnvironment, syscalls::*, }; @@ -193,9 +193,10 @@ pub fn proc_fork( let tasks_outer = tasks.clone(); let store_data = store_data.clone(); - let run = move |mut props: TaskWasmRunProperties| { + let run = move |props: TaskWasmRunProperties| async move { let ctx = props.ctx; let mut store = props.store; + let local_tasks = props.local_tasks; // Rewind the stack and carry on { @@ -224,12 +225,12 @@ pub fn proc_fork( } // Invoke the start function - run::(ctx, store, child_handle, None); + run::(ctx, store, child_handle, None, local_tasks).await; }; tasks_outer .task_wasm( - TaskWasm::new(Box::new(run), child_env, module, false, false) + TaskWasm::new(run, child_env, module, false, false) .with_globals(snapshot) .with_memory(spawn_type), ) @@ -263,11 +264,12 @@ pub fn proc_fork( }) } -fn run( +async fn run( ctx: WasiFunctionEnv, mut store: Store, child_handle: WasiThreadHandle, rewind_state: Option<(RewindState, RewindResultType)>, + local_tasks: LocalTaskSpawner, ) -> ExitCode { let env = ctx.data(&store); let tasks = env.tasks().clone(); @@ -300,7 +302,14 @@ fn run( .start .clone() .unwrap(); - ContextSwitchingEnvironment::run_main_context(&ctx, store, start.into(), vec![]) + ContextSwitchingEnvironment::run_main_context( + &ctx, + store, + start.into(), + vec![], + local_tasks.clone(), + ) + .await } else { trace!(%pid, %tid, "re-invoking thread_spawn"); let start = ctx @@ -312,7 +321,14 @@ fn run( .clone() .unwrap(); let params = vec![0i32.into(), 0i32.into()]; - ContextSwitchingEnvironment::run_main_context(&ctx, store, start.into(), params) + ContextSwitchingEnvironment::run_main_context( + &ctx, + store, + start.into(), + params, + local_tasks.clone(), + ) + .await }; if let Err(err) = err { match err.downcast::() { @@ -326,16 +342,20 @@ fn run( let respawn = { let tasks = tasks.clone(); let rewind_state = deep.rewind; - move |ctx, store, rewind_result| { - run::( - ctx, - store, - child_handle, - Some(( - rewind_state, - RewindResultType::RewindWithResult(rewind_result), - )), - ); + move |ctx, store, rewind_result, local_tasks| -> WasmTaskFuture { + Box::pin(async move { + run::( + ctx, + store, + child_handle, + Some(( + rewind_state, + RewindResultType::RewindWithResult(rewind_result), + )), + local_tasks, + ) + .await; + }) } }; diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index b6df120a51ce..7db699c30be2 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -6,7 +6,7 @@ use crate::{ os::task::thread::WasiMemoryLayout, runtime::{ TaintReason, - task_manager::{TaskWasm, TaskWasmRunProperties}, + task_manager::{LocalTaskSpawner, TaskWasm, TaskWasmRunProperties, WasmTaskFuture}, }, state::context_switching::ContextSwitchingEnvironment, syscalls::*, @@ -136,22 +136,23 @@ pub fn thread_spawn_internal_using_layout( thread_env.thread = thread_handle.as_thread(); thread_env.layout = layout; - // TODO: Currently asynchronous threading does not work with multi - // threading in JS but it does work for the main thread. This will - // require more work to find out why. - thread_env.enable_deep_sleep = if cfg!(feature = "js") { - false - } else { - unsafe { env.capable_of_deep_sleep() } - }; + thread_env.enable_deep_sleep = unsafe { env.capable_of_deep_sleep() }; // This next function gets a context for the local thread and then // calls into the process - let mut execute_module = { + let execute_module = { let thread_handle = thread_handle; - move |ctx: WasiFunctionEnv, mut store: Store| { + move |ctx: WasiFunctionEnv, store: Store, local_tasks: LocalTaskSpawner| async move { // Call the thread - call_module::(ctx, store, start_ptr_offset, thread_handle, rewind_state) + call_module::( + ctx, + store, + start_ptr_offset, + thread_handle, + rewind_state, + local_tasks, + ) + .await } }; @@ -180,12 +181,12 @@ pub fn thread_spawn_internal_using_layout( // Now spawn a thread trace!("threading: spawning background thread"); - let run = move |props: TaskWasmRunProperties| { - execute_module(props.ctx, props.store); + let run = move |props: TaskWasmRunProperties| async move { + execute_module(props.ctx, props.store, props.local_tasks).await; }; - let mut task_wasm = TaskWasm::new(Box::new(run), thread_env, thread_module, false, false) - .with_memory(spawn_type); + let mut task_wasm = + TaskWasm::new(run, thread_env, thread_module, false, false).with_memory(spawn_type); tasks.task_wasm(task_wasm).map_err(Into::::into)?; @@ -194,10 +195,11 @@ pub fn thread_spawn_internal_using_layout( } // This function calls into the module -fn call_module_internal( +async fn call_module_internal( ctx: &WasiFunctionEnv, mut store: Store, start_ptr_offset: M::Offset, + local_tasks: LocalTaskSpawner, ) -> (Store, Result, DeepSleepWork>) { // Note: we ensure both unwraps can happen before getting to this point let spawn = ctx @@ -220,7 +222,9 @@ fn call_module_internal( store, spawn, vec![Value::I32(tid_i32), Value::I32(start_pointer_i32)], - ); + local_tasks, + ) + .await; let thread_result = thread_result.map(|_| ()); trace!("callback finished (ret={:?})", thread_result); @@ -294,12 +298,13 @@ fn handle_thread_result( } /// Calls the module -fn call_module( +async fn call_module( mut ctx: WasiFunctionEnv, mut store: Store, start_ptr_offset: M::Offset, thread_handle: Arc, rewind_state: Option<(RewindState, RewindResultType)>, + local_tasks: LocalTaskSpawner, ) { let env = ctx.data(&store); let tasks = env.tasks().clone(); @@ -320,7 +325,8 @@ fn call_module( } // Now invoke the module - let (mut store, ret) = call_module_internal::(&ctx, store, start_ptr_offset); + let (mut store, ret) = + call_module_internal::(&ctx, store, start_ptr_offset, local_tasks).await; // If it went to deep sleep then we need to handle that if let Err(deep) = ret { @@ -328,15 +334,15 @@ fn call_module( let rewind = deep.rewind; let respawn = { let tasks = tasks.clone(); - move |ctx, store, trigger_res| { - // Call the thread - call_module::( + move |ctx, store, trigger_res, local_tasks| -> WasmTaskFuture { + Box::pin(call_module::( ctx, store, start_ptr_offset, thread_handle, Some((rewind, RewindResultType::RewindWithResult(trigger_res))), - ); + local_tasks, + )) } }; diff --git a/lib/wasix/src/utils/mod.rs b/lib/wasix/src/utils/mod.rs index b9017cc40972..9fc874d4a615 100644 --- a/lib/wasix/src/utils/mod.rs +++ b/lib/wasix/src/utils/mod.rs @@ -1,7 +1,6 @@ mod dummy_waker; mod owned_mutex_guard; pub mod store; -pub mod thread_local_executor; mod thread_parker; #[cfg(feature = "js")] diff --git a/lib/wasix/src/utils/thread_local_executor.rs b/lib/wasix/src/utils/thread_local_executor.rs deleted file mode 100644 index 76d3ba8aa213..000000000000 --- a/lib/wasix/src/utils/thread_local_executor.rs +++ /dev/null @@ -1,105 +0,0 @@ -use futures::executor::LocalPool; -#[cfg(not(feature = "js"))] -use futures::{executor::LocalSpawner, task::LocalSpawnExt}; -use std::thread::ThreadId; -use thiserror::Error; - -/// A `Send`able spawner that spawns onto a thread-local executor -/// -/// Despite being `Send`, the spawner enforces at runtime that -/// it is only used to spawn on the thread it was created on. -// -// If that limitation is a problem, we can consider implementing a version that -// accepts `Send` futures and sends them to the correct thread via channels. -#[derive(Clone, Debug)] -pub(crate) struct ThreadLocalSpawner { - /// A reference to the local executor's spawner - #[cfg(not(feature = "js"))] - spawner: LocalSpawner, - /// The thread this spawner is associated with - /// - /// Used to generate better error messages when trying to spawn on the wrong thread - thread: ThreadId, -} -// SAFETY: The ThreadLocalSpawner enforces the spawner is only used on the correct thread. -// See the safety comment in ThreadLocalSpawner::spawn_local and ThreadLocalSpawner::spawner -unsafe impl Send for ThreadLocalSpawner {} -// SAFETY: The ThreadLocalSpawner enforces the spawner is only used on the correct thread. -// See the safety comment in ThreadLocalSpawner::spawn_local and ThreadLocalSpawner::spawner -unsafe impl Sync for ThreadLocalSpawner {} - -/// Errors that can occur during `spawn_local` calls -#[derive(Debug, Error)] -pub enum ThreadLocalSpawnerError { - #[error( - "The ThreadLocalSpawner can only spawn tasks on the thread it was created on. Expected to be on {expected:?} but was actually on {found:?}" - )] - NotOnTheCorrectThread { expected: ThreadId, found: ThreadId }, - #[error( - "The local executor associated with this spawner has been shut down and cannot accept new tasks" - )] - LocalPoolShutDown, - #[error("An error occurred while spawning the task")] - SpawnError, -} - -impl ThreadLocalSpawner { - /// Spawn a future onto the same thread as the local spawner - /// - /// Needs to be called from the same thread on which the associated executor was created - pub(crate) fn spawn_local + 'static>( - &self, - future: F, - ) -> Result<(), ThreadLocalSpawnerError> { - // SAFETY: This is what makes implementing Send on ThreadLocalSpawner safe. We ensure that we only spawn - // on the same thread as the one the spawner was created on. - if std::thread::current().id() != self.thread { - return Err(ThreadLocalSpawnerError::NotOnTheCorrectThread { - expected: self.thread, - found: std::thread::current().id(), - }); - } - - // As we now know that we are on the correct thread, we can use the spawner safely - #[cfg(feature = "js")] - { - wasm_bindgen_futures::spawn_local(future); - Ok(()) - } - #[cfg(not(feature = "js"))] - { - self.spawner - .spawn_local(future) - .map_err(|e| match e.is_shutdown() { - true => ThreadLocalSpawnerError::LocalPoolShutDown, - false => ThreadLocalSpawnerError::SpawnError, - }) - } - } -} - -/// A thread-local executor that can run tasks on the current thread -pub(crate) struct ThreadLocalExecutor { - /// The local pool - pool: LocalPool, -} - -impl ThreadLocalExecutor { - pub(crate) fn new() -> Self { - let local_pool = futures::executor::LocalPool::new(); - Self { pool: local_pool } - } - - pub(crate) fn spawner(&self) -> ThreadLocalSpawner { - ThreadLocalSpawner { - #[cfg(not(feature = "js"))] - spawner: self.pool.spawner(), - // SAFETY: This will always be the thread where the spawner was created on, as the ThreadLocalExecutor is not Send - thread: std::thread::current().id(), - } - } - - pub(crate) fn run_until(&mut self, future: F) -> F::Output { - self.pool.run_until(future) - } -} From d0b89790e177b7ce62067f27618aeeb8b578ffc9 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 30 Jul 2026 15:55:43 -0700 Subject: [PATCH 13/32] feat(wasix): add pluggable package caches --- Cargo.lock | 1 + lib/wasix/Cargo.toml | 1 + .../runtime/package_loader/builtin_loader.rs | 93 ++++++--- lib/wasix/src/runtime/package_loader/mod.rs | 7 +- .../src/runtime/resolver/backend_source.rs | 189 +++++++++++------- lib/wasix/src/runtime/resolver/mod.rs | 2 +- 6 files changed, 189 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a58965036e2b..74d11a6ed5c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7964,6 +7964,7 @@ dependencies = [ "wcgi", "wcgi-host", "web-sys", + "web-time", "webc", "windows-sys 0.61.2", "xxhash-rust", diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index abb5e71b5694..ca572426251e 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -64,6 +64,7 @@ chrono = { workspace = true, default-features = false, features = [ "clock", ], optional = true } bytes.workspace = true +web-time.workspace = true anyhow.workspace = true sha2.workspace = true waker-fn.workspace = true diff --git a/lib/wasix/src/runtime/package_loader/builtin_loader.rs b/lib/wasix/src/runtime/package_loader/builtin_loader.rs index 0ea50d245c3f..60de9f30db14 100644 --- a/lib/wasix/src/runtime/package_loader/builtin_loader.rs +++ b/lib/wasix/src/runtime/package_loader/builtin_loader.rs @@ -32,7 +32,8 @@ use crate::{ pub struct BuiltinPackageLoader { client: Arc, in_memory: Option, - cache: Option, + cache: Option>, + filesystem_cache: Option>, /// A mapping from hostnames to tokens tokens: HashMap, @@ -57,6 +58,7 @@ impl BuiltinPackageLoader { in_memory: Some(InMemoryCache::default()), client: Arc::new(crate::http::default_http_client().unwrap()), cache: None, + filesystem_cache: None, hash_validation: HashIntegrityValidationMode::NoValidate, tokens: HashMap::new(), } @@ -71,10 +73,21 @@ impl BuiltinPackageLoader { } pub fn with_cache_dir(self, cache_dir: impl Into) -> Self { + let cache = Arc::new(FileSystemCache { + cache_dir: cache_dir.into(), + }); BuiltinPackageLoader { - cache: Some(FileSystemCache { - cache_dir: cache_dir.into(), - }), + cache: Some(cache.clone()), + filesystem_cache: Some(cache), + ..self + } + } + + /// Use a target-provided persistent package cache. + pub fn with_cache(self, cache: Arc) -> Self { + BuiltinPackageLoader { + cache: Some(cache), + filesystem_cache: None, ..self } } @@ -88,7 +101,7 @@ impl BuiltinPackageLoader { } pub fn cache(&self) -> Option<&FileSystemCache> { - self.cache.as_ref() + self.filesystem_cache.as_deref() } pub fn validate_cache( @@ -96,9 +109,9 @@ impl BuiltinPackageLoader { mode: CacheValidationMode, ) -> Result, anyhow::Error> { let cache = self - .cache - .as_ref() - .context("can not validate cache - no cache configured")?; + .filesystem_cache + .as_deref() + .context("can not validate cache - no filesystem cache configured")?; let items = cache.validate_hashes()?; let mut errors = Vec::new(); @@ -185,14 +198,24 @@ impl BuiltinPackageLoader { return Ok(Some(cached)); } - if let Some(cache) = self.cache.as_ref() - && let Some(cached) = cache.lookup(hash).await? - { - if let Some(in_memory) = &self.in_memory { - tracing::debug!("Copying from the filesystem cache to the in-memory cache"); - in_memory.save(&cached, *hash); + if let Some(cache) = self.cache.as_ref() { + match cache.lookup(hash).await { + Ok(Some(cached)) => { + if let Some(in_memory) = &self.in_memory { + tracing::debug!("Copying from the persistent cache to the in-memory cache"); + in_memory.save(&cached, *hash); + } + return Ok(Some(cached)); + } + Ok(None) => {} + Err(error) => { + tracing::warn!( + error = &*error, + pkg.hash = %hash, + "Unable to read a cached package; treating it as a cache miss", + ); + } } - return Ok(Some(cached)); } Ok(None) @@ -438,10 +461,7 @@ impl PackageLoader for BuiltinPackageLoader { // in a smart way to keep memory usage down. if let Some(cache) = &self.cache { - match cache - .save_and_load_as_mmapped(bytes.clone(), &summary.dist) - .await - { + match cache.save(bytes.clone(), &summary.dist).await { Ok(container) => { tracing::debug!("Cached to disk"); if let Some(in_memory) = &self.in_memory { @@ -519,6 +539,16 @@ pub struct FileSystemCache { cache_dir: PathBuf, } +/// Persistent storage for immutable WEBC package contents. +#[async_trait::async_trait] +pub trait PackageCache: Send + Sync + std::fmt::Debug { + /// Load and decode a package by its expected content hash. + async fn lookup(&self, hash: &WebcHash) -> Result, Error>; + + /// Persist downloaded bytes and return a decoded container. + async fn save(&self, webc: Bytes, dist: &DistributionInfo) -> Result; +} + impl FileSystemCache { const FILE_SUFFIX: &'static str = ".bin"; @@ -584,7 +614,7 @@ impl FileSystemCache { Ok(items) } - async fn lookup(&self, hash: &WebcHash) -> Result, Error> { + async fn lookup_impl(&self, hash: &WebcHash) -> Result, Error> { let path = self.path(hash); let container = crate::spawn_blocking({ @@ -606,7 +636,7 @@ impl FileSystemCache { } } - async fn save(&self, webc: Bytes, dist: &DistributionInfo) -> Result { + async fn save_impl(&self, webc: Bytes, dist: &DistributionInfo) -> Result { let path = self.path(&dist.webc_sha256); let dist = dist.clone(); let temp_dir = self.temp_dir(); @@ -648,11 +678,11 @@ impl FileSystemCache { dist: &DistributionInfo, ) -> Result { // First, save it to disk - self.save(webc, dist).await?; + self.save_impl(webc, dist).await?; // Now try to load it again. The resulting container should use // a memory-mapped file rather than an in-memory buffer. - match self.lookup(&dist.webc_sha256).await? { + match self.lookup_impl(&dist.webc_sha256).await? { Some(container) => Ok(container), None => { // Something really weird has occurred and we can't see the @@ -763,6 +793,17 @@ impl FileSystemCache { } } +#[async_trait::async_trait] +impl PackageCache for FileSystemCache { + async fn lookup(&self, hash: &WebcHash) -> Result, Error> { + self.lookup_impl(hash).await + } + + async fn save(&self, webc: Bytes, dist: &DistributionInfo) -> Result { + self.save_and_load_as_mmapped(webc, dist).await + } +} + #[derive(Debug, Default)] struct InMemoryCache(RwLock>); @@ -875,7 +916,7 @@ mod tests { assert_eq!(manifest.entrypoint.as_deref(), Some("python")); // it should have been automatically saved to disk let path = loader - .cache + .filesystem_cache .as_ref() .unwrap() .path(&summary.dist.webc_sha256); @@ -1155,7 +1196,7 @@ mod tests { } let path1 = cache - .save( + .save_impl( Bytes::from_static(b"test1"), &DistributionInfo { webc: Url::parse("file:///test1.webc").unwrap(), @@ -1165,7 +1206,7 @@ mod tests { .await .unwrap(); let path2 = cache - .save( + .save_impl( Bytes::from_static(b"test2"), &DistributionInfo { webc: Url::parse("file:///test2.webc").unwrap(), diff --git a/lib/wasix/src/runtime/package_loader/mod.rs b/lib/wasix/src/runtime/package_loader/mod.rs index 64fdeb4d410e..48f1fd00ea5a 100644 --- a/lib/wasix/src/runtime/package_loader/mod.rs +++ b/lib/wasix/src/runtime/package_loader/mod.rs @@ -4,6 +4,9 @@ mod types; mod unsupported; pub use self::{ - builtin_loader::BuiltinPackageLoader, load_package_tree::load_package_tree, - types::PackageLoader, types::to_module_hash, unsupported::UnsupportedPackageLoader, + builtin_loader::{BuiltinPackageLoader, PackageCache}, + load_package_tree::load_package_tree, + types::PackageLoader, + types::to_module_hash, + unsupported::UnsupportedPackageLoader, }; diff --git a/lib/wasix/src/runtime/resolver/backend_source.rs b/lib/wasix/src/runtime/resolver/backend_source.rs index f0d18d6fe4ca..ca2b6e48f886 100644 --- a/lib/wasix/src/runtime/resolver/backend_source.rs +++ b/lib/wasix/src/runtime/resolver/backend_source.rs @@ -1,14 +1,12 @@ -use std::{ - path::{MAIN_SEPARATOR_STR, PathBuf}, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{io::Write, path::PathBuf, sync::Arc, time::Duration}; use anyhow::{Context, Error}; +use bytes::Bytes; use http::{HeaderMap, Method}; use semver::{Version, VersionReq}; use url::Url; use wasmer_config::package::{NamedPackageId, PackageHash, PackageId, PackageIdent, PackageSource}; +use web_time::SystemTime; use webc::metadata::Manifest; use crate::{ @@ -24,7 +22,7 @@ use crate::{ pub struct BackendSource { registry_endpoint: Url, client: Arc, - cache: Option, + cache: Option, token: Option, preferred_webc_version: webc::Version, } @@ -45,8 +43,13 @@ impl BackendSource { /// Cache query results locally. pub fn with_local_cache(self, cache_dir: impl Into, timeout: Duration) -> Self { + self.with_query_cache(Arc::new(FileSystemQueryCache::new(cache_dir)), timeout) + } + + /// Cache registry query results in target-provided persistent storage. + pub fn with_query_cache(self, cache: Arc, timeout: Duration) -> Self { BackendSource { - cache: Some(FileSystemCache::new(cache_dir, timeout)), + cache: Some(QueryCacheConfig { cache, timeout }), ..self } } @@ -262,7 +265,7 @@ impl Source for BackendSource { }; if let Some(cache) = &self.cache { - match cache.lookup_cached_query(&package_name) { + match lookup_cached_query(cache, &package_name).await { Ok(Some(cached)) => { if let Ok(cached) = matching_package_summaries( package, @@ -291,7 +294,7 @@ impl Source for BackendSource { .map_err(|error| QueryError::new_other(error, package))?; if let Some(cache) = &self.cache - && let Err(e) = cache.update(&package_name, &response) + && let Err(e) = update_cached_query(cache, &package_name, &response).await { tracing::warn!( package_name, @@ -444,100 +447,76 @@ fn decode_summary( }) } -/// A local cache for package queries. +/// Persistent storage for serialized registry query responses. +#[async_trait::async_trait] +pub trait QueryCache: Send + Sync + std::fmt::Debug { + /// Load the serialized cache entry for a package name. + async fn load(&self, package_name: &str) -> Result, Error>; + + /// Persist a serialized cache entry for a package name. + async fn save(&self, package_name: &str, bytes: Bytes) -> Result<(), Error>; + + /// Remove the cache entry for a package name, if present. + async fn remove(&self, package_name: &str) -> Result<(), Error>; +} + #[derive(Debug, Clone)] -struct FileSystemCache { - cache_dir: PathBuf, +struct QueryCacheConfig { + cache: Arc, timeout: Duration, } -impl FileSystemCache { - fn new(cache_dir: impl Into, timeout: Duration) -> Self { - FileSystemCache { +/// A local filesystem cache for registry queries. +#[derive(Debug, Clone)] +struct FileSystemQueryCache { + cache_dir: PathBuf, +} + +impl FileSystemQueryCache { + fn new(cache_dir: impl Into) -> Self { + Self { cache_dir: cache_dir.into(), - timeout, } } fn path(&self, package_name: &str) -> PathBuf { self.cache_dir - .join(package_name.replace(MAIN_SEPARATOR_STR, "#")) + .join(package_name.replace('/', "#").replace('\\', "#")) } +} - fn lookup_cached_query(&self, package_name: &str) -> Result, Error> { +#[async_trait::async_trait] +impl QueryCache for FileSystemQueryCache { + async fn load(&self, package_name: &str) -> Result, Error> { let filename = self.path(package_name); let _span = tracing::debug_span!("lookup_cached_query", filename=%filename.display()).entered(); tracing::trace!("Reading cached entry from disk"); - let json = match std::fs::read(&filename) { - Ok(json) => json, + match std::fs::read(&filename) { + Ok(json) => Ok(Some(Bytes::from(json))), Err(e) if e.kind() == std::io::ErrorKind::NotFound => { tracing::debug!("Cache miss"); - return Ok(None); + Ok(None) } Err(e) => { - return Err( - Error::new(e).context(format!("Unable to read \"{}\"", filename.display())) - ); - } - }; - - let entry: CacheEntry = match serde_json::from_slice(&json) { - Ok(entry) => entry, - Err(e) => { - // If the entry is invalid, we should delete it to avoid work - // in the future - let _ = std::fs::remove_file(&filename); - - return Err(Error::new(e).context("Unable to parse the cached query")); + Err(Error::new(e).context(format!("Unable to read \"{}\"", filename.display()))) } - }; - - if !entry.is_still_valid(self.timeout) { - tracing::debug!(timestamp = entry.unix_timestamp, "Cached entry is stale"); - let _ = std::fs::remove_file(&filename); - return Ok(None); } - - if entry.package_name != package_name { - let _ = std::fs::remove_file(&filename); - anyhow::bail!( - "The cached response at \"{}\" corresponds to the \"{}\" package, but expected \"{}\"", - filename.display(), - entry.package_name, - package_name, - ); - } - - Ok(Some(entry.response)) } - fn update(&self, package_name: &str, response: &WebQuery) -> Result<(), Error> { - let entry = CacheEntry { - unix_timestamp: SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs(), - package_name: package_name.to_string(), - response: response.clone(), - }; - + async fn save(&self, package_name: &str, bytes: Bytes) -> Result<(), Error> { let _ = std::fs::create_dir_all(&self.cache_dir); - // First, save our cache entry to disk let mut temp = tempfile::NamedTempFile::new_in(&self.cache_dir) .context("Unable to create a temporary file")?; - serde_json::to_writer_pretty(&mut temp, &entry) - .context("Unable to serialize the cache entry")?; + temp.write_all(&bytes) + .context("Unable to write the cached query")?; temp.as_file() .sync_all() .context("Flushing the temp file failed")?; - // Now we've saved our cache entry we need to move it to the right - // location. We do this in two steps so concurrent queries don't see - // the cache entry until it has been completely written. let filename = self.path(package_name); tracing::debug!( filename=%filename.display(), @@ -557,6 +536,65 @@ impl FileSystemCache { Ok(()) } + + async fn remove(&self, package_name: &str) -> Result<(), Error> { + match std::fs::remove_file(self.path(package_name)) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error.into()), + } + } +} + +async fn lookup_cached_query( + cache: &QueryCacheConfig, + package_name: &str, +) -> Result, Error> { + let Some(json) = cache.cache.load(package_name).await? else { + return Ok(None); + }; + + let entry: CacheEntry = match serde_json::from_slice(&json) { + Ok(entry) => entry, + Err(error) => { + let _ = cache.cache.remove(package_name).await; + return Err(Error::new(error).context("Unable to parse the cached query")); + } + }; + + if !entry.is_still_valid(cache.timeout) { + tracing::debug!(timestamp = entry.unix_timestamp, "Cached entry is stale"); + let _ = cache.cache.remove(package_name).await; + return Ok(None); + } + + if entry.package_name != package_name { + let _ = cache.cache.remove(package_name).await; + anyhow::bail!( + "The cached response corresponds to the \"{}\" package, but expected \"{}\"", + entry.package_name, + package_name, + ); + } + + Ok(Some(entry.response)) +} + +async fn update_cached_query( + cache: &QueryCacheConfig, + package_name: &str, + response: &WebQuery, +) -> Result<(), Error> { + let entry = CacheEntry { + unix_timestamp: SystemTime::UNIX_EPOCH + .elapsed() + .unwrap_or_default() + .as_secs(), + package_name: package_name.to_string(), + response: response.clone(), + }; + let bytes = serde_json::to_vec_pretty(&entry).context("Unable to serialize cached query")?; + cache.cache.save(package_name, Bytes::from(bytes)).await } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -1211,12 +1249,13 @@ mod tests { let temp = tempfile::tempdir().unwrap(); let source = BackendSource::new(registry_endpoint, client.clone()) .with_local_cache(temp.path(), Duration::from_secs(0)); - source - .cache - .as_ref() - .unwrap() - .update("wasmer/python", &cached_value) - .unwrap(); + update_cached_query( + source.cache.as_ref().unwrap(), + "wasmer/python", + &cached_value, + ) + .await + .unwrap(); let summaries = source.query(&request).await.unwrap(); diff --git a/lib/wasix/src/runtime/resolver/mod.rs b/lib/wasix/src/runtime/resolver/mod.rs index 9cc31400bcb3..99bcba9087db 100644 --- a/lib/wasix/src/runtime/resolver/mod.rs +++ b/lib/wasix/src/runtime/resolver/mod.rs @@ -11,7 +11,7 @@ pub(crate) mod utils; mod web_source; pub use self::{ - backend_source::BackendSource, + backend_source::{BackendSource, QueryCache}, filesystem_source::FileSystemSource, in_memory_source::InMemorySource, inputs::{ From 24bfbaa0260524b50a1d007ae0c95a929f2df5c5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Jul 2026 14:28:01 -0700 Subject: [PATCH 14/32] Use only context switching or asyncify when needed --- lib/wasix/src/state/context_switching.rs | 5 ++++- lib/wasix/src/state/env.rs | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/wasix/src/state/context_switching.rs b/lib/wasix/src/state/context_switching.rs index 5b00eaa38d40..cfbad70c7d00 100644 --- a/lib/wasix/src/state/context_switching.rs +++ b/lib/wasix/src/state/context_switching.rs @@ -130,7 +130,10 @@ impl ContextSwitchingEnvironment { ); } - if !store.engine().supports_async() { + // JSPI and Asyncify are alternative suspension mechanisms. Only use + // the asynchronous entrypoint when the backend supports it and the + // guest is not already instrumented to unwind its own stack. + if ctx.data(&store).will_use_asyncify() || !store.engine().supports_async() { let result = entrypoint.call(&mut store, ¶ms); return (store, result); } diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index be2e04b13ee6..ef0fe13c5b8b 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -298,10 +298,11 @@ impl WasiEnv { /// Returns true if this WASM process will need and try to use /// asyncify while its running which normally means. pub fn will_use_asyncify(&self) -> bool { - self.inner() - .static_module_instance_handles() - .map(|handles| self.enable_deep_sleep || handles.has_stack_checkpoint) - .unwrap_or(false) + let inner = self.inner(); + let handles = inner.main_module_instance_handles(); + self.enable_deep_sleep + || handles.has_stack_checkpoint + || handles.asyncify_start_unwind.is_some() } /// Re-initializes this environment so that it can be executed again From 31a0fba09e61003cb8b09721d4a1dd71f8da9bf1 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Jul 2026 16:24:00 -0700 Subject: [PATCH 15/32] feat(wasix): scope terminals to process trees --- lib/wasix/src/state/builder.rs | 15 ++++++++++++++- lib/wasix/src/state/env.rs | 23 +++++++++++++++++++---- lib/wasix/src/syscalls/wasix/tty_get.rs | 4 +--- lib/wasix/src/syscalls/wasix/tty_set.rs | 2 +- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/wasix/src/state/builder.rs b/lib/wasix/src/state/builder.rs index c75cbfb12d48..5c3c52384b5b 100644 --- a/lib/wasix/src/state/builder.rs +++ b/lib/wasix/src/state/builder.rs @@ -21,8 +21,8 @@ use crate::{ bin_factory::{BinFactory, BinaryPackage}, capabilities::Capabilities, fs::{WasiFs, WasiFsRoot, WasiInodes}, - os::command::VirtualCommand, os::task::control_plane::{ControlPlaneConfig, ControlPlaneError, WasiControlPlane}, + os::{TtyBridge, command::VirtualCommand}, state::WasiState, syscalls::types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO}, }; @@ -70,6 +70,7 @@ pub struct WasiEnvBuilder { pub(super) fs: Option, pub(super) engine: Option, pub(super) runtime: Option>, + pub(super) tty: Option>, pub(super) current_dir: Option, /// List of webc dependencies to be injected. @@ -822,6 +823,17 @@ impl WasiEnvBuilder { self.runtime = Some(runtime); } + /// Override the terminal bridge for this process tree. + pub fn tty(mut self, tty: Arc) -> Self { + self.tty = Some(tty); + self + } + + /// Override the terminal bridge for this process tree. + pub fn set_tty(&mut self, tty: Arc) { + self.tty = Some(tty); + } + pub fn capabilities(mut self, capabilities: Capabilities) -> Self { self.set_capabilities(capabilities); self @@ -1066,6 +1078,7 @@ impl WasiEnvBuilder { let init = WasiEnvInit { state, runtime, + tty: self.tty, webc_dependencies: uses, mapped_commands: map_commands, control_plane, diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index ef0fe13c5b8b..08b4a6bda3a3 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -7,10 +7,13 @@ use crate::{ capabilities::Capabilities, fs::{WasiFsRoot, WasiInodes}, import_object_for_all_wasi_versions, - os::task::{ - control_plane::ControlPlaneError, - process::{WasiProcess, WasiProcessId}, - thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, + os::{ + TtyBridge, + task::{ + control_plane::ControlPlaneError, + process::{WasiProcess, WasiProcessId}, + thread::{WasiMemoryLayout, WasiThread, WasiThreadHandle, WasiThreadId}, + }, }, state::PreparedInstanceGroupData, syscalls::platform_clock_time_get, @@ -75,6 +78,7 @@ async fn write_readonly_buffer_to_fs( pub struct WasiEnvInit { pub(crate) state: WasiState, pub runtime: Arc, + pub tty: Option>, pub webc_dependencies: Vec, pub mapped_commands: HashMap, pub bin_factory: BinFactory, @@ -131,6 +135,7 @@ impl WasiEnvInit { preopen: self.state.preopen.clone(), }, runtime: self.runtime.clone(), + tty: self.tty.clone(), webc_dependencies: self.webc_dependencies.clone(), mapped_commands: self.mapped_commands.clone(), bin_factory: self.bin_factory.clone(), @@ -174,6 +179,8 @@ pub struct WasiEnv { pub owned_handles: Vec, /// Implementation of the WASI runtime. pub runtime: Arc, + /// Terminal state scoped to this process tree, overriding the runtime default. + pub tty: Option>, pub capabilities: Capabilities, @@ -233,6 +240,7 @@ impl Clone for WasiEnv { inner: Default::default(), owned_handles: self.owned_handles.clone(), runtime: self.runtime.clone(), + tty: self.tty.clone(), capabilities: self.capabilities.clone(), enable_deep_sleep: self.enable_deep_sleep, enable_journal: self.enable_journal, @@ -275,6 +283,7 @@ impl WasiEnv { inner: Default::default(), owned_handles: Vec::new(), runtime: self.runtime.clone(), + tty: self.tty.clone(), capabilities: self.capabilities.clone(), enable_deep_sleep: self.enable_deep_sleep, enable_journal: self.enable_journal, @@ -442,6 +451,7 @@ impl WasiEnv { .threading .enable_exponential_cpu_backoff, runtime: init.runtime, + tty: init.tty, bin_factory: init.bin_factory, capabilities: init.capabilities, disable_fs_cleanup: false, @@ -664,6 +674,11 @@ impl WasiEnv { self.runtime.deref() } + /// Returns the process-tree terminal, falling back to the runtime default. + pub fn tty(&self) -> Option<&(dyn TtyBridge + Send + Sync)> { + self.tty.as_deref().or_else(|| self.runtime.tty()) + } + /// Returns a copy of the current tasks implementation for this environment pub fn tasks(&self) -> &Arc { self.runtime.task_manager() diff --git a/lib/wasix/src/syscalls/wasix/tty_get.rs b/lib/wasix/src/syscalls/wasix/tty_get.rs index 93d28dd74081..e57422d56804 100644 --- a/lib/wasix/src/syscalls/wasix/tty_get.rs +++ b/lib/wasix/src/syscalls/wasix/tty_get.rs @@ -9,9 +9,7 @@ pub fn tty_get( tty_state: WasmPtr, ) -> Errno { let env = ctx.data(); - - let env = ctx.data(); - let bridge = if let Some(t) = env.runtime.tty() { + let bridge = if let Some(t) = env.tty() { t } else { return Errno::Notsup; diff --git a/lib/wasix/src/syscalls/wasix/tty_set.rs b/lib/wasix/src/syscalls/wasix/tty_set.rs index d97accdb06d2..c41b980142a5 100644 --- a/lib/wasix/src/syscalls/wasix/tty_set.rs +++ b/lib/wasix/src/syscalls/wasix/tty_set.rs @@ -58,7 +58,7 @@ pub fn tty_set_internal( state: WasiTtyState, ) -> Result<(), Errno> { let env = ctx.data(); - let bridge = if let Some(t) = env.runtime.tty() { + let bridge = if let Some(t) = env.tty() { t } else { return Err(Errno::Notsup); From 909650811f928ec17d76a397489d80c2498f9fdd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Jul 2026 16:50:25 -0700 Subject: [PATCH 16/32] fix(wasix): preserve descriptor rights in dup2 --- lib/wasix/src/fs/mod.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/wasix/src/fs/mod.rs b/lib/wasix/src/fs/mod.rs index 4dc443aaa2a2..ed24fd329657 100644 --- a/lib/wasix/src/fs/mod.rs +++ b/lib/wasix/src/fs/mod.rs @@ -2182,7 +2182,11 @@ impl WasiFs { let new_fd_entry = Fd { inner: FdInner { offset: fd_entry.inner.offset.clone(), - rights: fd_entry.inner.rights_inheriting, + // dup2 aliases an existing descriptor; it does not open a + // child beneath it. Preserve the descriptor's active + // rights just like clone_fd does. In particular, stdio's + // inheriting rights are intentionally empty. + rights: fd_entry.inner.rights, fd_flags: { let mut f = fd_entry.inner.fd_flags; f.set(Fdflagsext::CLOEXEC, false); @@ -2904,6 +2908,23 @@ mod tests { use crate::WasiEnvBuilder; use crate::bin_factory::{BinaryPackage, BinaryPackageMount, BinaryPackageMounts}; + #[tokio::test] + async fn dup2_preserves_active_stdio_rights() { + let init = WasiEnvBuilder::new("test_prog") + .engine(Engine::default()) + .build_init() + .unwrap(); + let fs = &init.state.fs; + let stdout = fs.get_fd(__WASI_STDOUT_FILENO).unwrap(); + assert!(stdout.inner.rights.contains(Rights::FD_FILESTAT_GET)); + assert!(stdout.inner.rights_inheriting.is_empty()); + + fs.dup2_at(__WASI_STDOUT_FILENO, 42).unwrap(); + let duplicate = fs.get_fd(42).unwrap(); + assert_eq!(duplicate.inner.rights, stdout.inner.rights); + assert!(duplicate.inner.rights.contains(Rights::FD_FILESTAT_GET)); + } + fn webc_symlink_fs() -> virtual_fs::WebcVolumeFileSystem { let timestamps = webc::v3::Timestamps::default(); let dir = webc::v3::write::Directory::new( From e8d722ffd31ddbf0ea4ae012c82867e225ed5e4a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 31 Jul 2026 20:21:11 -0700 Subject: [PATCH 17/32] fix(wasix): deduplicate spawned command environment --- lib/wasix/src/state/env.rs | 70 ++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index 08b4a6bda3a3..a26af6b03627 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -21,7 +21,7 @@ use crate::{ use futures::future::BoxFuture; use rand::RngExt; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ops::Deref, path::{Path, PathBuf}, str, @@ -45,7 +45,28 @@ use wasmer_wasix_types::{ use webc::metadata::annotations::Wasi; pub use super::handles::*; -use super::{Linker, WasiState, context_switching::ContextSwitchingEnvironment, conv_env_vars}; +use super::{Linker, WasiState, context_switching::ContextSwitchingEnvironment}; + +fn add_command_env_defaults(environment: &mut Vec>, defaults: Vec) { + let mut names = environment + .iter() + .filter_map(|entry| { + entry + .iter() + .position(|byte| *byte == b'=') + .map(|separator| entry[..separator].to_vec()) + }) + .collect::>(); + + for default in defaults { + let Some((key, value)) = default.split_once('=') else { + continue; + }; + if names.insert(key.as_bytes().to_vec()) { + environment.push([key.as_bytes(), b"=", value.as_bytes()].concat()); + } + } +} async fn write_readonly_buffer_to_fs( fs: &WasiFsRoot, @@ -1348,22 +1369,7 @@ impl WasiEnv { })) = cmd.metadata().wasi() { if let Some(env_vars) = env_vars { - let env_vars = env_vars - .into_iter() - .map(|env_var| { - let (k, v) = env_var.split_once('=').unwrap(); - - (k.to_string(), v.as_bytes().to_vec()) - }) - .collect::>(); - - let env_vars = conv_env_vars(env_vars); - - self.state - .envs - .lock() - .unwrap() - .extend_from_slice(env_vars.as_slice()); + add_command_env_defaults(&mut self.state.envs.lock().unwrap(), env_vars); } if let Some(main_args) = main_args { @@ -1379,3 +1385,31 @@ impl WasiEnv { } } } + +#[cfg(test)] +mod tests { + use super::add_command_env_defaults; + + #[test] + fn spawned_command_environment_is_default_only() { + let mut environment = vec![b"HOME=/workspace".to_vec(), b"PATH=/bin".to_vec()]; + + add_command_env_defaults( + &mut environment, + vec![ + "HOME=/package".to_owned(), + "PREFIX=/".to_owned(), + "PREFIX=/duplicate".to_owned(), + ], + ); + + assert_eq!( + environment, + vec![ + b"HOME=/workspace".to_vec(), + b"PATH=/bin".to_vec(), + b"PREFIX=/".to_vec(), + ] + ); + } +} From 9d62f5f5511e82d79cac185b707109e495c031aa Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Aug 2026 15:16:53 -0700 Subject: [PATCH 18/32] Improved dependencies for SDK --- lib/api/Cargo.toml | 7 +++++-- lib/api/src/error.rs | 7 ++++++- lib/package/Cargo.toml | 2 +- lib/wasix/Cargo.toml | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index ae4df557f33b..b48f498ff921 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -34,7 +34,7 @@ bytes.workspace = true tracing = { workspace = true, default-features = true } # - Optional shared dependencies. wat = { workspace = true, optional = true } -symbolic-demangle.workspace = true +symbolic-demangle = { workspace = true, optional = true } shared-buffer.workspace = true loupe = { workspace = true, optional = true, features = [ @@ -127,7 +127,10 @@ artifact-size = [ # Features for `sys`. sys = ["std", "dep:wasmer-vm", "dep:wasmer-compiler"] -sys-default = ["sys", "wat", "cranelift"] +sys-default = ["sys", "wat", "cranelift", "demangle"] + +# Improve native stack traces without forcing the demanglers into browser builds. +demangle = ["dep:symbolic-demangle"] headless = [] diff --git a/lib/api/src/error.rs b/lib/api/src/error.rs index 37afa1797983..aaabbbef8d64 100644 --- a/lib/api/src/error.rs +++ b/lib/api/src/error.rs @@ -253,7 +253,12 @@ impl RuntimeError { writeln!(f)?; write!(f, " at ")?; match frame.function_name() { - Some(name) => write!(f, "{}", symbolic_demangle::demangle(name))?, + Some(name) => { + #[cfg(feature = "demangle")] + write!(f, "{}", symbolic_demangle::demangle(name))?; + #[cfg(not(feature = "demangle"))] + write!(f, "{name}")?; + } None => write!(f, "")?, } write!( diff --git a/lib/package/Cargo.toml b/lib/package/Cargo.toml index 91bcfb805e0e..1b2157d78e1c 100644 --- a/lib/package/Cargo.toml +++ b/lib/package/Cargo.toml @@ -30,7 +30,6 @@ cfg-if.workspace = true ciborium.workspace = true semver.workspace = true url.workspace = true -insta = { workspace = true, features = ["filters", "yaml"] } flate2.workspace = true tar.workspace = true tempfile.workspace = true @@ -40,6 +39,7 @@ ignore.workspace = true libc.workspace = true [dev-dependencies] +insta = { workspace = true, features = ["filters", "yaml"] } pretty_assertions.workspace = true tempfile.workspace = true regex.workspace = true diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index ca572426251e..720556b7dffb 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -227,6 +227,7 @@ sys = [ "wasmer/wat", "wasmer/js-serializable-module", "wasmer/sys", + "wasmer/demangle", # We must ensure at least one compiler backend (or `headless`) is enabled for `wasmer/sys`. "wasmer/headless", ] @@ -253,7 +254,6 @@ js = [ "web-sys", "wasmer/js-default", "wasmer/wasm-types-polyfill", - "wasmer/wat", "wasmer/js-serializable-module", ] js-default = ["js"] From b5fb301e1ee3a63198a1495dc221c54b0671464c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Aug 2026 15:41:01 -0700 Subject: [PATCH 19/32] Improve dependencies features further --- Cargo.toml | 4 ++-- lib/backend-api/Cargo.toml | 2 +- lib/cli/Cargo.toml | 2 +- lib/package/Cargo.toml | 6 ++++++ lib/package/src/package/mod.rs | 12 ++++++++---- lib/package/src/utils.rs | 32 ++++++++++++++++++++++++++++++++ lib/swift/Cargo.toml | 2 +- lib/virtual-fs/Cargo.toml | 8 ++++---- lib/wasix/Cargo.toml | 12 ++++++++++-- lib/wasix/src/lib.rs | 23 +++++++++++++++++++++-- 10 files changed, 86 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed3baf9bbbfd..2732f064eff6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,13 +89,13 @@ version = "7.2.1" [workspace.dependencies] # Repo-local crates -wasmer-package = { version = "0.702.1", path = "lib/package" } +wasmer-package = { version = "0.702.1", path = "lib/package", default-features = false } wasmer-config = { path = "./lib/config" } wasmer-wasix = { path = "./lib/wasix" } wasmer-sdk = { path = "./lib/sdk" } # Wasmer-owned crates -webc = "=12.0.0" +webc = { version = "=12.0.0", default-features = false } shared-buffer = "0.1.4" loupe = "0.2.0" diff --git a/lib/backend-api/Cargo.toml b/lib/backend-api/Cargo.toml index 6911f87e449c..c2044fde52a0 100644 --- a/lib/backend-api/Cargo.toml +++ b/lib/backend-api/Cargo.toml @@ -16,7 +16,7 @@ rust-version.workspace = true [dependencies] # Wasmer dependencies. wasmer-config = { version = "0.702.1", path = "../config" } -wasmer-package.workspace = true +wasmer-package = { workspace = true, features = ["webc-v1", "webc-v2", "webc-v3"] } webc.workspace = true # crates.io dependencies. diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index e48f19707c83..ffba6c50f685 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -136,7 +136,7 @@ wasmer-compiler = { version = "=7.2.1", path = "../compiler", features = [ wasmer-compiler-cranelift = { version = "=7.2.1", path = "../compiler-cranelift", optional = true } wasmer-compiler-singlepass = { version = "=7.2.1", path = "../compiler-singlepass", optional = true } wasmer-compiler-llvm = { version = "=7.2.1", path = "../compiler-llvm", optional = true } -wasmer-package.workspace = true +wasmer-package = { workspace = true, features = ["webc-v1", "webc-v2", "webc-v3"] } wasmer-vm = { version = "=7.2.1", path = "../vm", optional = true } wasmer-wasix = { path = "../wasix", version = "=0.702.1", features = [ "logging", diff --git a/lib/package/Cargo.toml b/lib/package/Cargo.toml index 1b2157d78e1c..227433c2a802 100644 --- a/lib/package/Cargo.toml +++ b/lib/package/Cargo.toml @@ -45,3 +45,9 @@ tempfile.workspace = true regex.workspace = true ureq.workspace = true hexdump.workspace = true + +[features] +default = ["webc-v3"] +webc-v1 = ["webc/v1"] +webc-v2 = ["webc/v2"] +webc-v3 = ["webc/v3"] diff --git a/lib/package/src/package/mod.rs b/lib/package/src/package/mod.rs index d4df84327ffe..c647bc03535f 100644 --- a/lib/package/src/package/mod.rs +++ b/lib/package/src/package/mod.rs @@ -17,18 +17,21 @@ pub use self::{ #[cfg(test)] mod tests { + #[cfg(feature = "webc-v2")] use sha2::Digest; + #[cfg(feature = "webc-v2")] use shared_buffer::OwnedBuffer; use tempfile::TempDir; - use webc::{ - metadata::annotations::FileSystemMapping, - migration::{are_semantically_equivalent, v2_to_v3, v3_to_v2}, - }; + #[cfg(feature = "webc-v2")] + use webc::metadata::annotations::FileSystemMapping; + #[cfg(feature = "webc-v2")] + use webc::migration::{are_semantically_equivalent, v2_to_v3, v3_to_v2}; use crate::{package::Package, utils::from_bytes}; #[test] + #[cfg(feature = "webc-v2")] fn migration_roundtrip() { let temp = TempDir::new().unwrap(); let wasmer_toml = r#" @@ -218,6 +221,7 @@ mod tests { } #[test] + #[cfg(feature = "webc-v2")] fn fs_entry_is_not_required_for_migration() { let temp = TempDir::new().unwrap(); let wasmer_toml = r#" diff --git a/lib/package/src/utils.rs b/lib/package/src/utils.rs index 55d6e62dda95..14985dcbf1d7 100644 --- a/lib/package/src/utils.rs +++ b/lib/package/src/utils.rs @@ -97,6 +97,7 @@ fn parse_dir(path: &Path) -> Result { } #[allow(clippy::result_large_err)] +#[cfg(feature = "webc-v1")] fn parse_v1_mmap(f: File) -> Result { // We need to explicitly use WebcMmap to get a memory-mapped // parser @@ -106,6 +107,13 @@ fn parse_v1_mmap(f: File) -> Result { } #[allow(clippy::result_large_err)] +#[cfg(not(feature = "webc-v1"))] +fn parse_v1_mmap(_f: File) -> Result { + Err(ContainerError::FeatureNotEnabled { feature: "v1" }) +} + +#[allow(clippy::result_large_err)] +#[cfg(feature = "webc-v2")] fn parse_v2_mmap(f: File) -> Result { // Note: OwnedReader::from_file() will automatically try to // use a memory-mapped file when possible. @@ -114,6 +122,13 @@ fn parse_v2_mmap(f: File) -> Result { } #[allow(clippy::result_large_err)] +#[cfg(not(feature = "webc-v2"))] +fn parse_v2_mmap(_f: File) -> Result { + Err(ContainerError::FeatureNotEnabled { feature: "v2" }) +} + +#[allow(clippy::result_large_err)] +#[cfg(feature = "webc-v3")] fn parse_v3_mmap(f: File) -> Result { // Note: OwnedReader::from_file() will automatically try to // use a memory-mapped file when possible. @@ -121,6 +136,12 @@ fn parse_v3_mmap(f: File) -> Result { Ok(Container::new(webc)) } +#[allow(clippy::result_large_err)] +#[cfg(not(feature = "webc-v3"))] +fn parse_v3_mmap(_f: File) -> Result { + Err(ContainerError::FeatureNotEnabled { feature: "v3" }) +} + /// Convert a `Features` object to a list of WebAssembly feature strings /// that can be used in annotations. /// @@ -222,3 +243,14 @@ pub fn wasm_annotations_to_features(feature_strings: &[String]) -> Features { features } + +#[cfg(all(test, not(feature = "webc-v1"), not(feature = "webc-v2")))] +mod modern_webc_tests { + use super::from_bytes; + + #[test] + fn rejects_legacy_versions() { + assert!(from_bytes(b"\0webc001".to_vec()).is_err()); + assert!(from_bytes(b"\0webc002".to_vec()).is_err()); + } +} diff --git a/lib/swift/Cargo.toml b/lib/swift/Cargo.toml index 32b175a201ab..00ea69057ad1 100644 --- a/lib/swift/Cargo.toml +++ b/lib/swift/Cargo.toml @@ -26,7 +26,7 @@ wasmer-wasix = { version = "=0.702.1", path = "../wasix", default-features = fal "sys", ] } webc.workspace = true -wasmer-package.workspace = true +wasmer-package = { workspace = true, features = ["webc-v1", "webc-v2", "webc-v3"] } [build-dependencies] diff --git a/lib/virtual-fs/Cargo.toml b/lib/virtual-fs/Cargo.toml index 5dc7c30d0ad9..69f31937331b 100644 --- a/lib/virtual-fs/Cargo.toml +++ b/lib/virtual-fs/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] -wasmer-package.workspace = true +wasmer-package = { workspace = true, features = ["webc-v3"] } virtual-mio = { path = "../virtual-io", version = "0.702.1", default-features = false } dashmap.workspace = true derive_more.workspace = true @@ -35,7 +35,7 @@ tokio = { workspace = true, features = [ "macros", ], default-features = false } tracing = { workspace = true, default-features = true } -webc = { workspace = true, optional = true, features = ["v1"] } +webc = { workspace = true, optional = true } serde = { workspace = true, default-features = false, features = [ "derive", ], optional = true } @@ -61,8 +61,8 @@ host-fs = [ "tokio/io-std", "tokio/rt", ] -webc-fs = ["webc", "anyhow"] -static-fs = ["webc", "anyhow"] +webc-fs = ["webc", "webc/v3", "anyhow"] +static-fs = ["webc", "webc/v1", "anyhow"] # Deprecated feature (a compile-time warning reported if used). enable-serde = [] js = ["dep:web-time", "getrandom/wasm_js"] diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index 720556b7dffb..2a86e6691a41 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -20,7 +20,7 @@ harness = false required-features = ["sys-thread"] [dependencies] -wasmer-package.workspace = true +wasmer-package = { workspace = true, default-features = false, features = ["webc-v3"] } wasmer-wasix-types = { path = "../wasi-types", version = "0.702.1", features = [ "enable-serde", ] } @@ -42,7 +42,7 @@ http.workspace = true dashmap.workspace = true fnv.workspace = true base64.workspace = true -webc.workspace = true +webc = { workspace = true, features = ["v3"] } serde_yaml.workspace = true rkyv.workspace = true shared-buffer.workspace = true @@ -230,6 +230,11 @@ sys = [ "wasmer/demangle", # We must ensure at least one compiler backend (or `headless`) is enabled for `wasmer/sys`. "wasmer/headless", + "memory64", + "wasmer-package/webc-v1", + "wasmer-package/webc-v2", + "webc/v1", + "webc/v2", ] sys-default = ["sys"] sys-poll = [] @@ -240,6 +245,9 @@ journal = ["tokio/fs", "wasmer-journal/log-file"] # Deprecated. Kept it for compatibility compiler = [] +# Enables the `wasix_64v1` ABI and its 64-bit syscall implementations. +memory64 = [] + singlepass = ["wasmer/singlepass"] v8 = ["wasmer/v8"] llvm = ["wasmer/llvm"] diff --git a/lib/wasix/src/lib.rs b/lib/wasix/src/lib.rs index 4aebbf42b6c9..86e1de5b9c66 100644 --- a/lib/wasix/src/lib.rs +++ b/lib/wasix/src/lib.rs @@ -84,6 +84,9 @@ use thiserror::Error; pub use wasmer; pub use wasmer_wasix_types; +#[cfg(feature = "memory64")] +#[allow(unused_imports, reason = "used by namespace! expansions")] +use wasmer::Memory64; use wasmer::{ AsStoreMut, Exports, FunctionEnv, Imports, Memory32, MemoryAccessError, MemorySize, RuntimeError, imports, namespace, @@ -657,6 +660,7 @@ fn wasix_exports_32(mut store: &mut impl AsStoreMut, env: &FunctionEnv) namespace } +#[cfg(feature = "memory64")] fn wasix_exports_64(mut store: &mut impl AsStoreMut, env: &FunctionEnv) -> Exports { let engine_supports_async = store.as_store_ref().engine().supports_async(); @@ -817,7 +821,6 @@ fn import_object_for_all_wasi_versions( let exports_wasi_unstable = wasi_unstable_exports(store, env); let exports_wasi_snapshot_preview1 = wasi_snapshot_preview1_exports(store, env); let exports_wasix_32v1 = wasix_exports_32(store, env); - let exports_wasix_64v1 = wasix_exports_64(store, env); // Allowed due to JS feature flag complications. #[allow(unused_mut)] @@ -826,9 +829,16 @@ fn import_object_for_all_wasi_versions( "wasi_unstable" => exports_wasi_unstable, "wasi_snapshot_preview1" => exports_wasi_snapshot_preview1, "wasix_32v1" => exports_wasix_32v1, - "wasix_64v1" => exports_wasix_64v1, }; + #[cfg(feature = "memory64")] + { + let exports_wasix_64v1 = wasix_exports_64(store, env); + imports.extend(&imports! { + "wasix_64v1" => exports_wasix_64v1, + }); + } + imports } @@ -864,6 +874,7 @@ fn generate_import_object_wasix32_v1( } } +#[cfg(feature = "memory64")] fn generate_import_object_wasix64_v1( store: &mut impl AsStoreMut, env: &FunctionEnv, @@ -874,6 +885,14 @@ fn generate_import_object_wasix64_v1( } } +#[cfg(not(feature = "memory64"))] +fn generate_import_object_wasix64_v1( + _store: &mut impl AsStoreMut, + _env: &FunctionEnv, +) -> Imports { + Imports::new() +} + fn mem_error_to_wasi(err: MemoryAccessError) -> Errno { match err { MemoryAccessError::HeapOutOfBounds => Errno::Memviolation, From 652ca3b71c6b7abadc8c159232eaa900edcbbfcf Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Aug 2026 16:29:37 -0700 Subject: [PATCH 20/32] Split wasmer-package with two features: execution and authoring, for smaller builds --- lib/backend-api/Cargo.toml | 2 +- lib/cli/Cargo.toml | 2 +- lib/package/Cargo.toml | 55 ++++++--- lib/package/src/error.rs | 83 ++++++++++++++ lib/package/src/lib.rs | 9 +- lib/package/src/package/mod.rs | 6 +- lib/package/src/package/package.rs | 104 +----------------- lib/package/src/utils.rs | 51 ++++++++- lib/sdk/Cargo.toml | 2 +- lib/swift/Cargo.toml | 2 +- lib/virtual-fs/Cargo.toml | 2 +- lib/wasix/Cargo.toml | 4 +- lib/wasix/src/bin_factory/binary_package.rs | 3 + .../runtime/package_loader/builtin_loader.rs | 2 +- 14 files changed, 194 insertions(+), 133 deletions(-) create mode 100644 lib/package/src/error.rs diff --git a/lib/backend-api/Cargo.toml b/lib/backend-api/Cargo.toml index c2044fde52a0..7013ca9ae741 100644 --- a/lib/backend-api/Cargo.toml +++ b/lib/backend-api/Cargo.toml @@ -16,7 +16,7 @@ rust-version.workspace = true [dependencies] # Wasmer dependencies. wasmer-config = { version = "0.702.1", path = "../config" } -wasmer-package = { workspace = true, features = ["webc-v1", "webc-v2", "webc-v3"] } +wasmer-package = { workspace = true, features = ["execution", "webc-v1", "webc-v2", "webc-v3"] } webc.workspace = true # crates.io dependencies. diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index ffba6c50f685..d1e70de6514e 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -136,7 +136,7 @@ wasmer-compiler = { version = "=7.2.1", path = "../compiler", features = [ wasmer-compiler-cranelift = { version = "=7.2.1", path = "../compiler-cranelift", optional = true } wasmer-compiler-singlepass = { version = "=7.2.1", path = "../compiler-singlepass", optional = true } wasmer-compiler-llvm = { version = "=7.2.1", path = "../compiler-llvm", optional = true } -wasmer-package = { workspace = true, features = ["webc-v1", "webc-v2", "webc-v3"] } +wasmer-package = { workspace = true, features = ["authoring", "webc-v1", "webc-v2", "webc-v3"] } wasmer-vm = { version = "=7.2.1", path = "../vm", optional = true } wasmer-wasix = { path = "../wasix", version = "=0.702.1", features = [ "logging", diff --git a/lib/package/Cargo.toml b/lib/package/Cargo.toml index 227433c2a802..12d77a8a6801 100644 --- a/lib/package/Cargo.toml +++ b/lib/package/Cargo.toml @@ -14,26 +14,26 @@ rust-version.workspace = true [dependencies] webc.workspace = true -wasmer-config = { version = "0.702.1", path = "../config" } -wasmer-types = { version = "7.2.1", path = "../types", features = [ +wasmer-config = { version = "0.702.1", path = "../config", optional = true } +wasmer-types = { version = "7.2.1", path = "../types", optional = true, features = [ "detect-wasm-features", ] } -toml.workspace = true -bytes.workspace = true -sha2.workspace = true -shared-buffer.workspace = true -serde.workspace = true -serde_json.workspace = true -anyhow.workspace = true +toml = { workspace = true, optional = true } +bytes = { workspace = true, optional = true } +sha2 = { workspace = true, optional = true } +shared-buffer = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +anyhow = { workspace = true, optional = true } thiserror.workspace = true -cfg-if.workspace = true -ciborium.workspace = true -semver.workspace = true -url.workspace = true -flate2.workspace = true -tar.workspace = true -tempfile.workspace = true -ignore.workspace = true +cfg-if = { workspace = true, optional = true } +ciborium = { workspace = true, optional = true } +semver = { workspace = true, optional = true } +url = { workspace = true, optional = true } +flate2 = { workspace = true, optional = true } +tar = { workspace = true, optional = true } +tempfile = { workspace = true, optional = true } +ignore = { workspace = true, optional = true } [target.'cfg(all(target_family = "wasm", target_os = "wasi"))'.dependencies] libc.workspace = true @@ -47,7 +47,26 @@ ureq.workspace = true hexdump.workspace = true [features] -default = ["webc-v3"] +default = ["execution", "authoring", "webc-v3"] +execution = ["dep:bytes", "dep:wasmer-types"] +authoring = [ + "execution", + "dep:anyhow", + "dep:cfg-if", + "dep:ciborium", + "dep:flate2", + "dep:ignore", + "dep:semver", + "dep:serde", + "dep:serde_json", + "dep:sha2", + "dep:shared-buffer", + "dep:tar", + "dep:tempfile", + "dep:toml", + "dep:url", + "dep:wasmer-config", +] webc-v1 = ["webc/v1"] webc-v2 = ["webc/v2"] webc-v3 = ["webc/v3"] diff --git a/lib/package/src/error.rs b/lib/package/src/error.rs new file mode 100644 index 000000000000..5e30ab566708 --- /dev/null +++ b/lib/package/src/error.rs @@ -0,0 +1,83 @@ +#[cfg(feature = "authoring")] +use std::path::PathBuf; + +use webc::{ContainerError, DetectError}; + +#[cfg(feature = "authoring")] +use crate::package::ManifestError; + +/// Errors that may occur while loading a Wasmer package. +#[derive(Debug, thiserror::Error)] +#[allow(clippy::result_large_err)] +#[non_exhaustive] +pub enum WasmerPackageError { + #[cfg(feature = "authoring")] + #[error("Unable to create a temporary directory")] + TempDir(#[source] std::io::Error), + #[cfg(feature = "authoring")] + #[error("Unable to open \"{}\"", path.display())] + FileOpen { + path: PathBuf, + #[source] + error: std::io::Error, + }, + #[cfg(feature = "authoring")] + #[error("Unable to read \"{}\"", path.display())] + FileRead { + path: PathBuf, + #[source] + error: std::io::Error, + }, + #[cfg(feature = "authoring")] + #[error("IO Error: {0:?}")] + IoError(#[from] std::io::Error), + #[cfg(feature = "authoring")] + #[error("Malformed path format: {0:?}")] + MalformedPath(PathBuf), + #[cfg(feature = "authoring")] + #[error("Unable to extract the tarball")] + Tarball(#[source] std::io::Error), + #[cfg(feature = "authoring")] + #[error("Unable to deserialize \"{}\"", path.display())] + TomlDeserialize { + path: PathBuf, + #[source] + error: toml::de::Error, + }, + #[cfg(feature = "authoring")] + #[error("Unable to deserialize \"{}\"", path.display())] + JsonDeserialize { + path: PathBuf, + #[source] + error: serde_json::Error, + }, + #[cfg(feature = "authoring")] + #[error("Unable to find the \"wasmer.toml\"")] + MissingManifest, + #[cfg(feature = "authoring")] + #[error("Unable to get the absolute path for \"{}\"", path.display())] + Canonicalize { + path: PathBuf, + #[source] + error: std::io::Error, + }, + #[cfg(feature = "authoring")] + #[error("Unable to load the \"wasmer.toml\" manifest")] + Manifest(#[from] ManifestError), + #[cfg(feature = "authoring")] + #[error("The manifest is invalid")] + Validation(#[from] wasmer_config::package::ValidationError), + #[cfg(feature = "authoring")] + #[error("Path: \"{}\" does not exist", path.display())] + PathNotExists { path: PathBuf }, + #[cfg(feature = "authoring")] + #[error("Volume creation failed: {0:?}")] + VolumeCreation(#[from] anyhow::Error), + #[cfg(feature = "authoring")] + #[error("serde error: {0:?}")] + SerdeError(#[from] ciborium::value::Error), + #[error("container error: {0:?}")] + ContainerError(#[from] ContainerError), + #[error("detect error: {0:?}")] + DetectError(#[from] DetectError), +} diff --git a/lib/package/src/lib.rs b/lib/package/src/lib.rs index 2d4ab3b19769..825c803a1219 100644 --- a/lib/package/src/lib.rs +++ b/lib/package/src/lib.rs @@ -1,7 +1,14 @@ #[macro_use] -#[cfg(test)] +#[cfg(all(test, feature = "authoring"))] mod macros; +mod error; + +pub use error::WasmerPackageError; + +#[cfg(feature = "authoring")] pub mod convert; +#[cfg(feature = "authoring")] pub mod package; +#[cfg(feature = "execution")] pub mod utils; diff --git a/lib/package/src/package/mod.rs b/lib/package/src/package/mod.rs index c647bc03535f..a98df013b928 100644 --- a/lib/package/src/package/mod.rs +++ b/lib/package/src/package/mod.rs @@ -7,13 +7,11 @@ pub(crate) mod volume; pub use self::{ manifest::ManifestError, - package::{ - Package, WalkBuilderFactory, WasmerPackageError, include_everything_walker, - wasmer_ignore_walker, - }, + package::{Package, WalkBuilderFactory, include_everything_walker, wasmer_ignore_walker}, strictness::Strictness, volume::{WasmerPackageVolume, fs::*, in_memory::*}, }; +pub use crate::WasmerPackageError; #[cfg(test)] mod tests { diff --git a/lib/package/src/package/package.rs b/lib/package/src/package/package.rs index b3cb89095660..c473c5a79165 100644 --- a/lib/package/src/package/package.rs +++ b/lib/package/src/package/package.rs @@ -22,8 +22,7 @@ use tempfile::TempDir; use wasmer_config::package::Manifest as WasmerManifest; use webc::{ - AbstractVolume, AbstractWebc, Container, ContainerError, DetectError, PathSegment, Version, - Volume, + AbstractVolume, AbstractWebc, Container, PathSegment, Version, Volume, metadata::{Manifest as WebcManifest, annotations::Wapm}, v3::{ ChecksumAlgorithm, Timestamps, @@ -31,109 +30,14 @@ use webc::{ }, }; +use crate::WasmerPackageError; + use super::{ - ManifestError, MemoryVolume, Strictness, + MemoryVolume, Strictness, manifest::wasmer_manifest_to_webc, volume::{WasmerPackageVolume, fs::FsVolume}, }; -/// Errors that may occur while loading a Wasmer package from disk. -#[derive(Debug, thiserror::Error)] -#[allow(clippy::result_large_err)] -#[non_exhaustive] -pub enum WasmerPackageError { - /// Unable to create a temporary directory. - #[error("Unable to create a temporary directory")] - TempDir(#[source] std::io::Error), - /// Unable to open a file. - #[error("Unable to open \"{}\"", path.display())] - FileOpen { - /// The file being opened. - path: PathBuf, - /// The underlying error. - #[source] - error: std::io::Error, - }, - /// Unable to read a file. - #[error("Unable to read \"{}\"", path.display())] - FileRead { - /// The file being opened. - path: PathBuf, - /// The underlying error. - #[source] - error: std::io::Error, - }, - - /// Generic IO error. - #[error("IO Error: {0:?}")] - IoError(#[from] std::io::Error), - - /// Unexpected path format - #[error("Malformed path format: {0:?}")] - MalformedPath(PathBuf), - - /// Unable to extract the tarball. - #[error("Unable to extract the tarball")] - Tarball(#[source] std::io::Error), - /// Unable to deserialize the `wasmer.toml` file. - #[error("Unable to deserialize \"{}\"", path.display())] - TomlDeserialize { - /// The file being deserialized. - path: PathBuf, - /// The underlying error. - #[source] - error: toml::de::Error, - }, - /// Unable to deserialize a json file. - #[error("Unable to deserialize \"{}\"", path.display())] - JsonDeserialize { - /// The file being deserialized. - path: PathBuf, - /// The underlying error. - #[source] - error: serde_json::Error, - }, - /// Unable to find the `wasmer.toml` file. - #[error("Unable to find the \"wasmer.toml\"")] - MissingManifest, - /// Unable to canonicalize a path. - #[error("Unable to get the absolute path for \"{}\"", path.display())] - Canonicalize { - /// The path being canonicalized. - path: PathBuf, - /// The underlying error. - #[source] - error: std::io::Error, - }, - /// Unable to load the `wasmer.toml` manifest. - #[error("Unable to load the \"wasmer.toml\" manifest")] - Manifest(#[from] ManifestError), - /// A manifest validation error. - #[error("The manifest is invalid")] - Validation(#[from] wasmer_config::package::ValidationError), - /// A path in the fs mapping does not exist - #[error("Path: \"{}\" does not exist", path.display())] - PathNotExists { - /// Path entry in fs mapping - path: PathBuf, - }, - /// Any error happening when populating the volumes tree map of a package - #[error("Volume creation failed: {0:?}")] - VolumeCreation(#[from] anyhow::Error), - - /// Error when serializing or deserializing - #[error("serde error: {0:?}")] - SerdeError(#[from] ciborium::value::Error), - - /// Container Error - #[error("container error: {0:?}")] - ContainerError(#[from] ContainerError), - - /// Detect Error - #[error("detect error: {0:?}")] - DetectError(#[from] DetectError), -} - // Serious Java vibes from this one! Still better than repeating the // function type everywhere. // I'm opting not to use a trait object here to avoid generics and boxing. diff --git a/lib/package/src/utils.rs b/lib/package/src/utils.rs index 14985dcbf1d7..f03a1a75b710 100644 --- a/lib/package/src/utils.rs +++ b/lib/package/src/utils.rs @@ -3,16 +3,22 @@ reason = "WasmerPackageError is large, but not often used" )] -use bytes::{Buf, Bytes}; +#[cfg(feature = "authoring")] +use bytes::Buf; +use bytes::Bytes; +#[cfg(feature = "authoring")] +use std::io::{BufRead, BufReader}; use std::{ fs::File, - io::{BufRead, BufReader, Read, Seek}, + io::{Read, Seek}, path::Path, }; use wasmer_types::Features; use webc::{Container, ContainerError, Version}; -use crate::package::{Package, WasmerPackageError}; +use crate::WasmerPackageError; +#[cfg(feature = "authoring")] +use crate::package::Package; /// Check if something looks like a `*.tar.gz` file. fn is_tarball(mut file: impl Read + Seek) -> bool { @@ -35,7 +41,13 @@ pub fn from_disk(path: impl AsRef) -> Result) -> Result) -> Result) -> Result Result { let pkg = Package::from_tarball(reader)?; Ok(Container::new(pkg)) } #[allow(clippy::result_large_err)] +#[cfg(feature = "authoring")] fn parse_dir(path: &Path) -> Result { let wasmer_toml = path.join("wasmer.toml"); let pkg = Package::from_manifest(wasmer_toml)?; @@ -254,3 +280,22 @@ mod modern_webc_tests { assert!(from_bytes(b"\0webc002".to_vec()).is_err()); } } + +#[cfg(all(test, feature = "execution", not(feature = "authoring")))] +mod execution_only_tests { + use webc::ContainerError; + + use super::from_bytes; + use crate::WasmerPackageError; + + #[test] + fn rejects_authoring_formats() { + let error = from_bytes(vec![0x1f, 0x8b]).unwrap_err(); + assert!(matches!( + error, + WasmerPackageError::ContainerError(ContainerError::FeatureNotEnabled { + feature: "authoring" + }) + )); + } +} diff --git a/lib/sdk/Cargo.toml b/lib/sdk/Cargo.toml index d73e62ef9062..9198794c9348 100644 --- a/lib/sdk/Cargo.toml +++ b/lib/sdk/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] wasmer-backend-api = { path = "../backend-api", version = "=0.702.1" } wasmer-config = { path = "../config", version = "=0.702.1" } -wasmer-package = { path = "../package", version = "=0.702.1" } +wasmer-package = { path = "../package", version = "=0.702.1", features = ["authoring"] } reqwest = { workspace = true, default-features = false, features = [ "json", diff --git a/lib/swift/Cargo.toml b/lib/swift/Cargo.toml index 00ea69057ad1..b78c454cc12a 100644 --- a/lib/swift/Cargo.toml +++ b/lib/swift/Cargo.toml @@ -26,7 +26,7 @@ wasmer-wasix = { version = "=0.702.1", path = "../wasix", default-features = fal "sys", ] } webc.workspace = true -wasmer-package = { workspace = true, features = ["webc-v1", "webc-v2", "webc-v3"] } +wasmer-package = { workspace = true, features = ["execution", "webc-v1", "webc-v2", "webc-v3"] } [build-dependencies] diff --git a/lib/virtual-fs/Cargo.toml b/lib/virtual-fs/Cargo.toml index 69f31937331b..b0f532f28c3a 100644 --- a/lib/virtual-fs/Cargo.toml +++ b/lib/virtual-fs/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] -wasmer-package = { workspace = true, features = ["webc-v3"] } +wasmer-package = { workspace = true, features = ["execution", "webc-v3"] } virtual-mio = { path = "../virtual-io", version = "0.702.1", default-features = false } dashmap.workspace = true derive_more.workspace = true diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index 2a86e6691a41..8dc7a05d8744 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -20,7 +20,7 @@ harness = false required-features = ["sys-thread"] [dependencies] -wasmer-package = { workspace = true, default-features = false, features = ["webc-v3"] } +wasmer-package = { workspace = true, default-features = false, features = ["execution", "webc-v3"] } wasmer-wasix-types = { path = "../wasi-types", version = "0.702.1", features = [ "enable-serde", ] } @@ -212,6 +212,7 @@ ctrlc = ["tokio/signal"] # the minimal sys implementation sys-minimal = ["wasmer/sys", "sys-thread", "tokio/fs"] sys = [ + "package-authoring", "webc/mmap", "time", "virtual-mio/sys", @@ -236,6 +237,7 @@ sys = [ "webc/v1", "webc/v2", ] +package-authoring = ["wasmer-package/authoring"] sys-default = ["sys"] sys-poll = [] extra-logging = [] diff --git a/lib/wasix/src/bin_factory/binary_package.rs b/lib/wasix/src/bin_factory/binary_package.rs index b01510dde842..172644e3bae6 100644 --- a/lib/wasix/src/bin_factory/binary_package.rs +++ b/lib/wasix/src/bin_factory/binary_package.rs @@ -5,9 +5,11 @@ use std::{ use anyhow::Context; use once_cell::sync::OnceCell; +#[cfg(feature = "package-authoring")] use sha2::Digest; use virtual_fs::{FileSystem, MountFileSystem}; use wasmer_config::package::{PackageHash, PackageId, PackageSource}; +#[cfg(feature = "package-authoring")] use wasmer_package::package::Package; use webc::Container; use webc::compat::SharedBytes; @@ -176,6 +178,7 @@ pub struct BinaryPackage { } impl BinaryPackage { + #[cfg(feature = "package-authoring")] #[tracing::instrument(level = "debug", skip_all)] pub async fn from_dir( dir: &Path, diff --git a/lib/wasix/src/runtime/package_loader/builtin_loader.rs b/lib/wasix/src/runtime/package_loader/builtin_loader.rs index 60de9f30db14..dc4a5fcb3fd8 100644 --- a/lib/wasix/src/runtime/package_loader/builtin_loader.rs +++ b/lib/wasix/src/runtime/package_loader/builtin_loader.rs @@ -11,7 +11,7 @@ use http::{HeaderMap, Method}; use tempfile::NamedTempFile; use url::Url; use wasmer_package::{ - package::WasmerPackageError, + WasmerPackageError, utils::{from_bytes, from_disk}, }; use webc::DetectError; From a5aa7e2bedf6ebef48f63766dfeb6fba2fadddc4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Aug 2026 18:49:30 -0700 Subject: [PATCH 21/32] fix(WASIX): Add support for shebang scripts --- lib/wasix/src/bin_factory/mod.rs | 296 ++++++++++++++++++++++++++----- 1 file changed, 251 insertions(+), 45 deletions(-) diff --git a/lib/wasix/src/bin_factory/mod.rs b/lib/wasix/src/bin_factory/mod.rs index a39afd46cd55..86271032762c 100644 --- a/lib/wasix/src/bin_factory/mod.rs +++ b/lib/wasix/src/bin_factory/mod.rs @@ -101,7 +101,7 @@ impl BinFactory { self.get_executable(name, fs) .await .and_then(|executable| match executable { - Executable::Wasm(_) => None, + Executable::Wasm(_) | Executable::Script(_) => None, Executable::BinaryPackage(pkg) => Some(pkg), }) } @@ -112,30 +112,41 @@ impl BinFactory { env: WasiEnv, ) -> Pin> + 'a>> { Box::pin(async move { - // Find the binary (or die trying) and make the spawn type - let res = self - .get_executable(name.as_str(), Some(env.fs_root())) - .await - .ok_or_else(|| SpawnError::BinaryNotFound { - binary: name.clone(), - }); - let executable = res?; - - // Execute - match executable { - Executable::Wasm(bytes) => { - let data = HashedModuleData::new(bytes.clone()); - spawn_exec_wasm(data, name.as_str(), env, &self.runtime).await - } - Executable::BinaryPackage(pkg) => { - { - let cmd = package_command_by_name(&pkg, name.as_str())?; - env.prepare_spawn(cmd); + let mut name = name; + + // A shebang is handled by the kernel on Unix. WASIX's binary factory + // fills that role for virtual filesystems, so resolve scripts here + // before trying to compile their bytes as WebAssembly. + for _ in 0..MAX_SHEBANG_DEPTH { + let (resolved_name, executable) = self + .get_executable_for_spawn(name.as_str(), &env) + .await + .ok_or_else(|| SpawnError::BinaryNotFound { + binary: name.clone(), + })?; + name = resolved_name; + + match executable { + Executable::Wasm(bytes) => { + let data = HashedModuleData::new(bytes.clone()); + return spawn_exec_wasm(data, name.as_str(), env, &self.runtime).await; } + Executable::BinaryPackage(pkg) => { + { + let cmd = package_command_by_name(&pkg, name.as_str())?; + env.prepare_spawn(cmd); + } - spawn_exec(pkg.as_ref().clone(), name.as_str(), env, &self.runtime).await + return spawn_exec(pkg.as_ref().clone(), name.as_str(), env, &self.runtime) + .await; + } + Executable::Script(script) => { + name = prepare_script_execution(&env, &name, script)?; + } } } + + Err(SpawnError::InvalidABI) }) } @@ -203,15 +214,153 @@ impl BinFactory { } } - // NAK - cache.insert(name, None); + // Do not negatively cache filesystem lookups: package managers and + // running guests can create executables after an earlier miss. + None + } + + async fn get_executable_for_spawn( + &self, + name: &str, + env: &WasiEnv, + ) -> Option<(String, Executable)> { + if name.contains('/') { + let name = if name.starts_with('/') { + name.to_string() + } else { + env.state.fs.relative_path_to_absolute(name.to_string()) + }; + return self + .get_executable(&name, Some(env.fs_root())) + .await + .map(|executable| (name, executable)); + } + + for directory in executable_search_path(env) { + let path = if directory.is_empty() { + name.to_string() + } else { + format!("{}/{}", directory.trim_end_matches('/'), name) + }; + let path = if path.starts_with('/') { + path + } else { + env.state.fs.relative_path_to_absolute(path) + }; + if let Some(executable) = self.get_executable(&path, Some(env.fs_root())).await { + return Some((path, executable)); + } + } + None } } +fn executable_search_path(env: &WasiEnv) -> Vec { + env.state + .envs + .lock() + .unwrap() + .iter() + .rev() + .find_map(|entry| { + entry + .strip_prefix(b"PATH=") + .map(|path| String::from_utf8_lossy(path).into_owned()) + }) + .unwrap_or_else(|| "/usr/local/bin:/bin:/usr/bin".to_string()) + .split(':') + .map(str::to_string) + .collect() +} + pub enum Executable { Wasm(OwnedBuffer), BinaryPackage(Arc), + Script(Shebang), +} + +const MAX_SHEBANG_DEPTH: usize = 4; + +#[derive(Debug)] +pub struct Shebang { + interpreter: String, + argument: Option, +} + +fn parse_shebang(bytes: &[u8]) -> Option { + let line = bytes.strip_prefix(b"#!")?; + let line_end = line + .iter() + .position(|byte| *byte == b'\n') + .unwrap_or(line.len()); + let line = std::str::from_utf8(&line[..line_end]) + .ok()? + .trim_end_matches('\r') + .trim(); + let (interpreter, argument) = line + .split_once(char::is_whitespace) + .map(|(interpreter, argument)| (interpreter, Some(argument.trim().to_string()))) + .unwrap_or((line, None)); + + if interpreter.is_empty() { + return None; + } + + Some(Shebang { + interpreter: interpreter.to_string(), + argument: argument.filter(|argument| !argument.is_empty()), + }) +} + +fn prepare_script_execution( + env: &WasiEnv, + script_name: &str, + script: Shebang, +) -> Result { + let mut args = env.state.args.lock().unwrap(); + let (interpreter, new_args) = script_command(script_name, script, &args)?; + *args = new_args; + Ok(interpreter) +} + +fn script_command( + script_name: &str, + script: Shebang, + original_args: &[String], +) -> Result<(String, Vec), SpawnError> { + let user_args = original_args.iter().skip(1).cloned(); + + // `/usr/bin/env NAME` is the portable shebang used by npm executables. + // Resolve NAME through the same package/PATH machinery as a direct exec, + // rather than requiring a host `/usr/bin/env` binary in the guest image. + let (interpreter, interpreter_args) = if script.interpreter.ends_with("/env") { + let argument = script.argument.ok_or(SpawnError::InvalidABI)?; + let mut words = argument.split_whitespace(); + let first = words.next().ok_or(SpawnError::InvalidABI)?; + let (interpreter, remaining) = if first == "-S" { + let interpreter = words.next().ok_or(SpawnError::InvalidABI)?; + (interpreter.to_string(), words.map(str::to_string).collect()) + } else if first.starts_with('-') { + return Err(SpawnError::InvalidABI); + } else { + (first.to_string(), words.map(str::to_string).collect()) + }; + (interpreter, remaining) + } else { + ( + script.interpreter, + script.argument.into_iter().collect::>(), + ) + }; + + let args = std::iter::once(interpreter.clone()) + .chain(interpreter_args) + .chain(std::iter::once(script_name.to_string())) + .chain(user_args) + .collect(); + + Ok((interpreter, args)) } async fn load_executable_from_filesystem( @@ -225,35 +374,92 @@ async fn load_executable_from_filesystem( .open(path) .context("Unable to open the file")?; - // Fast path if the file is fully available in memory. - // Prevents redundant copying of the file data. + // Fast path if the file is fully available in memory. This prevents a + // redundant copy and keeps executable classification in one place. if let Some(buf) = f.as_owned_buffer() { - if wasmer_package::utils::is_container(buf.as_slice()) { - let bytes = buf.clone().into_bytes(); - if let Ok(container) = from_bytes(bytes.clone()) { - let pkg = BinaryPackage::from_webc(&container, rt) - .await - .context("Unable to load the package")?; - - return Ok(Executable::BinaryPackage(Arc::new(pkg))); - } - } - - Ok(Executable::Wasm(buf)) + load_executable_from_buffer(buf, rt).await } else { let mut data = Vec::with_capacity(f.size() as usize); f.read_to_end(&mut data).await.context("Read failed")?; + load_executable_from_buffer(OwnedBuffer::from_bytes(data), rt).await + } +} - let bytes: bytes::Bytes = data.into(); - - if let Ok(container) = from_bytes(bytes.clone()) { - let pkg = BinaryPackage::from_webc(&container, rt) +async fn load_executable_from_buffer( + buffer: OwnedBuffer, + rt: &(dyn Runtime + Send + Sync), +) -> Result { + if wasmer_package::utils::is_container(buffer.as_slice()) { + let bytes = buffer.clone().into_bytes(); + if let Ok(container) = from_bytes(bytes) { + let package = BinaryPackage::from_webc(&container, rt) .await .context("Unable to load the package")?; - - Ok(Executable::BinaryPackage(Arc::new(pkg))) - } else { - Ok(Executable::Wasm(OwnedBuffer::from_bytes(bytes))) + return Ok(Executable::BinaryPackage(Arc::new(package))); } } + + if let Some(script) = parse_shebang(buffer.as_slice()) { + Ok(Executable::Script(script)) + } else { + Ok(Executable::Wasm(buffer)) + } +} + +#[cfg(test)] +mod tests { + use super::{parse_shebang, script_command}; + + #[test] + fn parses_env_shebang() { + let script = parse_shebang(b"#!/usr/bin/env node\nconsole.log('hello')\n").unwrap(); + assert_eq!(script.interpreter, "/usr/bin/env"); + assert_eq!(script.argument.as_deref(), Some("node")); + } + + #[test] + fn parses_direct_shebang_with_crlf() { + let script = parse_shebang(b"#!/bin/bash -e\r\necho hello\r\n").unwrap(); + assert_eq!(script.interpreter, "/bin/bash"); + assert_eq!(script.argument.as_deref(), Some("-e")); + } + + #[test] + fn ignores_regular_files() { + assert!(parse_shebang(b"console.log('hello')\n").is_none()); + } + + #[test] + fn env_shebang_resolves_interpreter_and_preserves_arguments() { + let script = parse_shebang(b"#!/usr/bin/env node\n").unwrap(); + let original = vec!["next".to_string(), "dev".to_string()]; + let (interpreter, args) = + script_command("/workspace/.bin/next", script, &original).unwrap(); + + assert_eq!(interpreter, "node"); + assert_eq!(args, ["node", "/workspace/.bin/next", "dev"]); + } + + #[test] + fn env_split_string_preserves_interpreter_arguments() { + let script = parse_shebang(b"#!/usr/bin/env -S node --no-warnings\n").unwrap(); + let original = vec!["tool".to_string(), "input.js".to_string()]; + let (interpreter, args) = script_command("/workspace/tool", script, &original).unwrap(); + + assert_eq!(interpreter, "node"); + assert_eq!( + args, + ["node", "--no-warnings", "/workspace/tool", "input.js"] + ); + } + + #[test] + fn direct_shebang_inserts_optional_argument_before_script() { + let script = parse_shebang(b"#!/bin/bash -e\n").unwrap(); + let original = vec!["script".to_string(), "hello".to_string()]; + let (interpreter, args) = script_command("/workspace/script", script, &original).unwrap(); + + assert_eq!(interpreter, "/bin/bash"); + assert_eq!(args, ["/bin/bash", "-e", "/workspace/script", "hello"]); + } } From 18a62c627c3732513e4763e620abf9efa7aa165d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Mon, 3 Aug 2026 19:09:54 -0700 Subject: [PATCH 22/32] Improved executable retrieval --- lib/wasix/src/bin_factory/mod.rs | 101 +++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/lib/wasix/src/bin_factory/mod.rs b/lib/wasix/src/bin_factory/mod.rs index 86271032762c..312f82a72c70 100644 --- a/lib/wasix/src/bin_factory/mod.rs +++ b/lib/wasix/src/bin_factory/mod.rs @@ -25,7 +25,8 @@ pub use self::{ }, }; use crate::{ - Runtime, SpawnError, WasiEnv, + Runtime, SpawnError, VIRTUAL_ROOT_FD, WasiEnv, + fs::Kind, os::{ command::{Commands, VirtualCommand}, task::TaskJoinHandle, @@ -231,7 +232,7 @@ impl BinFactory { env.state.fs.relative_path_to_absolute(name.to_string()) }; return self - .get_executable(&name, Some(env.fs_root())) + .get_executable_from_wasi_fs(&name, env) .await .map(|executable| (name, executable)); } @@ -247,13 +248,35 @@ impl BinFactory { } else { env.state.fs.relative_path_to_absolute(path) }; - if let Some(executable) = self.get_executable(&path, Some(env.fs_root())).await { + if let Some(executable) = self.get_executable_from_wasi_fs(&path, env).await { return Some((path, executable)); } } None } + + async fn get_executable_from_wasi_fs(&self, path: &str, env: &WasiEnv) -> Option { + if let Some(binary) = self.local.read().unwrap().get(path).cloned().flatten() { + return Some(Executable::BinaryPackage(binary)); + } + + match load_executable_from_wasi_fs(env, Path::new(path), self.runtime()).await { + Ok(executable) => { + if let Executable::BinaryPackage(package) = &executable { + self.local + .write() + .unwrap() + .insert(path.to_string(), Some(package.clone())); + } + Some(executable) + } + Err(error) => { + tracing::debug!(path, error = &*error, "Unable to load executable"); + None + } + } + } } fn executable_search_path(env: &WasiEnv) -> Vec { @@ -385,6 +408,42 @@ async fn load_executable_from_filesystem( } } +async fn load_executable_from_wasi_fs( + env: &WasiEnv, + path: &Path, + rt: &(dyn Runtime + Send + Sync), +) -> Result { + let inode = env + .state + .fs + .get_inode_at_path( + &env.state.inodes, + VIRTUAL_ROOT_FD, + path.to_string_lossy().as_ref(), + true, + ) + .map_err(|error| anyhow::anyhow!("Unable to resolve executable: {error}"))?; + + let (buffer, backing_path) = { + let kind = inode.read(); + match &*kind { + Kind::File { handle, path, .. } => { + let buffer = handle + .as_ref() + .and_then(|handle| handle.read().unwrap().as_owned_buffer()); + (buffer, path.clone()) + } + _ => anyhow::bail!("Executable is not a regular file"), + } + }; + + if let Some(buffer) = buffer { + load_executable_from_buffer(buffer, rt).await + } else { + load_executable_from_filesystem(&env.state.fs.root_fs, &backing_path, rt).await + } +} + async fn load_executable_from_buffer( buffer: OwnedBuffer, rt: &(dyn Runtime + Send + Sync), @@ -408,7 +467,13 @@ async fn load_executable_from_buffer( #[cfg(test)] mod tests { - use super::{parse_shebang, script_command}; + use std::path::Path; + + use virtual_fs::{AsyncWriteExt, FileSystem}; + use wasmer::Engine; + + use super::{Executable, load_executable_from_wasi_fs, parse_shebang, script_command}; + use crate::WasiEnvBuilder; #[test] fn parses_env_shebang() { @@ -462,4 +527,32 @@ mod tests { assert_eq!(interpreter, "/bin/bash"); assert_eq!(args, ["/bin/bash", "-e", "/workspace/script", "hello"]); } + + #[tokio::test] + async fn loads_shebang_through_relative_symlink() { + let mut builder = WasiEnvBuilder::new("test").engine(Engine::default()); + builder.preopen_vfs_dirs(["/".to_string()]).unwrap(); + let env = builder.build().unwrap(); + let fs = &env.state.fs.root_fs; + fs.create_dir(Path::new("/bin")).unwrap(); + fs.create_dir(Path::new("/pkg")).unwrap(); + + let mut target = fs + .new_open_options() + .create(true) + .write(true) + .open(Path::new("/pkg/next")) + .unwrap(); + target + .write_all(b"#!/usr/bin/env node\nconsole.log('hello')\n") + .await + .unwrap(); + fs.create_symlink(Path::new("../pkg/next"), Path::new("/bin/next")) + .unwrap(); + + let executable = load_executable_from_wasi_fs(&env, Path::new("/bin/next"), env.runtime()) + .await + .unwrap(); + assert!(matches!(executable, Executable::Script(_))); + } } From 046307555ab1b9d31f30750e0f508ab5960d507c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 4 Aug 2026 11:18:46 -0700 Subject: [PATCH 23/32] fix(api-js-backend): Allow up to 4Gb memory pointers in JS backend --- lib/api/src/backend/js/error.rs | 52 +++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/api/src/backend/js/error.rs b/lib/api/src/backend/js/error.rs index f5d9e2c68aac..993283e0b800 100644 --- a/lib/api/src/backend/js/error.rs +++ b/lib/api/src/backend/js/error.rs @@ -139,14 +139,62 @@ fn downcast_from_ptr(value: &JsValue) -> Option { .ok() .and_then(|v| v.dyn_into::().ok()) .and_then(|destroy_into_raw| destroy_into_raw.call0(value).ok()) - .and_then(|ret| ret.as_f64())?; + .and_then(|ret| ret.as_f64()) + .and_then(wasm32_pointer_from_js_number)?; Some(::from_abi( - wasm_bindgen::__rt::WasmPtr::from_usize(ptr as u32 as usize), + wasm_bindgen::__rt::WasmPtr::from_usize(ptr as usize), )) } } +/// JavaScript exposes a WebAssembly `i32` result as a signed number, while +/// newer glue may explicitly coerce pointer results to an unsigned number. +/// Accept both representations and preserve the original wasm32 bit pattern. +fn wasm32_pointer_from_js_number(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + + if (0.0..=u32::MAX as f64).contains(&value) { + Some(value as u32) + } else if ((i32::MIN as f64)..0.0).contains(&value) { + Some(value as i32 as u32) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::wasm32_pointer_from_js_number; + + #[test] + fn converts_signed_and_unsigned_wasm32_pointers() { + assert_eq!(wasm32_pointer_from_js_number(42.0), Some(42)); + assert_eq!( + wasm32_pointer_from_js_number(i32::MIN as f64), + Some(0x8000_0000), + ); + assert_eq!( + wasm32_pointer_from_js_number(-1.0), + Some(u32::MAX), + ); + assert_eq!( + wasm32_pointer_from_js_number(u32::MAX as f64), + Some(u32::MAX), + ); + } + + #[test] + fn rejects_numbers_that_cannot_be_wasm32_pointers() { + assert_eq!(wasm32_pointer_from_js_number(f64::NAN), None); + assert_eq!(wasm32_pointer_from_js_number(1.5), None); + assert_eq!(wasm32_pointer_from_js_number(i32::MIN as f64 - 1.0), None); + assert_eq!(wasm32_pointer_from_js_number(u32::MAX as f64 + 1.0), None); + } +} + /// A `Send+Sync` version of a JavaScript error. #[derive(Debug)] enum JsTrap { From a59b36f20611d407cf58dd3a22349c6819084cbb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 4 Aug 2026 16:06:15 -0700 Subject: [PATCH 24/32] fix(api-js-backend): Add support for ExternRefs in Wasmer JS backend --- lib/api/src/backend/js/entities/external.rs | 111 +++++++++++++++--- .../src/backend/js/entities/function/mod.rs | 51 +++++--- .../src/backend/js/entities/function/typed.rs | 4 +- lib/api/src/backend/js/entities/memory/mod.rs | 63 +++++++++- lib/api/src/backend/js/entities/store/obj.rs | 7 +- lib/api/src/backend/js/entities/table.rs | 108 +++++++++-------- lib/api/src/backend/js/utils/convert.rs | 39 ++++-- lib/api/src/backend/js/vm/external.rs | 21 +++- lib/api/src/utils/polyfill.rs | 48 -------- 9 files changed, 315 insertions(+), 137 deletions(-) diff --git a/lib/api/src/backend/js/entities/external.rs b/lib/api/src/backend/js/entities/external.rs index b71121e5a5cc..e539691c4997 100644 --- a/lib/api/src/backend/js/entities/external.rs +++ b/lib/api/src/backend/js/entities/external.rs @@ -1,44 +1,127 @@ use std::any::Any; +use js_sys::{Object, Symbol}; +use wasm_bindgen::JsValue; + +use crate::js::entities::store::StoreHandle; +use crate::js::utils::js_handle::JsHandle; use crate::js::vm::VMExternRef; use crate::store::{AsStoreMut, AsStoreRef}; -#[derive(Debug, Clone)] #[repr(transparent)] /// A WebAssembly `extern ref` in `js`. -pub struct ExternRef; +pub(crate) struct ExternRefData(pub(crate) Box); + +impl std::fmt::Debug for ExternRefData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExternRefData").finish_non_exhaustive() + } +} + +#[derive(Debug, Clone)] +pub struct ExternRef { + value: JsHandle, + host_data: Option>, +} + +unsafe impl Send for ExternRef {} +unsafe impl Sync for ExternRef {} impl ExternRef { - pub fn new(_store: &mut impl AsStoreMut, _value: T) -> Self + pub fn new(store: &mut impl AsStoreMut, value: T) -> Self where T: Any + Send + Sync + 'static + Sized, { - unimplemented!("ExternRef is not yet supported in Javascript"); + let handle = StoreHandle::new( + store.objects_mut().as_js_mut(), + ExternRefData(Box::new(value)), + ); + let object = Object::new(); + js_sys::Reflect::set( + &object, + host_ref_index_key().as_ref(), + &JsValue::from_f64(handle.internal_handle().index() as f64), + ) + .expect("setting the externref store index should succeed"); + js_sys::Reflect::set( + &object, + host_ref_store_key().as_ref(), + &JsValue::from_f64(handle.store_id().as_raw().get() as f64), + ) + .expect("setting the externref store ID should succeed"); + Self { + value: JsHandle::new(object.into()), + host_data: Some(handle), + } + } + + pub(crate) fn from_js_value(store: &mut impl AsStoreMut, value: JsValue) -> Self { + let host_data = host_handle_from_js_value(store, &value); + Self { + value: JsHandle::new(value), + host_data, + } + } + + pub(crate) fn as_js_value(&self) -> JsValue { + (*self.value).clone() } - pub fn downcast<'a, T>(&self, _store: &'a impl AsStoreRef) -> Option<&'a T> + pub fn downcast<'a, T>(&self, store: &'a impl AsStoreRef) -> Option<&'a T> where T: Any + Send + Sync + 'static + Sized, { - unimplemented!("ExternRef is not yet supported in Javascript"); + self.host_data + .as_ref()? + .get(store.as_store_ref().objects().as_js()) + .0 + .downcast_ref() } pub(crate) fn vm_externref(&self) -> VMExternRef { - unimplemented!("ExternRef is not yet supported in Javascript"); + VMExternRef::new(self.as_js_value()) } pub(crate) unsafe fn from_vm_externref( - _store: &mut impl AsStoreMut, - _vm_externref: VMExternRef, + store: &mut impl AsStoreMut, + vm_externref: VMExternRef, ) -> Self { - unimplemented!("ExternRef is not yet supported in Javascript"); + Self::from_js_value(store, vm_externref.into_js_value()) + } + + pub fn is_from_store(&self, store: &impl AsStoreRef) -> bool { + self.host_data + .as_ref() + .is_none_or(|handle| handle.store_id() == store.as_store_ref().objects().id()) } - pub fn is_from_store(&self, _store: &impl AsStoreRef) -> bool { - true + pub fn ptr_eq(&self, other: &Self) -> bool { + js_sys::Object::is(&self.value, &other.value) } +} + +fn host_ref_index_key() -> Symbol { + Symbol::for_("wasmer.externref-index") +} + +fn host_ref_store_key() -> Symbol { + Symbol::for_("wasmer.externref-store") +} - pub fn ptr_eq(&self, _other: &Self) -> bool { - unimplemented!("ExternRef is not yet supported in Javascript"); +fn host_handle_from_js_value( + store: &mut impl AsStoreMut, + value: &JsValue, +) -> Option> { + let index = js_sys::Reflect::get(value, host_ref_index_key().as_ref()) + .ok()? + .as_f64()? as usize; + let store_id = js_sys::Reflect::get(value, host_ref_store_key().as_ref()) + .ok()? + .as_f64()? as u64; + let objects = store.objects_mut(); + if objects.id().as_raw().get() as u64 != store_id { + return None; } + let internal = crate::js::entities::store::InternalStoreHandle::from_index(index)?; + Some(unsafe { StoreHandle::from_internal(objects.id(), internal) }) } diff --git a/lib/api/src/backend/js/entities/function/mod.rs b/lib/api/src/backend/js/entities/function/mod.rs index 1dd70c27f003..8f6150449498 100644 --- a/lib/api/src/backend/js/entities/function/mod.rs +++ b/lib/api/src/backend/js/entities/function/mod.rs @@ -113,17 +113,20 @@ impl Function { }; let env_mut = AsyncFunctionEnvMut(BackendAsyncFunctionEnvMut::Js(js_env)); - let values = function_type - .params() - .iter() - .enumerate() - .map(|(index, ty)| js_value_to_wasmer(ty, &args.get(index as u32))) - .collect::>(); + let parameter_types = function_type.params().to_vec(); let result_types = function_type.results().to_vec(); + let args = args.clone(); let func = Rc::clone(&func); future_to_promise(async move { - let write_lock = callback_store.write_lock().await; + let mut write_lock = callback_store.write_lock().await; + let values = parameter_types + .iter() + .enumerate() + .map(|(index, ty)| { + js_value_to_wasmer(&mut write_lock, ty, &args.get(index as u32)) + }) + .collect::>(); let store_context = StoreContext::install_async(write_lock.inner); let future = func(env_mut, &values); drop(store_context); @@ -288,13 +291,15 @@ impl Function { let wrapped_func: JsValue = match function_type.results().len() { 0 => Closure::wrap(Box::new(move |args: &Array| { let mut store: StoreMut = unsafe { StoreMut::from_raw(raw_store as _) }; - let env: FunctionEnvMut = raw_env.clone().into_mut(&mut store); let wasm_arguments = function_type .params() .iter() .enumerate() - .map(|(i, param)| js_value_to_wasmer(param, &args.get(i as u32))) + .map(|(i, param)| { + js_value_to_wasmer(&mut store, param, &args.get(i as u32)) + }) .collect::>(); + let env: FunctionEnvMut = raw_env.clone().into_mut(&mut store); let _results = func(env, &wasm_arguments)?; Ok(()) }) @@ -302,13 +307,15 @@ impl Function { .into_js_value(), 1 => Closure::wrap(Box::new(move |args: &Array| { let mut store: StoreMut = unsafe { StoreMut::from_raw(raw_store as _) }; - let env: FunctionEnvMut = raw_env.clone().into_mut(&mut store); let wasm_arguments = function_type .params() .iter() .enumerate() - .map(|(i, param)| js_value_to_wasmer(param, &args.get(i as u32))) + .map(|(i, param)| { + js_value_to_wasmer(&mut store, param, &args.get(i as u32)) + }) .collect::>(); + let env: FunctionEnvMut = raw_env.clone().into_mut(&mut store); let results = func(env, &wasm_arguments)?; Ok(wasmer_value_to_js(&results[0])) }) @@ -316,13 +323,15 @@ impl Function { .into_js_value(), _n => Closure::wrap(Box::new(move |args: &Array| { let mut store: StoreMut = unsafe { StoreMut::from_raw(raw_store as _) }; - let env: FunctionEnvMut = raw_env.clone().into_mut(&mut store); let wasm_arguments = function_type .params() .iter() .enumerate() - .map(|(i, param)| js_value_to_wasmer(param, &args.get(i as u32))) + .map(|(i, param)| { + js_value_to_wasmer(&mut store, param, &args.get(i as u32)) + }) .collect::>(); + let env: FunctionEnvMut = raw_env.clone().into_mut(&mut store); let results = func(env, &wasm_arguments)?; Ok(wasmer_array_to_js_array(&results)) }) @@ -463,7 +472,7 @@ impl Function { match result_types.len() { 0 => Ok(Box::new([])), 1 => { - let value = js_value_to_wasmer(&result_types[0], &result); + let value = js_value_to_wasmer(store, &result_types[0], &result); Ok(vec![value].into_boxed_slice()) } _n => { @@ -471,7 +480,9 @@ impl Function { Ok(result_array .iter() .enumerate() - .map(|(i, js_val)| js_value_to_wasmer(&result_types[i], &js_val)) + .map(|(i, js_val)| { + js_value_to_wasmer(store, &result_types[i], &js_val) + }) .collect::>() .into_boxed_slice()) } @@ -506,9 +517,11 @@ impl Function { drop(store_context); let result = JsFuture::from(promise).await.map_err(RuntimeError::from)?; + let mut write_lock = store.write_lock().await; match function_type.results().len() { 0 => Ok(Box::<[Value]>::default()), 1 => Ok(vec![js_value_to_wasmer( + &mut write_lock, &function_type.results()[0], &result, )] @@ -519,7 +532,13 @@ impl Function { .results() .iter() .enumerate() - .map(|(index, ty)| js_value_to_wasmer(ty, &result.get(index as u32))) + .map(|(index, ty)| { + js_value_to_wasmer( + &mut write_lock, + ty, + &result.get(index as u32), + ) + }) .collect::>() .into_boxed_slice()) } diff --git a/lib/api/src/backend/js/entities/function/typed.rs b/lib/api/src/backend/js/entities/function/typed.rs index 129702f132c3..518a8cdad3d9 100644 --- a/lib/api/src/backend/js/entities/function/typed.rs +++ b/lib/api/src/backend/js/entities/function/typed.rs @@ -104,7 +104,7 @@ macro_rules! impl_native_traits { 0 => {}, 1 => unsafe { let ty = Rets::wasm_types()[0]; - let val = js_value_to_wasmer(&ty, &results); + let val = js_value_to_wasmer(&mut store, &ty, &results); *mut_rets = val.as_raw(&mut store); } _n => { @@ -112,7 +112,7 @@ macro_rules! impl_native_traits { for (i, ret_type) in Rets::wasm_types().iter().enumerate() { let ret = results.get(i as u32); unsafe { - let val = js_value_to_wasmer(&ret_type, &ret); + let val = js_value_to_wasmer(&mut store, &ret_type, &ret); let slot = mut_rets.add(i); *slot = val.as_raw(&mut store); } diff --git a/lib/api/src/backend/js/entities/memory/mod.rs b/lib/api/src/backend/js/entities/memory/mod.rs index 4d8d02b162fa..0dd650d57f0c 100644 --- a/lib/api/src/backend/js/entities/memory/mod.rs +++ b/lib/api/src/backend/js/entities/memory/mod.rs @@ -16,6 +16,12 @@ use crate::{ vm::{VMExtern, VMExternMemory}, }; +// Reserving the multi-gigabyte maximum declared by some WASIX programs can +// prevent browsers from creating the other shared memories used by the SDK. +// 512 MiB is large enough for substantial WASIX workloads while leaving useful +// address-space headroom for the runtime and concurrent processes. +const JS_SHARED_MEMORY_MAXIMUM_PAGES: Pages = Pages(6_144); + #[derive(Debug, Clone, Eq)] pub struct Memory { pub(crate) handle: VMMemory, @@ -37,8 +43,46 @@ unsafe impl Send for Memory {} unsafe impl Sync for Memory {} impl Memory { - pub fn new(store: &mut impl AsStoreMut, ty: MemoryType) -> Result { - let vm_memory = VMMemory::new(Self::js_memory_from_type(&ty)?, ty); + pub fn new(store: &mut impl AsStoreMut, mut ty: MemoryType) -> Result { + if ty.shared + && let Some(maximum) = ty.maximum + && maximum > JS_SHARED_MEMORY_MAXIMUM_PAGES + && ty.minimum <= JS_SHARED_MEMORY_MAXIMUM_PAGES + { + ty.maximum = Some(JS_SHARED_MEMORY_MAXIMUM_PAGES); + tracing::debug!( + requested_maximum = maximum.0, + allocated_maximum = JS_SHARED_MEMORY_MAXIMUM_PAGES.0, + "bounding shared memory growth for the JavaScript backend", + ); + } + let js_memory = match Self::js_memory_from_type(&ty) { + Ok(memory) => memory, + Err(original_error) if ty.shared => { + let Some(requested_maximum) = ty.maximum else { + return Err(original_error); + }; + + let mut allocated = None; + for maximum in smaller_shared_memory_maxima(ty.minimum, requested_maximum) { + let candidate = MemoryType::new(ty.minimum, Some(maximum), true); + if let Ok(memory) = Self::js_memory_from_type(&candidate) { + tracing::debug!( + requested_maximum = requested_maximum.0, + allocated_maximum = maximum.0, + "browser rejected the requested shared memory maximum; using a smaller compatible maximum", + ); + ty = candidate; + allocated = Some(memory); + break; + } + } + + allocated.ok_or(original_error)? + } + Err(error) => return Err(error), + }; + let vm_memory = VMMemory::new(js_memory, ty); Ok(Self::from_vm_extern(store, VMExternMemory::Js(vm_memory))) } @@ -176,6 +220,21 @@ impl Memory { } } +fn smaller_shared_memory_maxima(minimum: Pages, maximum: Pages) -> impl Iterator { + // Shared memories reserve their declared maximum in the browser. Reduce the + // reservation gradually so a child process can still retain a useful heap + // alongside its parent instead of jumping straight to half the capacity. + const STEP: u32 = 1_024; // 64 MiB + let mut candidate = maximum.0; + std::iter::from_fn(move || { + if candidate <= minimum.0 { + return None; + } + candidate = candidate.saturating_sub(STEP).max(minimum.0); + Some(Pages(candidate)) + }) +} + impl std::cmp::PartialEq for Memory { fn eq(&self, other: &Self) -> bool { self.handle == other.handle diff --git a/lib/api/src/backend/js/entities/store/obj.rs b/lib/api/src/backend/js/entities/store/obj.rs index ff840f187bd2..71f024d34ba3 100644 --- a/lib/api/src/backend/js/entities/store/obj.rs +++ b/lib/api/src/backend/js/entities/store/obj.rs @@ -3,7 +3,10 @@ use std::{marker::PhantomData, num::NonZeroUsize}; use wasm_bindgen::JsValue; use wasmer_types::StoreId; -use crate::js::vm::{function::VMFunctionEnvironment, global::VMGlobal}; +use crate::js::{ + entities::external::ExternRefData, + vm::{function::VMFunctionEnvironment, global::VMGlobal}, +}; use super::handle::InternalStoreHandle; @@ -40,6 +43,7 @@ impl_store_object! { // since the other JS objects (table, globals, memory and functions) // live in the JS VM Store by default function_environments => VMFunctionEnvironment, + externrefs => ExternRefData, } /// Set of objects managed by a context. @@ -48,6 +52,7 @@ pub struct StoreObjects { id: StoreId, globals: Vec, function_environments: Vec, + externrefs: Vec, } impl StoreObjects { diff --git a/lib/api/src/backend/js/entities/table.rs b/lib/api/src/backend/js/entities/table.rs index 91f8c562cc0f..e8228ba39b7e 100644 --- a/lib/api/src/backend/js/entities/table.rs +++ b/lib/api/src/backend/js/entities/table.rs @@ -3,8 +3,8 @@ use crate::{ js::vm::{VMFunction, VMTable}, vm::{VMExtern, VMExternTable}, }; -use js_sys::Function; -use wasmer_types::{FunctionType, TableType}; +use wasm_bindgen::{JsCast, JsValue}; +use wasmer_types::{FunctionType, TableType, Type}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Table { @@ -15,20 +15,27 @@ pub struct Table { // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone // unsafe impl Send for Table {} -fn set_table_item(table: &VMTable, item_index: u32, item: &Function) -> Result<(), RuntimeError> { - table.table.set(item_index, item).map_err(|e| e.into()) +fn set_table_item(table: &VMTable, item_index: u32, item: &JsValue) -> Result<(), RuntimeError> { + table + .table + .set_raw(item_index, item) + .map_err(|e| e.into()) } -fn get_function(store: &mut impl AsStoreMut, val: Value) -> Result, RuntimeError> { +fn get_table_item(store: &mut impl AsStoreMut, val: Value) -> Result { if !val.is_from_store(store) { return Err(RuntimeError::new("cannot pass Value across contexts")); } match val { - Value::FuncRef(Some(ref func)) => { - Ok(Some(func.as_js().handle.function.clone().into_inner())) - } - Value::FuncRef(None) => Ok(None), - // Only funcrefs is supported by the spec atm + Value::FuncRef(Some(ref func)) => Ok(func.as_js().handle.function.clone().into_inner().into()), + Value::FuncRef(None) | Value::ExternRef(None) => Ok(JsValue::null()), + Value::ExternRef(Some(ref reference)) => match &reference.0 { + crate::BackendExternRef::Js(reference) => Ok(reference.as_js_value()), + #[allow(unreachable_patterns)] + _ => Err(RuntimeError::new( + "cannot pass an externref across backends", + )), + }, _ => unimplemented!("The {val:?} is not yet supported"), } } @@ -45,19 +52,20 @@ impl Table { if let Some(max) = ty.maximum { js_sys::Reflect::set(&descriptor, &"maximum".into(), &max.into())?; } - js_sys::Reflect::set(&descriptor, &"element".into(), &"anyfunc".into())?; + let element = match ty.ty { + Type::FuncRef => "anyfunc", + Type::ExternRef => "externref", + other => { + return Err(RuntimeError::new(format!( + "{other:?} is not a valid JavaScript table element type" + ))); + } + }; + js_sys::Reflect::set(&descriptor, &"element".into(), &element.into())?; - let js_table = js_sys::WebAssembly::Table::new(&descriptor)?; - // TODO: use `Table.new_with_value` method from wasm-bindgen - // https://github.com/wasm-bindgen/wasm-bindgen/pull/4698 + let initial_value = get_table_item(&mut store, init)?; + let js_table = js_sys::WebAssembly::Table::new_with_value(&descriptor, initial_value)?; let table = VMTable::new(js_table, ty); - let num_elements = table.table.length(); - let func = get_function(&mut store, init)?; - if let Some(func) = func { - for i in 0..num_elements { - set_table_item(&table, i, &func)?; - } - } Ok(Self { handle: table }) } @@ -71,17 +79,32 @@ impl Table { } pub fn get(&self, store: &mut impl AsStoreMut, index: u32) -> Option { - if let Ok(func) = self.handle.table.get(index) { - let ty = VMFunction::type_from_js(&func) - .unwrap_or_else(|| FunctionType::new(vec![], vec![])); - let vm_function = VMFunction::new(func, ty); - let function = crate::Function::from_vm_extern( - store, - crate::vm::VMExternFunction::Js(vm_function), - ); - Some(Value::FuncRef(Some(function))) - } else { - None + let value = self.handle.table.get_raw(index).ok()?; + if value.is_null() { + return Some(match self.handle.ty.ty { + Type::FuncRef => Value::FuncRef(None), + Type::ExternRef => Value::ExternRef(None), + _ => return None, + }); + } + match self.handle.ty.ty { + Type::FuncRef => { + let func = value.dyn_into::().ok()?; + let ty = VMFunction::type_from_js(&func) + .unwrap_or_else(|| FunctionType::new(vec![], vec![])); + let vm_function = VMFunction::new(func, ty); + let function = crate::Function::from_vm_extern( + store, + crate::vm::VMExternFunction::Js(vm_function), + ); + Some(Value::FuncRef(Some(function))) + } + Type::ExternRef => Some(Value::ExternRef(Some(crate::ExternRef( + crate::BackendExternRef::Js( + crate::backend::js::entities::external::ExternRef::from_js_value(store, value), + ), + )))), + _ => None, } } @@ -91,11 +114,8 @@ impl Table { index: u32, val: Value, ) -> Result<(), RuntimeError> { - let item = get_function(store, val)?; - if let Some(item) = item { - set_table_item(&self.handle, index, &item)?; - } - Ok(()) + let item = get_table_item(store, val)?; + set_table_item(&self.handle, index, &item) } pub fn size(&self, _store: &impl AsStoreRef) -> u32 { @@ -108,15 +128,11 @@ impl Table { delta: u32, init: Value, ) -> Result { - // TODO: use `Table.grow_with_value` method from wasm-bindgen - // https://github.com/wasm-bindgen/wasm-bindgen/pull/4698 - let grow_by = self.handle.table.grow(delta)?; - if let Some(func) = get_function(store, init)? { - for i in grow_by..(grow_by + delta) { - set_table_item(&self.handle, i, &func)?; - } - } - Ok(grow_by) + let initial_value = get_table_item(store, init)?; + self.handle + .table + .grow_with_value(delta, initial_value) + .map_err(Into::into) } pub fn copy( diff --git a/lib/api/src/backend/js/utils/convert.rs b/lib/api/src/backend/js/utils/convert.rs index a8c00724f7eb..03a31b69e84a 100644 --- a/lib/api/src/backend/js/utils/convert.rs +++ b/lib/api/src/backend/js/utils/convert.rs @@ -35,7 +35,11 @@ pub trait AsJs: Sized { #[inline] /// Convert a JsValue into a wasmer Value -pub fn js_value_to_wasmer(ty: &Type, js_val: &JsValue) -> Value { +pub fn js_value_to_wasmer( + store: &mut impl AsStoreMut, + ty: &Type, + js_val: &JsValue, +) -> Value { match ty { Type::I32 => Value::I32(js_val.as_f64().unwrap() as _), Type::I64 => Value::I64(if js_val.is_bigint() { @@ -49,7 +53,19 @@ pub fn js_value_to_wasmer(ty: &Type, js_val: &JsValue) -> Value { let big_num: u128 = js_sys::BigInt::from(js_val.clone()).try_into().unwrap(); Value::V128(big_num) } - Type::ExternRef | Type::FuncRef | Type::ExceptionRef => unimplemented!( + Type::ExternRef => { + if js_val.is_null() { + Value::ExternRef(None) + } else { + Value::ExternRef(Some(crate::ExternRef(crate::BackendExternRef::Js( + crate::backend::js::entities::external::ExternRef::from_js_value( + store, + js_val.clone(), + ), + )))) + } + } + Type::FuncRef | Type::ExceptionRef => unimplemented!( "The type `{:?}` is not yet supported in the JS Function API", ty ), @@ -65,6 +81,12 @@ pub fn wasmer_value_to_js(val: &Value) -> JsValue { Value::F32(f) => JsValue::from_f64(*f as _), Value::F64(f) => JsValue::from_f64(*f), Value::V128(f) => JsValue::from_f64(*f as _), + Value::ExternRef(Some(reference)) => match &reference.0 { + crate::BackendExternRef::Js(reference) => reference.as_js_value(), + #[allow(unreachable_patterns)] + _ => unreachable!("a JS function received an externref from another backend"), + }, + Value::ExternRef(None) => JsValue::null(), val => unimplemented!( "The value `{:?}` is not yet supported in the JS Function API", val @@ -84,9 +106,12 @@ impl AsJs for Value { Self::V128(v) => JsValue::from(*v), Self::FuncRef(Some(func)) => func.as_js().handle.function.clone().into(), Self::FuncRef(None) => JsValue::null(), - Self::ExternRef(_) => { - unimplemented!("ExternRefs are not yet supported in the JS Function API",) - } + Self::ExternRef(Some(reference)) => match &reference.0 { + crate::BackendExternRef::Js(reference) => reference.as_js_value(), + #[allow(unreachable_patterns)] + _ => unreachable!("a JS function received an externref from another backend"), + }, + Self::ExternRef(None) => JsValue::null(), Self::ExceptionRef(_) => { unimplemented!("ExceptionRefs are not yet supported in the JS Function API",) } @@ -94,11 +119,11 @@ impl AsJs for Value { } fn from_jsvalue( - _store: &mut impl AsStoreMut, + store: &mut impl AsStoreMut, type_: &Self::DefinitionType, value: &JsValue, ) -> Result { - Ok(js_value_to_wasmer(type_, value)) + Ok(js_value_to_wasmer(store, type_, value)) } } diff --git a/lib/api/src/backend/js/vm/external.rs b/lib/api/src/backend/js/vm/external.rs index ec324cdda56c..063d9b859693 100644 --- a/lib/api/src/backend/js/vm/external.rs +++ b/lib/api/src/backend/js/vm/external.rs @@ -1,4 +1,7 @@ use wasmer_types::RawValue; +use wasm_bindgen::JsValue; + +use crate::js::utils::js_handle::JsHandle; use crate::{AsStoreMut, Extern, VMExternToExtern}; @@ -50,9 +53,25 @@ impl VMExternToExtern for VMExtern { } /// A reference to an external value in the `js` VM. -pub struct VMExternRef; +#[derive(Debug, Clone)] +pub struct VMExternRef { + value: JsHandle, +} + +unsafe impl Send for VMExternRef {} +unsafe impl Sync for VMExternRef {} impl VMExternRef { + pub(crate) fn new(value: JsValue) -> Self { + Self { + value: JsHandle::new(value), + } + } + + pub(crate) fn into_js_value(self) -> JsValue { + self.value.into_inner() + } + /// Converts the `VMExternRef` into a `RawValue`. pub fn into_raw(self) -> RawValue { unimplemented!(); diff --git a/lib/api/src/utils/polyfill.rs b/lib/api/src/utils/polyfill.rs index a8acaad3535c..082b47ca73a5 100644 --- a/lib/api/src/utils/polyfill.rs +++ b/lib/api/src/utils/polyfill.rs @@ -375,7 +375,6 @@ pub fn translate_module(data: &[u8]) -> WasmResult { .info .validate_signature_hashes() .map_err(|err| err.to_string())?; - validate_exported_types(&module_info)?; Ok(module_info) } @@ -476,53 +475,6 @@ fn parse_init_expr( Ok(InitExpr::new(operations.into_boxed_slice())) } -fn validate_exported_types(module_info: &ModuleInfoPolyfill) -> WasmResult<()> { - for (name, export) in module_info.info.exports.iter() { - let uses_externref = match export { - ExportIndex::Function(index) => { - let signature = module_info.info.functions.get(*index).ok_or_else(|| { - format!("function export `{name}` references unknown function {index:?}") - })?; - let ty = module_info.info.signatures.get(*signature).ok_or_else(|| { - format!("function export `{name}` references unknown signature {signature:?}") - })?; - ty.params() - .iter() - .chain(ty.results().iter()) - .any(|ty| *ty == Type::ExternRef) - } - ExportIndex::Table(index) => { - let ty = module_info.info.tables.get(*index).ok_or_else(|| { - format!("table export `{name}` references unknown table {index:?}") - })?; - ty.ty == Type::ExternRef - } - ExportIndex::Memory(_) => false, - ExportIndex::Global(index) => { - let ty = module_info.info.globals.get(*index).ok_or_else(|| { - format!("global export `{name}` references unknown global {index:?}") - })?; - ty.ty == Type::ExternRef - } - ExportIndex::Tag(index) => { - let signature = module_info.info.tags.get(*index).ok_or_else(|| { - format!("tag export `{name}` references unknown tag {index:?}") - })?; - let ty = module_info.info.signatures.get(*signature).ok_or_else(|| { - format!("tag export `{name}` references unknown signature {signature:?}") - })?; - ty.params().contains(&Type::ExternRef) - } - }; - - if uses_externref { - return Err("ExternRef is not supported by this backend yet".to_string()); - } - } - - Ok(()) -} - /// Helper function translating wasmparser types to Wasm Type. pub fn wptype_to_type(ty: wasmparser::ValType) -> WasmResult { match ty { From cfeadfc2ab80d076c74e317ceca7072fd3eff42e Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Tue, 4 Aug 2026 17:49:37 -0700 Subject: [PATCH 25/32] fix(api-js-backend): add copy_range_to_memory to the MemoryViews --- lib/api/src/backend/js/entities/memory/mod.rs | 2 +- .../src/backend/js/entities/memory/view.rs | 33 +++++++--- .../src/backend/sys/entities/memory/view.rs | 35 ++++++++--- .../src/backend/v8/entities/memory/view.rs | 35 ++++++++--- lib/api/src/entities/memory/view/inner.rs | 50 +++++++++++++-- lib/api/src/entities/memory/view/mod.rs | 13 ++++ lib/c-api-imports/src/lib.rs | 61 ++++++++++--------- 7 files changed, 167 insertions(+), 62 deletions(-) diff --git a/lib/api/src/backend/js/entities/memory/mod.rs b/lib/api/src/backend/js/entities/memory/mod.rs index 0dd650d57f0c..7afd344ece47 100644 --- a/lib/api/src/backend/js/entities/memory/mod.rs +++ b/lib/api/src/backend/js/entities/memory/mod.rs @@ -20,7 +20,7 @@ use crate::{ // prevent browsers from creating the other shared memories used by the SDK. // 512 MiB is large enough for substantial WASIX workloads while leaving useful // address-space headroom for the runtime and concurrent processes. -const JS_SHARED_MEMORY_MAXIMUM_PAGES: Pages = Pages(6_144); +const JS_SHARED_MEMORY_MAXIMUM_PAGES: Pages = Pages(16_384); #[derive(Debug, Clone, Eq)] pub struct Memory { diff --git a/lib/api/src/backend/js/entities/memory/view.rs b/lib/api/src/backend/js/entities/memory/view.rs index 628af3958ec0..bde971930fe6 100644 --- a/lib/api/src/backend/js/entities/memory/view.rs +++ b/lib/api/src/backend/js/entities/memory/view.rs @@ -268,17 +268,32 @@ impl<'a> MemoryView<'a> { /// Copies the memory to another new memory object pub fn copy_to_memory(&self, amount: u64, new_memory: &Self) -> Result<(), MemoryAccessError> { - let mut offset = 0; - let mut chunk = [0u8; 40960]; - while offset < amount { - let remaining = amount - offset; - let sublen = remaining.min(chunk.len() as u64) as usize; - self.read(offset, &mut chunk[..sublen])?; - - new_memory.write(offset, &chunk[..sublen])?; + self.copy_range_to_memory(0, 0, amount, new_memory) + } - offset += sublen as u64; + /// Copies a memory range directly between JavaScript WebAssembly memories. + pub(crate) fn copy_range_to_memory( + &self, + source_offset: u64, + target_offset: u64, + amount: u64, + new_memory: &Self, + ) -> Result<(), MemoryAccessError> { + let source_offset = u32::try_from(source_offset).map_err(|_| MemoryAccessError::Overflow)?; + let target_offset = u32::try_from(target_offset).map_err(|_| MemoryAccessError::Overflow)?; + let amount = u32::try_from(amount).map_err(|_| MemoryAccessError::Overflow)?; + let source_end = source_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + let target_end = target_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + if source_end > self.view.length() || target_end > new_memory.view.length() { + return Err(MemoryAccessError::HeapOutOfBounds); } + + let source = self.view.subarray(source_offset, source_end); + new_memory.view.set(&source, target_offset); Ok(()) } } diff --git a/lib/api/src/backend/sys/entities/memory/view.rs b/lib/api/src/backend/sys/entities/memory/view.rs index e68c9b81e587..8ccf7bb98383 100644 --- a/lib/api/src/backend/sys/entities/memory/view.rs +++ b/lib/api/src/backend/sys/entities/memory/view.rs @@ -192,16 +192,35 @@ impl<'a> MemoryView<'a> { #[allow(unused)] /// Copies the memory to another new memory object pub fn copy_to_memory(&self, amount: u64, new_memory: &Self) -> Result<(), MemoryAccessError> { - let mut offset = 0; - let mut chunk = [0u8; 40960]; - while offset < amount { - let remaining = amount - offset; - let sublen = remaining.min(chunk.len() as u64) as usize; - self.read(offset, &mut chunk[..sublen])?; + self.copy_range_to_memory(0, 0, amount, new_memory) + } - new_memory.write(offset, &chunk[..sublen])?; + pub(crate) fn copy_range_to_memory( + &self, + source_offset: u64, + target_offset: u64, + amount: u64, + new_memory: &Self, + ) -> Result<(), MemoryAccessError> { + let source_offset = usize::try_from(source_offset).map_err(|_| MemoryAccessError::Overflow)?; + let target_offset = usize::try_from(target_offset).map_err(|_| MemoryAccessError::Overflow)?; + let amount = usize::try_from(amount).map_err(|_| MemoryAccessError::Overflow)?; + let source_end = source_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + let target_end = target_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + if source_end > self.buffer.len || target_end > new_memory.buffer.len { + return Err(MemoryAccessError::HeapOutOfBounds); + } - offset += sublen as u64; + unsafe { + std::ptr::copy( + self.buffer.base.add(source_offset), + new_memory.buffer.base.add(target_offset), + amount, + ); } Ok(()) } diff --git a/lib/api/src/backend/v8/entities/memory/view.rs b/lib/api/src/backend/v8/entities/memory/view.rs index 7b8ed8dd66ce..1be09975b983 100644 --- a/lib/api/src/backend/v8/entities/memory/view.rs +++ b/lib/api/src/backend/v8/entities/memory/view.rs @@ -187,16 +187,35 @@ impl<'a> MemoryView<'a> { #[allow(unused)] /// Copies the memory to another new memory object pub fn copy_to_memory(&self, amount: u64, new_memory: &Self) -> Result<(), MemoryAccessError> { - let mut offset = 0; - let mut chunk = [0u8; 40960]; - while offset < amount { - let remaining = amount - offset; - let sublen = remaining.min(chunk.len() as u64) as usize; - self.read(offset, &mut chunk[..sublen])?; + self.copy_range_to_memory(0, 0, amount, new_memory) + } - new_memory.write(offset, &chunk[..sublen])?; + pub(crate) fn copy_range_to_memory( + &self, + source_offset: u64, + target_offset: u64, + amount: u64, + new_memory: &Self, + ) -> Result<(), MemoryAccessError> { + let source_offset = usize::try_from(source_offset).map_err(|_| MemoryAccessError::Overflow)?; + let target_offset = usize::try_from(target_offset).map_err(|_| MemoryAccessError::Overflow)?; + let amount = usize::try_from(amount).map_err(|_| MemoryAccessError::Overflow)?; + let source_end = source_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + let target_end = target_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + if source_end > self.buffer.len || target_end > new_memory.buffer.len { + return Err(MemoryAccessError::HeapOutOfBounds); + } - offset += sublen as u64; + unsafe { + std::ptr::copy( + self.buffer.base.add(source_offset), + new_memory.buffer.base.add(target_offset), + amount, + ); } Ok(()) } diff --git a/lib/api/src/entities/memory/view/inner.rs b/lib/api/src/entities/memory/view/inner.rs index bab183e83398..e55f30d3913d 100644 --- a/lib/api/src/entities/memory/view/inner.rs +++ b/lib/api/src/entities/memory/view/inner.rs @@ -222,16 +222,54 @@ impl<'a> BackendMemoryView<'a> { /// Copies the memory to another new memory object #[inline] pub fn copy_to_memory(&self, amount: u64, new_memory: &Self) -> Result<(), MemoryAccessError> { - let mut offset = 0; + self.copy_range_to_memory(0, 0, amount, new_memory) + } + + #[inline] + #[allow(unreachable_patterns)] + pub fn copy_range_to_memory( + &self, + source_offset: u64, + target_offset: u64, + amount: u64, + new_memory: &Self, + ) -> Result<(), MemoryAccessError> { + match (self, new_memory) { + #[cfg(feature = "sys")] + (Self::Sys(source), Self::Sys(target)) => { + return source.copy_range_to_memory(source_offset, target_offset, amount, target); + } + #[cfg(feature = "v8")] + (Self::V8(source), Self::V8(target)) => { + return source.copy_range_to_memory(source_offset, target_offset, amount, target); + } + #[cfg(feature = "js")] + (Self::Js(source), Self::Js(target)) => { + return source.copy_range_to_memory(source_offset, target_offset, amount, target); + } + _ => {} + } + + let source_end = source_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + let target_end = target_offset + .checked_add(amount) + .ok_or(MemoryAccessError::Overflow)?; + if source_end > self.data_size() || target_end > new_memory.data_size() { + return Err(MemoryAccessError::HeapOutOfBounds); + } + + let mut copied = 0; let mut chunk = [0u8; 40960]; - while offset < amount { - let remaining = amount - offset; + while copied < amount { + let remaining = amount - copied; let sublen = remaining.min(chunk.len() as u64) as usize; - self.read(offset, &mut chunk[..sublen])?; + self.read(source_offset + copied, &mut chunk[..sublen])?; - new_memory.write(offset, &chunk[..sublen])?; + new_memory.write(target_offset + copied, &chunk[..sublen])?; - offset += sublen as u64; + copied += sublen as u64; } Ok(()) } diff --git a/lib/api/src/entities/memory/view/mod.rs b/lib/api/src/entities/memory/view/mod.rs index 8f16105dc53c..dfc3e097d6ba 100644 --- a/lib/api/src/entities/memory/view/mod.rs +++ b/lib/api/src/entities/memory/view/mod.rs @@ -152,4 +152,17 @@ impl<'a> MemoryView<'a> { pub fn copy_to_memory(&self, amount: u64, new_memory: &Self) -> Result<(), MemoryAccessError> { self.0.copy_to_memory(amount, &new_memory.0) } + + /// Copies a range directly to another memory view. + #[doc(hidden)] + pub fn copy_range_to_memory( + &self, + source_offset: u64, + target_offset: u64, + amount: u64, + new_memory: &Self, + ) -> Result<(), MemoryAccessError> { + self.0 + .copy_range_to_memory(source_offset, target_offset, amount, &new_memory.0) + } } diff --git a/lib/c-api-imports/src/lib.rs b/lib/c-api-imports/src/lib.rs index 5a0a95f4df02..ded815ba18c6 100644 --- a/lib/c-api-imports/src/lib.rs +++ b/lib/c-api-imports/src/lib.rs @@ -1,7 +1,5 @@ use anyhow::{Context, Result, bail}; -use std::{ - collections::HashMap, fmt::Display, mem::size_of, num::NonZeroI32, ptr, slice, sync::Arc, -}; +use std::{collections::HashMap, fmt::Display, mem::size_of, num::NonZeroI32, slice, sync::Arc}; use wasmer_api::{ Extern, ExternRef, ExternType, Function, Function as WasmerFunction, FunctionEnv, @@ -676,11 +674,20 @@ fn allocate_guest_memory(env: &mut FunctionEnvMut, len: usize) -> O return Some(0); } - let malloc_fn = env.data().malloc_fn.clone()?; - let len = i32::try_from(len).ok()?; + let Some(malloc_fn) = env.data().malloc_fn.clone() else { + return None; + }; + let Ok(len) = i32::try_from(len) else { + return None; + }; let guest_ptr: i32 = { let (_, mut store_ref) = env.data_and_store_mut(); - malloc_fn.call(&mut store_ref, len).ok()? + match malloc_fn.call(&mut store_ref, len) { + Ok(ptr) => ptr, + Err(error) => { + return None; + } + } }; if guest_ptr <= INVALID_HANDLE { return None; @@ -1336,20 +1343,17 @@ fn copy_wasmer_memory_to_guest( let Some(guest_offset) = checked_memory_offset(guest_ptr, len, guest_view.data_size()) else { return false; }; - let source_base = source_view.data_ptr(); - let guest_base = guest_view.data_ptr(); - if ptr::eq(source_base, guest_base) { + if memory == &guest_memory { return false; } - unsafe { - // Both ranges are bounds-checked above and same-memory copies are rejected. - ptr::copy_nonoverlapping( - source_base.add(source_offset), - guest_base.add(guest_offset), - len, - ); - } - true + source_view + .copy_range_to_memory( + source_offset as u64, + guest_offset as u64, + len as u64, + &guest_view, + ) + .is_ok() } fn copy_guest_memory_to_wasmer( @@ -1372,20 +1376,17 @@ fn copy_guest_memory_to_wasmer( let Some(target_offset) = checked_memory_offset(0, len, target_view.data_size()) else { return false; }; - let guest_base = guest_view.data_ptr(); - let target_base = target_view.data_ptr(); - if ptr::eq(guest_base, target_base) { + if memory == &guest_memory { return false; } - unsafe { - // Both ranges are bounds-checked above and same-memory copies are rejected. - ptr::copy_nonoverlapping( - guest_base.add(guest_offset), - target_base.add(target_offset), - len, - ); - } - true + guest_view + .copy_range_to_memory( + guest_offset as u64, + target_offset as u64, + len as u64, + &target_view, + ) + .is_ok() } fn wasm_memory_data_size(env: FunctionEnvMut, memory_handle: i32) -> i32 { From a537593511a89d9697eda6cd8244d2c084730c1b Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 5 Aug 2026 09:53:50 -0700 Subject: [PATCH 26/32] fix(wasix): fix moving a directory preservers extternal hard link backing paths --- lib/wasix/src/syscalls/wasi/path_rename.rs | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/wasix/src/syscalls/wasi/path_rename.rs b/lib/wasix/src/syscalls/wasi/path_rename.rs index f6517c3ea4a9..7df179ef7b54 100644 --- a/lib/wasix/src/syscalls/wasi/path_rename.rs +++ b/lib/wasix/src/syscalls/wasi/path_rename.rs @@ -1,7 +1,5 @@ use std::path::PathBuf; -use anyhow::Context; - use super::*; use crate::syscalls::*; @@ -341,11 +339,42 @@ fn rename_inode_tree(inode: &InodeGuard, source_dir_path: &Path, target_dir_path fn adjust_path(path: &Path, source_dir_path: &Path, target_dir_path: &Path) -> PathBuf { let path = crate::fs::PosixPath::from_path(path); let source_dir_path = crate::fs::PosixPath::from_path(source_dir_path); - let relative_path = path - .strip_prefix(&source_dir_path) - .with_context(|| format!("Expected path {path:?} to be a subpath of {source_dir_path:?}")) - .expect("Fatal filesystem error"); + let Some(relative_path) = path.strip_prefix(&source_dir_path) else { + // A directory entry created by path_link may retain the backing path + // of its source when the mounted filesystem cannot materialize hard + // links. Moving its parent must move the entry, not that shared + // backing file. + return PathBuf::from(path.as_str()); + }; crate::fs::PosixPath::from_path(target_dir_path) .join(&relative_path) .into_path_buf() } + +#[cfg(test)] +mod tests { + use super::adjust_path; + use std::path::Path; + + #[test] + fn moving_a_directory_preserves_external_hard_link_backing_paths() { + let adjusted = adjust_path( + Path::new("/store/files/content"), + Path::new("/workspace/package_tmp"), + Path::new("/workspace/package"), + ); + + assert_eq!(adjusted, Path::new("/store/files/content")); + } + + #[test] + fn moving_a_directory_rebases_ordinary_descendants() { + let adjusted = adjust_path( + Path::new("/workspace/package_tmp/lib/index.js"), + Path::new("/workspace/package_tmp"), + Path::new("/workspace/package"), + ); + + assert_eq!(adjusted, Path::new("/workspace/package/lib/index.js")); + } +} From 27ed718e8b365a07fba4fbcc272101780633fb32 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 5 Aug 2026 10:23:56 -0700 Subject: [PATCH 27/32] fix(wasix, import apis): Added a prepare_imports to the runtime hooks to simplify usage --- lib/c-api-imports/src/wasix.rs | 58 +++++++ lib/cli/src/commands/run/runtime.rs | 9 ++ lib/wasix/src/runtime/mod.rs | 166 +++++++++++++++++--- lib/wasix/src/state/env.rs | 17 +- lib/wasix/src/state/linker/runtime_hooks.rs | 22 +-- 5 files changed, 218 insertions(+), 54 deletions(-) diff --git a/lib/c-api-imports/src/wasix.rs b/lib/c-api-imports/src/wasix.rs index 0ef06a1915ca..a7ec038cfc6b 100644 --- a/lib/c-api-imports/src/wasix.rs +++ b/lib/c-api-imports/src/wasix.rs @@ -18,6 +18,16 @@ impl InstantiationHook for WasmCapiRuntimeHooks { Ok((imports, InstantiationState::new(state))) } + fn prepare_imports( + &self, + module: &Module, + store: &mut StoreMut, + imports: &mut Imports, + ) -> Result { + let state = WasmCapiRuntimeHooks::add_imports(self, module, store, imports)?; + Ok(InstantiationState::new(state)) + } + fn configure_new_instance( &self, module: &Module, @@ -39,3 +49,51 @@ impl InstantiationHook for WasmCapiRuntimeHooks { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use wasmer_api::{AsStoreMut, MemoryType, Pages, Store}; + + #[test] + fn prepare_imports_reuses_wasix_memory_before_start() { + let mut store = Store::default(); + let module = Module::new( + &store, + r#"(module + (import "env" "memory" (memory 1 1)) + (import "wasm_c_api_v0" "wasm_byte_vec_new" + (func $wasm_byte_vec_new (param i32 i32 i32))) + (data (i32.const 16) "\ff\ff\ff\ff\ff\ff\ff\ff") + (func $initialize + (call $wasm_byte_vec_new + (i32.const 16) + (i32.const 0) + (i32.const 0))) + (start $initialize) + )"#, + ) + .expect("module compiles"); + let memory = Memory::new(&mut store, MemoryType::new(Pages(1), Some(Pages(1)), false)) + .expect("WASIX memory can be created"); + let mut imports = Imports::new(); + imports.define("env", "memory", memory.clone()); + + let hooks = WasmCapiRuntimeHooks::new(); + let _state = InstantiationHook::prepare_imports( + &hooks, + &module, + &mut store.as_store_mut(), + &mut imports, + ) + .expect("C API imports can be prepared"); + Instance::new(&mut store, &module, &imports).expect("start function succeeds"); + + let mut header = [0xff; 8]; + memory + .view(&store) + .read(16, &mut header) + .expect("vector header can be read"); + assert_eq!(header, [0; 8]); + } +} diff --git a/lib/cli/src/commands/run/runtime.rs b/lib/cli/src/commands/run/runtime.rs index 45e7458e67b3..67317f80e9ae 100644 --- a/lib/cli/src/commands/run/runtime.rs +++ b/lib/cli/src/commands/run/runtime.rs @@ -101,6 +101,15 @@ impl wasmer_wasix::Runtime for Monitorin self.runtime.additional_imports(module, store) } + fn prepare_imports( + &self, + module: &Module, + store: &mut wasmer::StoreMut, + imports: &mut wasmer::Imports, + ) -> anyhow::Result { + self.runtime.prepare_imports(module, store, imports) + } + fn configure_new_instance( &self, module: &Module, diff --git a/lib/wasix/src/runtime/mod.rs b/lib/wasix/src/runtime/mod.rs index 3b813fb08e32..e8d37126888d 100644 --- a/lib/wasix/src/runtime/mod.rs +++ b/lib/wasix/src/runtime/mod.rs @@ -40,7 +40,7 @@ use crate::{ }; /// Opaque per-instantiation state, created by -/// [`InstantiationHook::additional_imports`] and handed back to +/// [`InstantiationHook::prepare_imports`] and handed back to /// [`InstantiationHook::configure_new_instance`] for the instance built with /// those imports. /// @@ -108,7 +108,7 @@ impl fmt::Debug for InstantiationState { /// instance the runtime creates (process bootstrap, thread spawn, dynamically /// linked side module). /// -/// Both methods have a no-op default, so an implementation only needs to +/// Every method has a compatible default, so an implementation only needs to /// provide the phases it cares about. // Keeping both phases on one trait is what lets each hook route its own // InstantiationState from its import phase to its own setup phase when @@ -129,10 +129,29 @@ pub trait InstantiationHook: fmt::Debug + Send + Sync + 'static { Ok((wasmer::Imports::new(), InstantiationState::empty())) } - /// Configures an instantiated instance before initialization/startup. + /// Prepares the complete import object immediately before instantiation. + /// + /// The default implementation preserves the original + /// [`InstantiationHook::additional_imports`] contract by merging its + /// returned imports without replacing imports already supplied by WASIX. + /// Hooks that need to inspect or reuse an existing import, such as an + /// imported memory, can override this method and update `imports` in place. + /// Overrides must preserve existing entries. + fn prepare_imports( + &self, + module: &wasmer::Module, + store: &mut wasmer::StoreMut, + imports: &mut wasmer::Imports, + ) -> anyhow::Result { + let (additional_imports, state) = self.additional_imports(module, store)?; + merge_missing_imports(imports, &additional_imports); + Ok(state) + } + + /// Configures an instance after successful instantiation and startup. /// /// `state` is the [`InstantiationState`] this hook returned from the - /// [`InstantiationHook::additional_imports`] call whose imports the + /// [`InstantiationHook::prepare_imports`] call whose imports the /// instance was created with. fn configure_new_instance( &self, @@ -156,6 +175,15 @@ impl InstantiationHook for Arc { (**self).additional_imports(module, store) } + fn prepare_imports( + &self, + module: &wasmer::Module, + store: &mut wasmer::StoreMut, + imports: &mut wasmer::Imports, + ) -> anyhow::Result { + (**self).prepare_imports(module, store, imports) + } + fn configure_new_instance( &self, module: &wasmer::Module, @@ -370,10 +398,27 @@ where Ok((wasmer::Imports::new(), InstantiationState::empty())) } - /// Configure an instantiated instance before initialization/startup. + /// Prepare the complete import object immediately before instantiation. + /// + /// Existing implementations can continue overriding + /// [`Runtime::additional_imports`]. Runtimes that need to inspect or reuse + /// imports created by WASIX can override this method instead. Overrides + /// must preserve existing entries. + fn prepare_imports( + &self, + module: &wasmer::Module, + store: &mut wasmer::StoreMut, + imports: &mut wasmer::Imports, + ) -> anyhow::Result { + let (additional_imports, state) = self.additional_imports(module, store)?; + merge_missing_imports(imports, &additional_imports); + Ok(state) + } + + /// Configure an instance after successful instantiation and startup. /// /// `state` must be the [`InstantiationState`] returned by the - /// [`Runtime::additional_imports`] call whose imports this instance was + /// [`Runtime::prepare_imports`] call whose imports this instance was /// created with. fn configure_new_instance( &self, @@ -767,21 +812,34 @@ impl PluggableRuntime { } } -/// Runs the import phase of `hooks`, returning the merged imports and the -/// per-hook states, aligned by index with `hooks`. +fn merge_missing_imports(imports: &mut wasmer::Imports, additional_imports: &wasmer::Imports) { + for (namespace, name, value) in additional_imports.iter() { + if imports.exists(namespace, name) { + tracing::warn!( + "Skipping duplicate additional import {}.{}", + namespace, + name + ); + } else { + imports.define(namespace, name, value.clone()); + } + } +} + +/// Runs the import phase of `hooks`, updating the complete import object and +/// returning the per-hook states aligned by index with `hooks`. fn run_import_hooks( hooks: &[Arc], module: &wasmer::Module, store: &mut wasmer::StoreMut, -) -> anyhow::Result<(wasmer::Imports, Vec)> { - let mut imports = wasmer::Imports::new(); + imports: &mut wasmer::Imports, +) -> anyhow::Result> { let mut states = Vec::with_capacity(hooks.len()); for hook in hooks { - let (hook_imports, state) = hook.additional_imports(module, store)?; - imports.extend(&hook_imports); + let state = hook.prepare_imports(module, store, imports)?; states.push(state); } - Ok((imports, states)) + Ok(states) } /// Composite state used by [`OverriddenRuntime`] to carry the inner @@ -859,10 +917,24 @@ impl Runtime for PluggableRuntime { if self.instantiation_hooks.is_empty() { return Ok((wasmer::Imports::new(), InstantiationState::empty())); } - let (imports, states) = run_import_hooks(&self.instantiation_hooks, module, store)?; + let mut imports = wasmer::Imports::new(); + let states = run_import_hooks(&self.instantiation_hooks, module, store, &mut imports)?; Ok((imports, InstantiationState::new(states))) } + fn prepare_imports( + &self, + module: &wasmer::Module, + store: &mut wasmer::StoreMut, + imports: &mut wasmer::Imports, + ) -> anyhow::Result { + if self.instantiation_hooks.is_empty() { + return Ok(InstantiationState::empty()); + } + let states = run_import_hooks(&self.instantiation_hooks, module, store, imports)?; + Ok(InstantiationState::new(states)) + } + fn configure_new_instance( &self, module: &wasmer::Module, @@ -876,7 +948,7 @@ impl Runtime for PluggableRuntime { } let states = state .take::>() - .context("invalid instance setup state from additional_imports")?; + .context("invalid instance setup state from import preparation")?; run_setup_hooks( &self.instantiation_hooks, states, @@ -1099,8 +1171,7 @@ impl Runtime for OverriddenRuntime { if self.instantiation_hooks.is_empty() && inner_state.is_empty() { return Ok((imports, InstantiationState::empty())); } - let (own_imports, own_states) = run_import_hooks(&self.instantiation_hooks, module, store)?; - imports.extend(&own_imports); + let own_states = run_import_hooks(&self.instantiation_hooks, module, store, &mut imports)?; Ok(( imports, InstantiationState::new(OverriddenInstantiationState { @@ -1110,6 +1181,23 @@ impl Runtime for OverriddenRuntime { )) } + fn prepare_imports( + &self, + module: &wasmer::Module, + store: &mut wasmer::StoreMut, + imports: &mut wasmer::Imports, + ) -> anyhow::Result { + let inner_state = self.inner.prepare_imports(module, store, imports)?; + if self.instantiation_hooks.is_empty() && inner_state.is_empty() { + return Ok(InstantiationState::empty()); + } + let own_states = run_import_hooks(&self.instantiation_hooks, module, store, imports)?; + Ok(InstantiationState::new(OverriddenInstantiationState { + inner: inner_state, + own: own_states, + })) + } + fn configure_new_instance( &self, module: &wasmer::Module, @@ -1121,7 +1209,7 @@ impl Runtime for OverriddenRuntime { let state = if state.is_empty() { anyhow::ensure!( self.instantiation_hooks.is_empty(), - "missing instance setup state from additional_imports" + "missing instance setup state from import preparation" ); OverriddenInstantiationState { inner: InstantiationState::empty(), @@ -1130,7 +1218,7 @@ impl Runtime for OverriddenRuntime { } else { state .take::() - .context("invalid instance setup state from additional_imports")? + .context("invalid instance setup state from import preparation")? }; self.inner .configure_new_instance(module, store, instance, imported_memory, state.inner)?; @@ -1190,7 +1278,45 @@ impl Runtime for OverriddenRuntime { #[cfg(test)] mod tests { - use super::InstantiationState; + use super::{InstantiationHook, InstantiationState}; + use wasmer::{AsStoreMut, Extern, Global, Imports, Module, Store, Value}; + + #[derive(Debug)] + struct LegacyImportsHook; + + impl InstantiationHook for LegacyImportsHook { + fn additional_imports( + &self, + _module: &Module, + store: &mut wasmer::StoreMut, + ) -> anyhow::Result<(Imports, InstantiationState)> { + let mut imports = Imports::new(); + imports.define("host", "existing", Global::new(store, Value::I32(2))); + imports.define("host", "added", Global::new(store, Value::I32(3))); + Ok((imports, InstantiationState::empty())) + } + } + + #[test] + fn prepare_imports_preserves_the_legacy_additional_imports_contract() { + let mut store = Store::default(); + let module = Module::new(&store, "(module)").unwrap(); + let mut imports = Imports::new(); + imports.define("host", "existing", Global::new(&mut store, Value::I32(1))); + + LegacyImportsHook + .prepare_imports(&module, &mut store.as_store_mut(), &mut imports) + .unwrap(); + + let Some(Extern::Global(existing)) = imports.get_export("host", "existing") else { + panic!("existing import is not a global"); + }; + let Some(Extern::Global(added)) = imports.get_export("host", "added") else { + panic!("added import is not a global"); + }; + assert_eq!(existing.get(&mut store), Value::I32(1)); + assert_eq!(added.get(&mut store), Value::I32(3)); + } #[test] fn instantiation_state_round_trips_the_hook_data() { diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index a26af6b03627..a13e32d6e817 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -574,23 +574,10 @@ impl WasiEnv { import_object.define("env", "memory", memory); } let runtime = func_env.data(&store).runtime.clone(); - let (additional_imports, instantiation_state) = runtime - .additional_imports(&module, &mut store) + let instantiation_state = runtime + .prepare_imports(&module, &mut store, &mut import_object) .map_err(|err| WasiThreadError::AdditionalImportCreationFailed(Arc::new(err)))?; - for ((namespace, name), value) in &additional_imports { - // Downstream runtime imports must not override WASIX imports. - if import_object.exists(&namespace, &name) { - tracing::warn!( - "Skipping duplicate additional import {}.{}", - namespace, - name - ); - } else { - import_object.define(&namespace, &name, value); - } - } - let imported_memory = import_object .get_export("env", "memory") .and_then(|ext| match ext { diff --git a/lib/wasix/src/state/linker/runtime_hooks.rs b/lib/wasix/src/state/linker/runtime_hooks.rs index c9f81be95321..0e92a936847a 100644 --- a/lib/wasix/src/state/linker/runtime_hooks.rs +++ b/lib/wasix/src/state/linker/runtime_hooks.rs @@ -1,4 +1,3 @@ -use tracing::warn; use wasmer::{AsStoreMut, FunctionEnv, Imports, Instance, Memory, Module}; use crate::WasiEnv; @@ -16,11 +15,9 @@ pub(super) fn instantiate_with_runtime_hooks( let instantiation_state = { let mut store_mut = store.as_store_mut(); - let (additional_imports, instantiation_state) = runtime - .additional_imports(module, &mut store_mut) - .map_err(LinkError::RuntimeHookError)?; - merge_missing_imports(imports, &additional_imports); - instantiation_state + runtime + .prepare_imports(module, &mut store_mut, imports) + .map_err(LinkError::RuntimeHookError)? }; let instance = Instance::new(store, module, imports)?; @@ -40,16 +37,3 @@ pub(super) fn instantiate_with_runtime_hooks( Ok(instance) } - -fn merge_missing_imports(imports: &mut Imports, additional_imports: &Imports) { - for (namespace, name, value) in additional_imports.iter() { - if imports.exists(namespace, name) { - warn!( - "Skipping duplicate additional import {}.{}", - namespace, name - ); - } else { - imports.define(namespace, name, value.clone()); - } - } -} From 6fadc92390f4dd6b9754626de5d032a4d7c16e17 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 5 Aug 2026 14:47:55 -0700 Subject: [PATCH 28/32] fix(api-js-backend): Properly annotate all tables, not just imported ones --- lib/api/src/backend/js/entities/module.rs | 37 ++++++++++++++--------- lib/api/tests/module.rs | 27 +++++++++++++++++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/api/src/backend/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index c1140e531513..e4c34a8f6f25 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -9,9 +9,10 @@ use wasm_bindgen::{JsValue, prelude::*}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_futures::JsFuture; use wasmer_types::{ - CompileError, DeserializeError, ExportType, ExportsIterator, ExternType, FunctionType, - GlobalIndex, GlobalType, ImportIndex, ImportType, ImportsIterator, InitExpr, InitExprOp, - MemoryType, ModuleInfo, Mutability, Pages, SerializeError, TableIndex, TableType, Type, + CompileError, DeserializeError, ExportIndex, ExportType, ExportsIterator, ExternType, + FunctionType, GlobalIndex, GlobalType, ImportIndex, ImportType, ImportsIterator, InitExpr, + InitExprOp, MemoryType, ModuleInfo, Mutability, Pages, SerializeError, TableIndex, TableType, + Type, }; use crate::{ @@ -82,7 +83,9 @@ fn evaluate_i32_init_expr( } } } - u32::try_from(stack.pop()?).ok().filter(|_| stack.is_empty()) + u32::try_from(stack.pop()?) + .ok() + .filter(|_| stack.is_empty()) } // XXX @@ -282,17 +285,18 @@ impl Module { let instance = WebAssembly::Instance::new(&self.module, &imports_object) .map_err(|e: JsValue| -> RuntimeError { e.into() })?; #[cfg(feature = "wasm-types-polyfill")] - self.annotate_imported_table_functions(store, imports); + self.annotate_table_functions(store, imports, &instance); Ok(instance) } #[cfg(feature = "wasm-types-polyfill")] - fn annotate_imported_table_functions( + fn annotate_table_functions( &self, store: &mut impl AsStoreMut, imports: &Imports, + instance: &WebAssembly::Instance, ) { - let mut tables = HashMap::::new(); + let mut tables = HashMap::::new(); let mut globals = HashMap::::new(); for (key, import_index) in &self.info.imports { @@ -301,7 +305,7 @@ impl Module { }; match (import_index, extern_) { (ImportIndex::Table(index), Extern::Table(table)) => { - tables.insert(*index, table); + tables.insert(*index, table.as_js().handle.table.clone()); } (ImportIndex::Global(index), Extern::Global(global)) => { let value = match global.get(store) { @@ -314,6 +318,16 @@ impl Module { _ => {} } } + let instance_exports = instance.exports(); + for (name, export_index) in &self.info.exports { + let ExportIndex::Table(index) = export_index else { + continue; + }; + let Ok(value) = Reflect::get(&instance_exports, &name.into()) else { + continue; + }; + tables.insert(*index, value.into()); + } for initializer in &self.info.table_initializers { let Some(table) = tables.get(&initializer.table_index) else { continue; @@ -328,12 +342,7 @@ impl Module { let Some(function_type) = self.info.signatures.get(*signature_index) else { continue; }; - let Ok(function) = table - .as_js() - .handle - .table - .get(start.saturating_add(offset as u32)) - else { + let Ok(function) = table.get(start.saturating_add(offset as u32)) else { continue; }; crate::js::vm::VMFunction::annotate_type(&function, function_type); diff --git a/lib/api/tests/module.rs b/lib/api/tests/module.rs index b1449c7eeea6..8c0676997c5e 100644 --- a/lib/api/tests/module.rs +++ b/lib/api/tests/module.rs @@ -75,6 +75,33 @@ fn imported_table_elements_preserve_function_types() { ); } +#[cfg(all(target_arch = "wasm32", feature = "js"))] +#[wasm_bindgen_test] +fn exported_table_elements_preserve_function_types() { + let mut store = Store::default(); + let module = Module::new( + &store, + r#" + (module + (type $callback_type (func (param i32 i32) (result i32))) + (func $callback (type $callback_type) + local.get 0) + (table (export "table") 1 funcref) + (elem (i32.const 0) func $callback)) + "#, + ) + .unwrap(); + let instance = Instance::new(&mut store, &module, &imports! {}).unwrap(); + let table = instance.exports.get_table("table").unwrap(); + let Value::FuncRef(Some(function)) = table.get(&mut store, 0).unwrap() else { + panic!("expected the initialized table element to be a function"); + }; + assert_eq!( + function.ty(&store), + FunctionType::new(vec![Type::I32, Type::I32], vec![Type::I32]) + ); +} + #[engine_test] fn module_get_name() -> Result<(), String> { let store = Store::default(); From b576f86a1e2c28bc6f8f428d3fb5805c52d24bf2 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 6 Aug 2026 10:54:52 -0700 Subject: [PATCH 29/32] fix(wasm-c-api): Add shared memory extension to the wasm-c-api (and the imports) --- Cargo.lock | 3 + lib/c-api-imports/Cargo.toml | 3 + lib/c-api-imports/src/lib.rs | 1057 +++++++++++++++++- lib/c-api/src/wasm_c_api/externals/memory.rs | 38 +- lib/c-api/src/wasm_c_api/module.rs | 37 + lib/c-api/src/wasm_c_api/types/memory.rs | 62 +- lib/c-api/tests/wasm-c-api/include/wasm.h | 11 + 7 files changed, 1166 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74d11a6ed5c3..0cc4bc977ac9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7311,7 +7311,10 @@ name = "wasmer-c-api-imports" version = "7.2.1" dependencies = [ "anyhow", + "bytes", + "js-sys", "tokio", + "wasm-bindgen", "wasmer", "wasmer-types", "wasmer-wasix", diff --git a/lib/c-api-imports/Cargo.toml b/lib/c-api-imports/Cargo.toml index 43358e0f95d3..2444e963ec4f 100644 --- a/lib/c-api-imports/Cargo.toml +++ b/lib/c-api-imports/Cargo.toml @@ -23,6 +23,9 @@ wasmer-wasix = { version = "=0.702.1", path = "../wasix", optional = true, featu ], default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] +bytes.workspace = true +js-sys.workspace = true +wasm-bindgen.workspace = true wasmer-wasix = { version = "=0.702.1", path = "../wasix", optional = true, features = [ "js", ], default-features = false } diff --git a/lib/c-api-imports/src/lib.rs b/lib/c-api-imports/src/lib.rs index ded815ba18c6..50c71b63e97c 100644 --- a/lib/c-api-imports/src/lib.rs +++ b/lib/c-api-imports/src/lib.rs @@ -1,5 +1,21 @@ use anyhow::{Context, Result, bail}; -use std::{collections::HashMap, fmt::Display, mem::size_of, num::NonZeroI32, slice, sync::Arc}; +use std::{ + collections::HashMap, + fmt::Display, + mem::size_of, + num::NonZeroI32, + slice, + sync::{Arc, Mutex}, +}; + +#[cfg(target_arch = "wasm32")] +use bytes::Bytes; +#[cfg(target_arch = "wasm32")] +use std::sync::atomic::{AtomicU32, Ordering}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{JsCast, JsValue}; +#[cfg(target_arch = "wasm32")] +use wasmer_api::js::AsJs; use wasmer_api::{ Extern, ExternRef, ExternType, Function, Function as WasmerFunction, FunctionEnv, @@ -8,6 +24,9 @@ use wasmer_api::{ TypedFunction, Value, WasmPtr, namespace, }; +#[cfg(not(target_arch = "wasm32"))] +use wasmer_api::SharedMemory; + #[cfg(feature = "wasix")] mod wasix; @@ -86,11 +105,16 @@ struct WasmCapiSession { imported_memory_type: Option, imported_table_type: Option, resolve_module_sync: Option, + shared_objects: SharedObjectRegistry, func_env: Option>, } impl WasmCapiSession { - fn new(module: &Module, resolve_module_sync: Option) -> Self { + fn new( + module: &Module, + resolve_module_sync: Option, + shared_objects: SharedObjectRegistry, + ) -> Self { let imported_memory_type = module.imports().find_map(|import| { if import.module() == "env" && import.name() == "memory" @@ -116,6 +140,7 @@ impl WasmCapiSession { imported_memory_type, imported_table_type, resolve_module_sync, + shared_objects, func_env: None, } } @@ -143,6 +168,7 @@ impl WasmCapiSession { &mut *store, WasmCapiEnv { resolve_module_sync: self.resolve_module_sync.clone(), + shared_objects: self.shared_objects.clone(), ..WasmCapiEnv::default() }, ); @@ -264,12 +290,14 @@ pub struct WasmCapiInstantiationState { /// Runtime hooks that provide `wasm_c_api_v0` imports for WASIX guests. // The import phase creates a per-instantiation session holding the function // env backing the imported host functions, and hands it to the caller as -// opaque state. The hooks themselves are stateless, so concurrent -// instantiations — same module or not, same store or not — cannot receive -// each other's sessions. +// opaque state. Ordinary C API handles remain session-local. Only handles +// produced by wasm_module_share/wasm_memory_share live in the runtime-scoped +// registry, which is what lets worker instances obtain the same module or +// shared memory without leaking every C API object across instances. #[derive(Clone, Default)] pub struct WasmCapiRuntimeHooks { resolve_module_sync: Option, + shared_objects: SharedObjectRegistry, } impl std::fmt::Debug for WasmCapiRuntimeHooks { @@ -279,6 +307,7 @@ impl std::fmt::Debug for WasmCapiRuntimeHooks { "resolve_module_sync", &self.resolve_module_sync.as_ref().map(|_| ".."), ) + .field("shared_objects", &"..") .finish() } } @@ -316,7 +345,11 @@ impl WasmCapiRuntimeHooks { store: &mut StoreMut<'_>, imports: &mut Imports, ) -> Result { - let mut session = WasmCapiSession::new(module, self.resolve_module_sync.clone()); + let mut session = WasmCapiSession::new( + module, + self.resolve_module_sync.clone(), + self.shared_objects.clone(), + ); if !session.needs_imports() { return Ok(WasmCapiInstantiationState { session: None }); } @@ -370,6 +403,9 @@ struct WasmCapiEnv { table: Option, /// Host-side object and shadow-memory handles visible to the guest as i32s. state: WasmCapiState, + /// Runtime-scoped objects explicitly shared through the C API's share and + /// obtain operations. + shared_objects: SharedObjectRegistry, /// Self-reference needed when creating host functions that call guest callbacks. func_env: Option>, } @@ -444,6 +480,140 @@ enum WasmObject { Trap(String), } +#[cfg(not(target_arch = "wasm32"))] +#[derive(Clone)] +enum SharedWasmObject { + Module { module: Module }, + Memory { memory: SharedMemory }, +} + +#[cfg(target_arch = "wasm32")] +#[derive(Clone)] +enum SharedWasmObject { + Module { bytes: Bytes }, + Memory { ty: MemoryType }, +} + +struct SharedObjectState { + next_handle: i32, + objects: HashMap, +} + +impl Default for SharedObjectState { + fn default() -> Self { + Self { + next_handle: 1, + objects: HashMap::new(), + } + } +} + +#[derive(Clone)] +struct SharedObjectRegistry { + state: Arc>, + #[cfg(target_arch = "wasm32")] + id: u32, +} + +impl Default for SharedObjectRegistry { + fn default() -> Self { + #[cfg(target_arch = "wasm32")] + static NEXT_REGISTRY_ID: AtomicU32 = AtomicU32::new(1); + + Self { + state: Arc::new(Mutex::new(SharedObjectState::default())), + #[cfg(target_arch = "wasm32")] + id: NEXT_REGISTRY_ID.fetch_add(1, Ordering::Relaxed), + } + } +} + +impl SharedObjectRegistry { + fn insert(&self, object: SharedWasmObject) -> i32 { + let Ok(mut state) = self.state.lock() else { + return INVALID_HANDLE; + }; + let handle = state.next_handle; + if handle <= INVALID_HANDLE { + return INVALID_HANDLE; + } + + state.next_handle = handle.checked_add(1).unwrap_or(INVALID_HANDLE); + state.objects.insert(handle, object); + handle + } + + fn get(&self, handle: i32) -> Option { + if handle <= INVALID_HANDLE { + return None; + } + self.state.lock().ok()?.objects.get(&handle).cloned() + } + + fn release(&self, handle: i32) { + // Native callers share one registry directly, so deleting the handle + // can release its object immediately. Browser workers receive the JS + // object asynchronously and may obtain it after the source handle is + // deleted; retain the transfer record until this registry itself is + // dropped so source deletion cannot revoke an in-flight transfer. + #[cfg(not(target_arch = "wasm32"))] + if handle > INVALID_HANDLE + && let Ok(mut state) = self.state.lock() + { + state.objects.remove(&handle); + } + #[cfg(target_arch = "wasm32")] + if handle > INVALID_HANDLE { + let global = js_sys::global(); + if let Ok(callback) = + js_sys::Reflect::get(&global, &JsValue::from_str("__wasmerCapiDelete")) + && let Some(callback) = callback.dyn_ref::() + { + let _ = callback.call2(&global, &JsValue::from(self.id), &JsValue::from(handle)); + } + } + } + + #[cfg(target_arch = "wasm32")] + fn discard(&self, handle: i32) { + if let Ok(mut state) = self.state.lock() { + state.objects.remove(&handle); + } + } + + #[cfg(target_arch = "wasm32")] + fn publish(&self, handle: i32, value: &JsValue) -> bool { + let global = js_sys::global(); + let Ok(callback) = js_sys::Reflect::get(&global, &JsValue::from_str("__wasmerCapiShare")) + else { + return false; + }; + let Some(callback) = callback.dyn_ref::() else { + return false; + }; + callback + .call3( + &global, + &JsValue::from(self.id), + &JsValue::from(handle), + value, + ) + .is_ok() + } + + #[cfg(target_arch = "wasm32")] + fn obtain(&self, handle: i32) -> Option { + let global = js_sys::global(); + let callback = + js_sys::Reflect::get(&global, &JsValue::from_str("__wasmerCapiObtain")).ok()?; + let callback = callback.dyn_ref::()?; + let value = callback + .call2(&global, &JsValue::from(self.id), &JsValue::from(handle)) + .ok()?; + (!value.is_undefined() && !value.is_null()).then_some(value) + } +} + /// A guest-visible shadow of a Wasmer memory data pointer. #[derive(Clone, Copy)] struct MemoryShadow { @@ -684,7 +854,7 @@ fn allocate_guest_memory(env: &mut FunctionEnvMut, len: usize) -> O let (_, mut store_ref) = env.data_and_store_mut(); match malloc_fn.call(&mut store_ref, len) { Ok(ptr) => ptr, - Err(error) => { + Err(_) => { return None; } } @@ -754,7 +924,7 @@ impl<'env, 'store> GuestAllocation<'env, 'store> { } fn copy_from_wasmer_memory(&mut self, memory: &Memory, len: usize) -> bool { - copy_wasmer_memory_to_guest(self.env, memory, self.ptr, len) + copy_wasmer_memory_to_guest(self.env, memory, 0, self.ptr, len) } fn into_raw(mut self) -> i32 { @@ -1105,6 +1275,63 @@ fn wasm_module_validate(mut env: FunctionEnvMut, _store: i32, bytes Module::validate(&store, &bytes).is_ok() as i32 } +fn wasm_module_share(env: FunctionEnvMut, module_handle: i32) -> i32 { + let module = match env.data().state.get(module_handle) { + Some(WasmObject::Module(module)) => module.clone(), + _ => return INVALID_HANDLE, + }; + #[cfg(target_arch = "wasm32")] + let Ok(bytes) = module.serialize() else { + return INVALID_HANDLE; + }; + #[cfg(target_arch = "wasm32")] + let transferable: js_sys::WebAssembly::Module = module.clone().into(); + let registry = &env.data().shared_objects; + let handle = registry.insert(SharedWasmObject::Module { + #[cfg(not(target_arch = "wasm32"))] + module, + #[cfg(target_arch = "wasm32")] + bytes, + }); + #[cfg(target_arch = "wasm32")] + if handle != INVALID_HANDLE && !registry.publish(handle, transferable.as_ref()) { + registry.discard(handle); + return INVALID_HANDLE; + } + handle +} + +fn wasm_module_obtain( + mut env: FunctionEnvMut, + _store: i32, + shared_handle: i32, +) -> i32 { + #[cfg(target_arch = "wasm32")] + let module = { + let bytes = match env.data().shared_objects.get(shared_handle) { + Some(SharedWasmObject::Module { bytes }) => bytes, + _ => return INVALID_HANDLE, + }; + match env.data().shared_objects.obtain(shared_handle) { + Some(value) => match value.dyn_into::() { + Ok(module) => Module::from((module, bytes)), + Err(_) => return INVALID_HANDLE, + }, + None => return INVALID_HANDLE, + } + }; + #[cfg(not(target_arch = "wasm32"))] + let module = match env.data().shared_objects.get(shared_handle) { + Some(SharedWasmObject::Module { module }) => module, + _ => return INVALID_HANDLE, + }; + insert(&mut env, WasmObject::Module(module)) +} + +fn wasm_shared_module_delete(env: FunctionEnvMut, shared_handle: i32) { + env.data().shared_objects.release(shared_handle); +} + fn wasm_module_imports(mut env: FunctionEnvMut, module_handle: i32, out_ptr: i32) { let module = match env.data().state.get(module_handle) { Some(WasmObject::Module(module)) => module.clone(), @@ -1274,6 +1501,24 @@ fn wasm_memorytype_new(mut env: FunctionEnvMut, limits_ptr: i32) -> ) } +fn wasm_shared_memorytype_new(mut env: FunctionEnvMut, limits_ptr: i32) -> i32 { + let Some((min, max)) = read_limits(&mut env, limits_ptr) else { + return INVALID_HANDLE; + }; + insert( + &mut env, + WasmObject::MemoryType(MemoryType::new(Pages(min), max.map(Pages), true)), + ) +} + +fn wasm_memorytype_is_shared(env: FunctionEnvMut, type_handle: i32) -> i32 { + match env.data().state.get(type_handle) { + Some(WasmObject::MemoryType(ty)) if ty.shared => BOOL_TRUE, + Some(WasmObject::MemoryType(_)) => BOOL_FALSE, + _ => BOOL_FALSE, + } +} + fn wasm_memory_new(mut env: FunctionEnvMut, _store: i32, type_handle: i32) -> i32 { let ty = match env.data().state.get(type_handle) { Some(WasmObject::MemoryType(ty)) => *ty, @@ -1308,6 +1553,468 @@ fn wasm_memory_grow(mut env: FunctionEnvMut, memory_handle: i32, de } } +fn wasm_memory_type(mut env: FunctionEnvMut, memory_handle: i32) -> i32 { + let Some(memory) = memory_from_handle(&env, memory_handle) else { + return INVALID_HANDLE; + }; + let memory_type = memory.ty(&env); + insert(&mut env, WasmObject::MemoryType(memory_type)) +} + +fn wasm_memory_read( + mut env: FunctionEnvMut, + memory_handle: i32, + offset: i64, + destination: i32, + length: i32, +) -> i32 { + let (Ok(offset), Ok(length)) = (u64::try_from(offset), usize::try_from(length)) else { + return BOOL_FALSE; + }; + let Some(memory) = memory_from_handle(&env, memory_handle) else { + return BOOL_FALSE; + }; + copy_wasmer_memory_to_guest(&mut env, &memory, offset, destination, length) as i32 +} + +fn wasm_memory_write( + mut env: FunctionEnvMut, + memory_handle: i32, + offset: i64, + source: i32, + length: i32, +) -> i32 { + let (Ok(offset), Ok(length)) = (u64::try_from(offset), usize::try_from(length)) else { + return BOOL_FALSE; + }; + let Some(memory) = memory_from_handle(&env, memory_handle) else { + return BOOL_FALSE; + }; + copy_guest_memory_to_wasmer(&mut env, source, &memory, offset, length) as i32 +} + +#[cfg(target_arch = "wasm32")] +fn js_memory_buffer(memory: &Memory, store: &impl wasmer_api::AsStoreRef) -> Option { + let memory = memory + .as_jsvalue(store) + .dyn_into::() + .ok()?; + Some(memory.buffer()) +} + +#[cfg(target_arch = "wasm32")] +fn js_atomic_operation( + memory: &Memory, + store: &impl wasmer_api::AsStoreRef, + offset: u64, + operation: i32, + width: i32, + value: u64, + replacement: u64, +) -> Option { + use js_sys::{Atomics, BigInt64Array, Int8Array, Int16Array, Int32Array}; + + let buffer = js_memory_buffer(memory, store)?; + let index = u32::try_from(offset / u64::try_from(width).ok()?).ok()?; + let result = match width { + 1 => { + let array = Int8Array::new(&buffer); + match operation { + 0 => Atomics::add(&array, index, value as i32), + 1 => Atomics::and(&array, index, value as i32), + 2 => Atomics::or(&array, index, value as i32), + 3 => Atomics::sub(&array, index, value as i32), + 4 => Atomics::xor(&array, index, value as i32), + 5 => Atomics::exchange(&array, index, value as i32), + 6 => Atomics::compare_exchange(&array, index, value as i32, replacement as i32), + 7 => Atomics::load(&array, index), + 8 => Atomics::store(&array, index, value as i32), + _ => return None, + } + .ok()? as u32 as u64 + } + 2 => { + let array = Int16Array::new(&buffer); + match operation { + 0 => Atomics::add(&array, index, value as i32), + 1 => Atomics::and(&array, index, value as i32), + 2 => Atomics::or(&array, index, value as i32), + 3 => Atomics::sub(&array, index, value as i32), + 4 => Atomics::xor(&array, index, value as i32), + 5 => Atomics::exchange(&array, index, value as i32), + 6 => Atomics::compare_exchange(&array, index, value as i32, replacement as i32), + 7 => Atomics::load(&array, index), + 8 => Atomics::store(&array, index, value as i32), + _ => return None, + } + .ok()? as u32 as u64 + } + 4 => { + let array = Int32Array::new(&buffer); + match operation { + 0 => Atomics::add(&array, index, value as i32), + 1 => Atomics::and(&array, index, value as i32), + 2 => Atomics::or(&array, index, value as i32), + 3 => Atomics::sub(&array, index, value as i32), + 4 => Atomics::xor(&array, index, value as i32), + 5 => Atomics::exchange(&array, index, value as i32), + 6 => Atomics::compare_exchange(&array, index, value as i32, replacement as i32), + 7 => Atomics::load(&array, index), + 8 => Atomics::store(&array, index, value as i32), + _ => return None, + } + .ok()? as u32 as u64 + } + 8 => { + let array = BigInt64Array::new(&buffer); + match operation { + 0 => Atomics::add_bigint(&array, index, value as i64), + 1 => Atomics::and_bigint(&array, index, value as i64), + 2 => Atomics::or_bigint(&array, index, value as i64), + 3 => Atomics::sub_bigint(&array, index, value as i64), + 4 => Atomics::xor_bigint(&array, index, value as i64), + 5 => Atomics::exchange_bigint(&array, index, value as i64), + 6 => Atomics::compare_exchange_bigint( + &array, + index, + value as i64, + replacement as i64, + ), + 7 => Atomics::load_bigint(&array, i64::from(index)), + 8 => Atomics::store_bigint(&array, index, value as i64), + _ => return None, + } + .ok()? as u64 + } + _ => return None, + }; + Some(result) +} + +fn wasm_memory_atomic( + env: FunctionEnvMut, + memory_handle: i32, + offset: i64, + operation: i32, + width: i32, + value: i64, + replacement: i64, + result_pointer: i32, +) -> i32 { + #[cfg(target_arch = "wasm32")] + { + let (Ok(offset), Some(memory)) = ( + u64::try_from(offset), + memory_from_handle(&env, memory_handle), + ) else { + return BOOL_FALSE; + }; + if !matches!(width, 1 | 2 | 4 | 8) || offset % width as u64 != 0 { + return BOOL_FALSE; + } + let Some(result) = js_atomic_operation( + &memory, + &env, + offset, + operation, + width, + value as u64, + replacement as u64, + ) else { + return BOOL_FALSE; + }; + let Some(guest_memory) = env.data().memory.clone() else { + return BOOL_FALSE; + }; + let Some(result_pointer) = guest_memory_offset(result_pointer) else { + return BOOL_FALSE; + }; + if guest_memory + .view(&env) + .write(result_pointer, &result.to_le_bytes()) + .is_err() + { + return BOOL_FALSE; + } + BOOL_TRUE + } + #[cfg(not(target_arch = "wasm32"))] + { + use std::sync::atomic::{AtomicU8, AtomicU16, AtomicU32, AtomicU64, Ordering}; + + let (Ok(offset), Some(memory)) = ( + u64::try_from(offset), + memory_from_handle(&env, memory_handle), + ) else { + return BOOL_FALSE; + }; + if !matches!(width, 1 | 2 | 4 | 8) || offset % width as u64 != 0 { + return BOOL_FALSE; + } + let view = memory.view(&env); + if offset + .checked_add(width as u64) + .is_none_or(|end| end > view.data_size()) + { + return BOOL_FALSE; + } + macro_rules! atomic_operation { + ($atomic:ty, $value:ty) => {{ + let atomic = unsafe { &*(view.data_ptr().add(offset as usize) as *const $atomic) }; + let value = value as $value; + let replacement = replacement as $value; + match operation { + 0 => atomic.fetch_add(value, Ordering::SeqCst) as u64, + 1 => atomic.fetch_and(value, Ordering::SeqCst) as u64, + 2 => atomic.fetch_or(value, Ordering::SeqCst) as u64, + 3 => atomic.fetch_sub(value, Ordering::SeqCst) as u64, + 4 => atomic.fetch_xor(value, Ordering::SeqCst) as u64, + 5 => atomic.swap(value, Ordering::SeqCst) as u64, + 6 => atomic + .compare_exchange(value, replacement, Ordering::SeqCst, Ordering::SeqCst) + .unwrap_or_else(|actual| actual) as u64, + 7 => atomic.load(Ordering::SeqCst) as u64, + 8 => { + atomic.store(value, Ordering::SeqCst); + value as u64 + } + _ => return BOOL_FALSE, + } + }}; + } + let result = match width { + 1 => atomic_operation!(AtomicU8, u8), + 2 => atomic_operation!(AtomicU16, u16), + 4 => atomic_operation!(AtomicU32, u32), + 8 => atomic_operation!(AtomicU64, u64), + _ => return BOOL_FALSE, + }; + let Some(guest_memory) = env.data().memory.clone() else { + return BOOL_FALSE; + }; + let Some(result_pointer) = guest_memory_offset(result_pointer) else { + return BOOL_FALSE; + }; + if guest_memory + .view(&env) + .write(result_pointer, &result.to_le_bytes()) + .is_err() + { + return BOOL_FALSE; + } + BOOL_TRUE + } +} + +fn wasm_memory_atomic_wait( + env: FunctionEnvMut, + memory_handle: i32, + offset: i64, + width: i32, + expected: i64, + timeout_nanos: i64, +) -> i32 { + #[cfg(target_arch = "wasm32")] + { + use js_sys::{Atomics, BigInt64Array, Int32Array}; + + let (Ok(offset), Some(memory)) = ( + u64::try_from(offset), + memory_from_handle(&env, memory_handle), + ) else { + return -1; + }; + let Some(buffer) = js_memory_buffer(&memory, &env) else { + return -1; + }; + let timeout_millis = if timeout_nanos == i64::MAX { + f64::INFINITY + } else { + timeout_nanos.max(0) as f64 / 1_000_000.0 + }; + let result = match width { + 4 if offset % 4 == 0 => Atomics::wait_with_timeout( + &Int32Array::new(&buffer), + (offset / 4) as u32, + expected as i32, + timeout_millis, + ), + 8 if offset % 8 == 0 => Atomics::wait_with_timeout_bigint( + &BigInt64Array::new(&buffer), + (offset / 8) as u32, + expected, + timeout_millis, + ), + _ => return -1, + }; + match result.ok().and_then(|value| value.as_string()).as_deref() { + Some("ok") => 0, + Some("not-equal") => 1, + Some("timed-out") => 2, + _ => -1, + } + } + #[cfg(not(target_arch = "wasm32"))] + { + use std::{ + sync::atomic::{AtomicI32, AtomicI64, Ordering}, + time::Duration, + }; + + let (Ok(offset), Some(memory)) = ( + u64::try_from(offset), + memory_from_handle(&env, memory_handle), + ) else { + return -1; + }; + let view = memory.view(&env); + if offset + .checked_add(width as u64) + .is_none_or(|end| end > view.data_size()) + { + return -1; + } + let equal = unsafe { + match width { + 4 if offset % 4 == 0 => { + (&*(view.data_ptr().add(offset as usize) as *const AtomicI32)) + .load(Ordering::SeqCst) + == expected as i32 + } + 8 if offset % 8 == 0 => { + (&*(view.data_ptr().add(offset as usize) as *const AtomicI64)) + .load(Ordering::SeqCst) + == expected + } + _ => return -1, + } + }; + if !equal { + return 1; + } + let timeout = + (timeout_nanos != i64::MAX).then(|| Duration::from_nanos(timeout_nanos.max(0) as u64)); + let Some(shared) = memory.as_shared(&env) else { + return -1; + }; + shared + .wait(wasmer_api::MemoryLocation::new_32(offset as u32), timeout) + .ok() + .and_then(|result| i32::try_from(result).ok()) + .unwrap_or(-1) + } +} + +fn wasm_memory_atomic_notify( + env: FunctionEnvMut, + memory_handle: i32, + offset: i64, + count: i32, +) -> i32 { + #[cfg(target_arch = "wasm32")] + { + use js_sys::{Atomics, Int32Array}; + + let (Ok(offset), Ok(count), Some(memory)) = ( + u64::try_from(offset), + u32::try_from(count), + memory_from_handle(&env, memory_handle), + ) else { + return -1; + }; + if offset % 4 != 0 { + return -1; + } + let Some(buffer) = js_memory_buffer(&memory, &env) else { + return -1; + }; + Atomics::notify_with_count(&Int32Array::new(&buffer), (offset / 4) as u32, count) + .ok() + .and_then(|result| i32::try_from(result).ok()) + .unwrap_or(-1) + } + #[cfg(not(target_arch = "wasm32"))] + { + let (Ok(offset), Ok(count), Some(memory)) = ( + u32::try_from(offset), + u32::try_from(count), + memory_from_handle(&env, memory_handle), + ) else { + return -1; + }; + let Some(shared) = memory.as_shared(&env) else { + return -1; + }; + shared + .notify(wasmer_api::MemoryLocation::new_32(offset), count) + .ok() + .and_then(|result| i32::try_from(result).ok()) + .unwrap_or(-1) + } +} + +fn wasm_memory_share(env: FunctionEnvMut, memory_handle: i32) -> i32 { + let Some(memory) = memory_from_handle(&env, memory_handle) else { + return INVALID_HANDLE; + }; + #[cfg(target_arch = "wasm32")] + let transferable = memory.as_jsvalue(&env); + #[cfg(target_arch = "wasm32")] + let ty = memory.ty(&env); + #[cfg(target_arch = "wasm32")] + if !ty.shared { + return INVALID_HANDLE; + } + #[cfg(not(target_arch = "wasm32"))] + let Some(memory) = memory.as_shared(&env) else { + return INVALID_HANDLE; + }; + let registry = &env.data().shared_objects; + let handle = registry.insert(SharedWasmObject::Memory { + #[cfg(not(target_arch = "wasm32"))] + memory, + #[cfg(target_arch = "wasm32")] + ty, + }); + #[cfg(target_arch = "wasm32")] + if handle != INVALID_HANDLE && !registry.publish(handle, &transferable) { + registry.discard(handle); + return INVALID_HANDLE; + } + handle +} + +fn wasm_memory_obtain( + mut env: FunctionEnvMut, + _store: i32, + shared_handle: i32, +) -> i32 { + #[cfg(target_arch = "wasm32")] + let memory = { + let ty = match env.data().shared_objects.get(shared_handle) { + Some(SharedWasmObject::Memory { ty }) => ty, + _ => return INVALID_HANDLE, + }; + match env.data().shared_objects.obtain(shared_handle) { + Some(value) => match Memory::from_jsvalue(&mut env, &ty, &value) { + Ok(memory) => memory, + Err(_) => return INVALID_HANDLE, + }, + None => return INVALID_HANDLE, + } + }; + #[cfg(not(target_arch = "wasm32"))] + let memory = match env.data().shared_objects.get(shared_handle) { + Some(SharedWasmObject::Memory { memory }) => memory.attach(&mut env), + _ => return INVALID_HANDLE, + }; + insert(&mut env, WasmObject::Memory(memory)) +} + +fn wasm_shared_memory_delete(env: FunctionEnvMut, shared_handle: i32) { + env.data().shared_objects.release(shared_handle); +} + fn memory_from_handle(env: &FunctionEnvMut, memory_handle: i32) -> Option { match env.data().state.get(memory_handle)? { WasmObject::Memory(memory) => Some(memory.clone()), @@ -1317,15 +2024,17 @@ fn memory_from_handle(env: &FunctionEnvMut, memory_handle: i32) -> } fn memory_supports_shadow(env: &FunctionEnvMut, memory: &Memory) -> bool { - // C-API-created memories are currently non-shared, but instance exports can - // still expose shared memories. Guest shadow buffers cannot represent - // concurrent writes coherently, so shadow APIs fail closed for those. + // Guest shadow buffers cannot represent concurrent writes coherently, so + // the standard data-pointer APIs fail closed for shared memories. The + // host-backed SharedArrayBuffer bridge uses the explicit read/write and + // atomic imports instead. !memory.ty(env).shared } fn copy_wasmer_memory_to_guest( env: &mut FunctionEnvMut, memory: &Memory, + memory_offset: u64, guest_ptr: i32, len: usize, ) -> bool { @@ -1337,9 +2046,12 @@ fn copy_wasmer_memory_to_guest( }; let source_view = memory.view(&*env); let guest_view = guest_memory.view(&*env); - let Some(source_offset) = checked_memory_offset(0, len, source_view.data_size()) else { + let Some(source_end) = memory_offset.checked_add(len as u64) else { return false; }; + if source_end > source_view.data_size() { + return false; + } let Some(guest_offset) = checked_memory_offset(guest_ptr, len, guest_view.data_size()) else { return false; }; @@ -1347,12 +2059,7 @@ fn copy_wasmer_memory_to_guest( return false; } source_view - .copy_range_to_memory( - source_offset as u64, - guest_offset as u64, - len as u64, - &guest_view, - ) + .copy_range_to_memory(memory_offset, guest_offset as u64, len as u64, &guest_view) .is_ok() } @@ -1360,6 +2067,7 @@ fn copy_guest_memory_to_wasmer( env: &mut FunctionEnvMut, guest_ptr: i32, memory: &Memory, + memory_offset: u64, len: usize, ) -> bool { if len == 0 { @@ -1373,19 +2081,17 @@ fn copy_guest_memory_to_wasmer( let Some(guest_offset) = checked_memory_offset(guest_ptr, len, guest_view.data_size()) else { return false; }; - let Some(target_offset) = checked_memory_offset(0, len, target_view.data_size()) else { + let Some(target_end) = memory_offset.checked_add(len as u64) else { return false; }; + if target_end > target_view.data_size() { + return false; + } if memory == &guest_memory { return false; } guest_view - .copy_range_to_memory( - guest_offset as u64, - target_offset as u64, - len as u64, - &target_view, - ) + .copy_range_to_memory(guest_offset as u64, memory_offset, len as u64, &target_view) .is_ok() } @@ -1470,7 +2176,7 @@ fn sync_memory_shadows_to_wasmer(env: &mut FunctionEnvMut) { if !memory_supports_shadow(env, &memory) { continue; }; - let _ = copy_guest_memory_to_wasmer(env, shadow.guest_ptr, &memory, shadow.len); + let _ = copy_guest_memory_to_wasmer(env, shadow.guest_ptr, &memory, 0, shadow.len); } } @@ -1490,7 +2196,7 @@ fn refresh_memory_shadows_from_wasmer(env: &mut FunctionEnvMut) { if !memory_supports_shadow(env, &memory) { continue; } - let _ = copy_wasmer_memory_to_guest(env, &memory, shadow.guest_ptr, shadow.len); + let _ = copy_wasmer_memory_to_guest(env, &memory, 0, shadow.guest_ptr, shadow.len); } } @@ -2345,6 +3051,9 @@ fn register_wasm_c_api_imports( "wasm_store_delete" => WasmerFunction::new_typed_with_env(store, fe, delete_handle), "wasm_module_new" => WasmerFunction::new_typed_with_env(store, fe, wasm_module_new), "wasm_module_validate" => WasmerFunction::new_typed_with_env(store, fe, wasm_module_validate), + "wasm_module_share" => WasmerFunction::new_typed_with_env(store, fe, wasm_module_share), + "wasm_module_obtain" => WasmerFunction::new_typed_with_env(store, fe, wasm_module_obtain), + "wasm_shared_module_delete" => WasmerFunction::new_typed_with_env(store, fe, wasm_shared_module_delete), "wasm_module_delete" => WasmerFunction::new_typed_with_env(store, fe, delete_handle), "wasm_module_imports" => WasmerFunction::new_typed_with_env(store, fe, wasm_module_imports), "wasm_module_exports" => WasmerFunction::new_typed_with_env(store, fe, wasm_module_exports), @@ -2367,8 +3076,19 @@ fn register_wasm_c_api_imports( "wasm_val_vec_new_uninitialized" => WasmerFunction::new_typed_with_env(store, fe, wasm_val_vec_new_uninitialized), "wasm_val_vec_delete" => WasmerFunction::new_typed_with_env(store, fe, vec_delete), "wasm_memorytype_new" => WasmerFunction::new_typed_with_env(store, fe, wasm_memorytype_new), + "wasm_shared_memorytype_new" => WasmerFunction::new_typed_with_env(store, fe, wasm_shared_memorytype_new), + "wasm_memorytype_is_shared" => WasmerFunction::new_typed_with_env(store, fe, wasm_memorytype_is_shared), + "wasm_memory_read" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_read), + "wasm_memory_write" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_write), + "wasm_memory_atomic" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_atomic), + "wasm_memory_atomic_wait" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_atomic_wait), + "wasm_memory_atomic_notify" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_atomic_notify), "wasm_memorytype_delete" => WasmerFunction::new_typed_with_env(store, fe, delete_handle), "wasm_memory_new" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_new), + "wasm_memory_type" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_type), + "wasm_memory_share" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_share), + "wasm_memory_obtain" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_obtain), + "wasm_shared_memory_delete" => WasmerFunction::new_typed_with_env(store, fe, wasm_shared_memory_delete), "wasm_memory_delete" => WasmerFunction::new_typed_with_env(store, fe, delete_handle), "wasm_memory_copy" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_copy), "wasm_memory_size" => WasmerFunction::new_typed_with_env(store, fe, wasm_memory_size), @@ -2439,17 +3159,19 @@ fn register_wasm_c_api_imports( #[cfg(test)] mod tests { use super::{ - BOOL_FALSE, BOOL_TRUE, INVALID_HANDLE, INVALID_SIZE, MemoryShadow, Type, WASM_EXTERNREF, - WASM_F64, WASM_FUNCREF, WASM_I32, WASM_VAL_PAYLOAD_OFFSET, WasmCAPIVersion, WasmCapiEnv, - WasmCapiInstantiationState, WasmCapiRuntimeHooks, WasmCapiState, WasmObject, - copy_guest_memory_to_wasmer, copy_wasmer_memory_to_guest, guest_byte_ptr, - guest_memory_offset, guest_ptr_with_offset, module_wasm_c_api_version_used, + BOOL_FALSE, BOOL_TRUE, INVALID_HANDLE, INVALID_SIZE, MemoryShadow, SharedObjectRegistry, + Type, WASM_EXTERNREF, WASM_F64, WASM_FUNCREF, WASM_I32, WASM_VAL_PAYLOAD_OFFSET, + WasmCAPIVersion, WasmCapiEnv, WasmCapiInstantiationState, WasmCapiRuntimeHooks, + WasmCapiState, WasmObject, copy_guest_memory_to_wasmer, copy_wasmer_memory_to_guest, + guest_byte_ptr, guest_memory_offset, guest_ptr_with_offset, module_wasm_c_api_version_used, non_null_guest_ptr, read_wasm_val, ref_values_same, refresh_memory_shadows_from_wasmer, sync_memory_shadows_to_wasmer, type_to_wasm_kind, wasm_foreign_new, wasm_func_as_ref, - wasm_kind_to_type, wasm_memory_data, wasm_memory_data_size, wasm_memory_size, - wasm_ref_as_func, wasm_ref_copy, wasm_ref_get_host_info, wasm_ref_same, - wasm_ref_set_host_info, wasm_table_get, wasm_table_grow, wasm_table_set, wasm_table_size, - write_wasm_val, + wasm_kind_to_type, wasm_memory_data, wasm_memory_data_size, wasm_memory_obtain, + wasm_memory_read, wasm_memory_share, wasm_memory_size, wasm_memory_type, wasm_memory_write, + wasm_module_obtain, wasm_module_share, wasm_ref_as_func, wasm_ref_copy, + wasm_ref_get_host_info, wasm_ref_same, wasm_ref_set_host_info, wasm_shared_memory_delete, + wasm_shared_module_delete, wasm_table_get, wasm_table_grow, wasm_table_set, + wasm_table_size, write_wasm_val, }; use wasmer_api::{ AsStoreMut, Function, FunctionEnv, Instance, Memory, MemoryType, Module, Pages, Store, @@ -2811,6 +3533,210 @@ mod tests { ); } + #[test] + fn shared_memory_obtain_preserves_coherent_storage() { + let mut store = Store::default(); + let memory = Memory::new(&mut store, MemoryType::new(Pages(1), Some(Pages(2)), true)) + .expect("shared memory can be created"); + let func_env = FunctionEnv::new(&mut store, WasmCapiEnv::default()); + let memory_handle = func_env + .as_mut(&mut store) + .state + .insert(WasmObject::Memory(memory.clone())); + + let memory_type_handle = + wasm_memory_type(func_env.clone().into_mut(&mut store), memory_handle); + assert!(matches!( + func_env.as_mut(&mut store).state.get(memory_type_handle), + Some(WasmObject::MemoryType(memory_type)) if memory_type.shared + )); + + let shared_handle = wasm_memory_share(func_env.clone().into_mut(&mut store), memory_handle); + assert_ne!(shared_handle, INVALID_HANDLE); + let obtained_handle = wasm_memory_obtain( + func_env.clone().into_mut(&mut store), + INVALID_HANDLE, + shared_handle, + ); + assert_ne!(obtained_handle, INVALID_HANDLE); + + let obtained = match func_env.as_mut(&mut store).state.get(obtained_handle) { + Some(WasmObject::Memory(memory)) => memory.clone(), + _ => panic!("obtained handle should contain a memory"), + }; + memory + .view(&store) + .write(37, &[1, 2, 3, 4]) + .expect("source shared-memory write succeeds"); + let mut bytes = [0; 4]; + obtained + .view(&store) + .read(37, &mut bytes) + .expect("obtained shared-memory read succeeds"); + assert_eq!(bytes, [1, 2, 3, 4]); + } + + #[test] + fn shared_memory_crosses_independent_c_api_sessions() { + let mut source_store = Store::default(); + let mut target_store = Store::new(source_store.engine().clone()); + let registry = SharedObjectRegistry::default(); + let source_env = FunctionEnv::new( + &mut source_store, + WasmCapiEnv { + shared_objects: registry.clone(), + ..WasmCapiEnv::default() + }, + ); + let target_env = FunctionEnv::new( + &mut target_store, + WasmCapiEnv { + shared_objects: registry, + ..WasmCapiEnv::default() + }, + ); + let memory = Memory::new( + &mut source_store, + MemoryType::new(Pages(1), Some(Pages(2)), true), + ) + .expect("shared memory can be created"); + let source_handle = source_env + .as_mut(&mut source_store) + .state + .insert(WasmObject::Memory(memory.clone())); + + let shared_handle = wasm_memory_share( + source_env.clone().into_mut(&mut source_store), + source_handle, + ); + let target_handle = wasm_memory_obtain( + target_env.clone().into_mut(&mut target_store), + INVALID_HANDLE, + shared_handle, + ); + assert_ne!(target_handle, INVALID_HANDLE); + wasm_shared_memory_delete( + source_env.clone().into_mut(&mut source_store), + shared_handle, + ); + assert_eq!( + wasm_memory_obtain( + target_env.clone().into_mut(&mut target_store), + INVALID_HANDLE, + shared_handle, + ), + INVALID_HANDLE, + "deleting the shared handle prevents a later obtain" + ); + + let obtained = match target_env + .as_mut(&mut target_store) + .state + .get(target_handle) + { + Some(WasmObject::Memory(memory)) => memory.clone(), + _ => panic!("target session should obtain the shared memory"), + }; + memory + .view(&source_store) + .write(91, &[5, 6, 7, 8]) + .expect("source write succeeds"); + let mut bytes = [0; 4]; + obtained + .view(&target_store) + .read(91, &mut bytes) + .expect("target read succeeds"); + assert_eq!(bytes, [5, 6, 7, 8]); + } + + #[test] + fn shared_module_obtain_preserves_compiled_module() { + let mut store = Store::default(); + let module = compile_wat(&store, "(module (func (export \"run\")))"); + let func_env = FunctionEnv::new(&mut store, WasmCapiEnv::default()); + let module_handle = func_env + .as_mut(&mut store) + .state + .insert(WasmObject::Module(module)); + + let shared_handle = wasm_module_share(func_env.clone().into_mut(&mut store), module_handle); + assert_ne!(shared_handle, INVALID_HANDLE); + let obtained_handle = wasm_module_obtain( + func_env.clone().into_mut(&mut store), + INVALID_HANDLE, + shared_handle, + ); + assert_ne!(obtained_handle, INVALID_HANDLE); + + let obtained = match func_env.as_mut(&mut store).state.get(obtained_handle) { + Some(WasmObject::Module(module)) => module.clone(), + _ => panic!("obtained handle should contain a module"), + }; + Instance::new(&mut store, &obtained, &wasmer_api::imports! {}) + .expect("obtained module instantiates"); + } + + #[test] + fn shared_module_crosses_independent_c_api_sessions() { + let mut source_store = Store::default(); + let mut target_store = Store::new(source_store.engine().clone()); + let registry = SharedObjectRegistry::default(); + let source_env = FunctionEnv::new( + &mut source_store, + WasmCapiEnv { + shared_objects: registry.clone(), + ..WasmCapiEnv::default() + }, + ); + let target_env = FunctionEnv::new( + &mut target_store, + WasmCapiEnv { + shared_objects: registry, + ..WasmCapiEnv::default() + }, + ); + let module = compile_wat(&source_store, "(module (func (export \"run\")))"); + let source_handle = source_env + .as_mut(&mut source_store) + .state + .insert(WasmObject::Module(module)); + + let shared_handle = wasm_module_share( + source_env.clone().into_mut(&mut source_store), + source_handle, + ); + let target_handle = wasm_module_obtain( + target_env.clone().into_mut(&mut target_store), + INVALID_HANDLE, + shared_handle, + ); + assert_ne!(target_handle, INVALID_HANDLE); + wasm_shared_module_delete( + source_env.clone().into_mut(&mut source_store), + shared_handle, + ); + assert_eq!( + wasm_module_obtain( + target_env.clone().into_mut(&mut target_store), + INVALID_HANDLE, + shared_handle, + ), + INVALID_HANDLE, + "deleting the shared handle prevents a later obtain" + ); + + let obtained = match target_env + .as_mut(&mut target_store) + .state + .get(target_handle) + { + Some(WasmObject::Module(module)) => module.clone(), + _ => panic!("target session should obtain the shared module"), + }; + Instance::new(&mut target_store, &obtained, &wasmer_api::imports! {}) + .expect("target session's module instantiates in its store"); + } + #[test] fn memory_shadow_copy_rejects_same_memory() { let mut store = Store::default(); @@ -2825,8 +3751,63 @@ mod tests { ); let mut env = func_env.into_mut(&mut store); - assert!(!copy_wasmer_memory_to_guest(&mut env, &memory, 16, 4)); - assert!(!copy_guest_memory_to_wasmer(&mut env, 16, &memory, 4)); + assert!(!copy_wasmer_memory_to_guest(&mut env, &memory, 0, 16, 4)); + assert!(!copy_guest_memory_to_wasmer(&mut env, 16, &memory, 0, 4)); + } + + #[test] + fn memory_read_write_copy_ranges_without_shadow_allocations() { + let mut store = Store::default(); + let guest_memory = + Memory::new(&mut store, MemoryType::new(Pages(1), Some(Pages(1)), false)) + .expect("guest memory can be created"); + let nested_memory = + Memory::new(&mut store, MemoryType::new(Pages(1), Some(Pages(1)), false)) + .expect("nested memory can be created"); + nested_memory + .view(&store) + .write(8, &[1, 2, 3, 4]) + .expect("nested bytes can be initialized"); + + let mut capi_env = WasmCapiEnv { + memory: Some(guest_memory.clone()), + ..WasmCapiEnv::default() + }; + let nested_handle = capi_env + .state + .insert(WasmObject::Memory(nested_memory.clone())); + let func_env = FunctionEnv::new(&mut store, capi_env); + + assert_eq!( + wasm_memory_read( + func_env.clone().into_mut(&mut store), + nested_handle, + 8, + 16, + 4, + ), + BOOL_TRUE + ); + let mut bytes = [0; 4]; + guest_memory + .view(&store) + .read(16, &mut bytes) + .expect("guest bytes can be read"); + assert_eq!(bytes, [1, 2, 3, 4]); + + guest_memory + .view(&store) + .write(24, &[5, 6, 7, 8]) + .expect("guest bytes can be initialized"); + assert_eq!( + wasm_memory_write(func_env.into_mut(&mut store), nested_handle, 32, 24, 4,), + BOOL_TRUE + ); + nested_memory + .view(&store) + .read(32, &mut bytes) + .expect("nested bytes can be read"); + assert_eq!(bytes, [5, 6, 7, 8]); } #[test] diff --git a/lib/c-api/src/wasm_c_api/externals/memory.rs b/lib/c-api/src/wasm_c_api/externals/memory.rs index 58ca9e2dbea4..ff77ab516c4b 100644 --- a/lib/c-api/src/wasm_c_api/externals/memory.rs +++ b/lib/c-api/src/wasm_c_api/externals/memory.rs @@ -2,7 +2,7 @@ use crate::error::update_last_error; use super::super::types::wasm_memorytype_t; use super::{super::store::wasm_store_t, wasm_extern_t}; -use wasmer_api::{Extern, Memory, Pages}; +use wasmer_api::{Extern, Memory, Pages, SharedMemory}; #[allow(non_camel_case_types)] #[repr(C)] @@ -11,6 +11,12 @@ pub struct wasm_memory_t { pub(crate) extern_: wasm_extern_t, } +#[allow(non_camel_case_types)] +#[derive(Clone)] +pub struct wasm_shared_memory_t { + memory: SharedMemory, +} + impl wasm_memory_t { pub(crate) fn try_from(e: &wasm_extern_t) -> Option<&wasm_memory_t> { match &e.inner { @@ -44,6 +50,36 @@ pub unsafe extern "C" fn wasm_memory_copy(memory: &wasm_memory_t) -> Box, +) -> Option> { + let memory = memory?; + let store_ref = unsafe { memory.extern_.store.store() }; + let shared = memory.extern_.memory().as_shared(&store_ref)?; + Some(Box::new(wasm_shared_memory_t { memory: shared })) +} + +/// Attaches a detached shared memory to a store. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wasm_memory_obtain( + store: Option<&mut wasm_store_t>, + shared: Option<&wasm_shared_memory_t>, +) -> Option> { + let store = store?; + let shared = shared?; + let mut store_mut = unsafe { store.inner.store_mut() }; + let memory = shared.memory.clone().attach(&mut store_mut); + Some(Box::new(wasm_memory_t { + extern_: wasm_extern_t::new(store.inner.clone(), memory.into()), + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wasm_shared_memory_delete(_memory: Option>) {} + #[unsafe(no_mangle)] pub unsafe extern "C" fn wasm_memory_same( wasm_memory1: &wasm_memory_t, diff --git a/lib/c-api/src/wasm_c_api/module.rs b/lib/c-api/src/wasm_c_api/module.rs index 7fff2fd58856..210b58f3c2cc 100644 --- a/lib/c-api/src/wasm_c_api/module.rs +++ b/lib/c-api/src/wasm_c_api/module.rs @@ -10,6 +10,14 @@ pub struct wasm_module_t { pub(crate) inner: Module, } +/// A compiled module detached from a store and safe to transfer between +/// threads before obtaining it in another store. +#[derive(Clone)] +#[allow(non_camel_case_types)] +pub struct wasm_shared_module_t { + inner: Module, +} + /// A WebAssembly module contains stateless WebAssembly code that has /// already been compiled and can be instantiated multiple times. /// @@ -46,6 +54,35 @@ pub unsafe extern "C" fn wasm_module_new( #[unsafe(no_mangle)] pub unsafe extern "C" fn wasm_module_delete(_module: Option>) {} +/// Shares a compiled module so it can be obtained by another store/thread. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wasm_module_share( + module: Option<&wasm_module_t>, +) -> Option> { + Some(Box::new(wasm_shared_module_t { + inner: module?.inner.clone(), + })) +} + +/// Obtains a shared compiled module for a store. +/// +/// Wasmer modules are engine-owned and cheaply cloneable. The store argument +/// is retained by the standard C API contract; compatibility is checked when +/// the module is instantiated. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wasm_module_obtain( + store: Option<&mut wasm_store_t>, + shared: Option<&wasm_shared_module_t>, +) -> Option> { + store?; + Some(Box::new(wasm_module_t { + inner: shared?.inner.clone(), + })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wasm_shared_module_delete(_module: Option>) {} + /// Validates a new WebAssembly module given the configuration /// in the [store][super::store]. /// diff --git a/lib/c-api/src/wasm_c_api/types/memory.rs b/lib/c-api/src/wasm_c_api/types/memory.rs index 003a8ac2ef96..e6389db1c66f 100644 --- a/lib/c-api/src/wasm_c_api/types/memory.rs +++ b/lib/c-api/src/wasm_c_api/types/memory.rs @@ -48,10 +48,7 @@ impl wasm_memorytype_t { } } -wasm_declare_boxed_vec!(memorytype); - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn wasm_memorytype_new(limits: &wasm_limits_t) -> Box { +pub(crate) fn memory_type_from_limits(limits: &wasm_limits_t, shared: bool) -> MemoryType { let min_pages = Pages(limits.min as _); let max_pages = if limits.max == LIMITS_MAX_SENTINEL { None @@ -59,11 +56,37 @@ pub unsafe extern "C" fn wasm_memorytype_new(limits: &wasm_limits_t) -> Box Box { + Box::new(wasm_memorytype_t::new(memory_type_from_limits( + limits, false, + ))) +} + +/// Creates a shared memory type. +/// +/// This extends the WebAssembly C API without changing the ABI of +/// [`wasm_limits_t`], which does not carry a shared-memory flag. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wasm_shared_memorytype_new( + limits: &wasm_limits_t, +) -> Box { + Box::new(wasm_memorytype_t::new(memory_type_from_limits( + limits, true, ))) } +/// Returns whether a memory type describes shared memory. +#[unsafe(no_mangle)] +pub extern "C" fn wasm_memorytype_is_shared(memory_type: &wasm_memorytype_t) -> bool { + memory_type.inner().memory_type.shared +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn wasm_memorytype_delete(_memory_type: Option>) {} @@ -81,3 +104,30 @@ const LIMITS_MAX_SENTINEL: u32 = u32::MAX; pub unsafe extern "C" fn wasm_memorytype_limits(memory_type: &wasm_memorytype_t) -> &wasm_limits_t { &memory_type.inner().limits } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shared_memorytype_preserves_limits() { + let limits = wasm_limits_t { min: 1, max: 2 }; + let shared = unsafe { wasm_shared_memorytype_new(&limits) }; + + assert!(wasm_memorytype_is_shared(&shared)); + assert_eq!(shared.inner().memory_type.minimum, Pages(1)); + assert_eq!(shared.inner().memory_type.maximum, Some(Pages(2))); + } + + #[test] + fn standard_memorytype_is_not_shared() { + let limits = wasm_limits_t { + min: 0, + max: LIMITS_MAX_SENTINEL, + }; + let memory_type = unsafe { wasm_memorytype_new(&limits) }; + + assert!(!wasm_memorytype_is_shared(&memory_type)); + assert_eq!(memory_type.inner().memory_type.maximum, None); + } +} diff --git a/lib/c-api/tests/wasm-c-api/include/wasm.h b/lib/c-api/tests/wasm-c-api/include/wasm.h index 58115fa386d7..8734923448d6 100644 --- a/lib/c-api/tests/wasm-c-api/include/wasm.h +++ b/lib/c-api/tests/wasm-c-api/include/wasm.h @@ -248,6 +248,11 @@ WASM_DECLARE_TYPE(memorytype) WASM_API_EXTERN own wasm_memorytype_t* wasm_memorytype_new(const wasm_limits_t*); +// Wasmer extensions for the WebAssembly threads proposal. The current C API +// limits structure does not otherwise carry the shared-memory attribute. +WASM_API_EXTERN own wasm_memorytype_t* wasm_shared_memorytype_new(const wasm_limits_t*); +WASM_API_EXTERN bool wasm_memorytype_is_shared(const wasm_memorytype_t*); + WASM_API_EXTERN const wasm_limits_t* wasm_memorytype_limits(const wasm_memorytype_t*); @@ -470,6 +475,7 @@ WASM_API_EXTERN bool wasm_table_grow(wasm_table_t*, wasm_table_size_t delta, was // Memory Instances WASM_DECLARE_REF(memory) +WASM_DECLARE_OWN(shared_memory) typedef uint32_t wasm_memory_pages_t; @@ -485,6 +491,11 @@ WASM_API_EXTERN size_t wasm_memory_data_size(const wasm_memory_t*); WASM_API_EXTERN wasm_memory_pages_t wasm_memory_size(const wasm_memory_t*); WASM_API_EXTERN bool wasm_memory_grow(wasm_memory_t*, wasm_memory_pages_t delta); +// Wasmer extensions for transferring shared memories between stores/threads. +WASM_API_EXTERN own wasm_shared_memory_t* wasm_memory_share(const wasm_memory_t*); +WASM_API_EXTERN own wasm_memory_t* wasm_memory_obtain( + wasm_store_t*, const wasm_shared_memory_t*); + // Externals From 84a0c4dddb333c04a5bad2bdf338f42c265f9fd5 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 6 Aug 2026 10:56:46 -0700 Subject: [PATCH 30/32] fix(api-js-backend): make memory maximum pages limit bigger --- lib/api/src/backend/js/entities/memory/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/api/src/backend/js/entities/memory/mod.rs b/lib/api/src/backend/js/entities/memory/mod.rs index 7afd344ece47..249dc1730f73 100644 --- a/lib/api/src/backend/js/entities/memory/mod.rs +++ b/lib/api/src/backend/js/entities/memory/mod.rs @@ -16,11 +16,10 @@ use crate::{ vm::{VMExtern, VMExternMemory}, }; -// Reserving the multi-gigabyte maximum declared by some WASIX programs can -// prevent browsers from creating the other shared memories used by the SDK. -// 512 MiB is large enough for substantial WASIX workloads while leaving useful -// address-space headroom for the runtime and concurrent processes. -const JS_SHARED_MEMORY_MAXIMUM_PAGES: Pages = Pages(16_384); +// QuickJS currently represents ArrayBuffer lengths with a signed 32-bit value. +// Keep shared WASIX memories just below 2 GiB so nested WebAssembly runtimes can +// grow large workloads without exposing a buffer QuickJS cannot address. +const JS_SHARED_MEMORY_MAXIMUM_PAGES: Pages = Pages(32_767); #[derive(Debug, Clone, Eq)] pub struct Memory { From 4a84ca03f18d654729cb60db0a0b747df9c6c4ec Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 6 Aug 2026 11:14:41 -0700 Subject: [PATCH 31/32] fix(wasix): Fix WASIX thread lifecycle handling Add an optional task-manager hook for terminating the executor that owns a specific WASM thread. This allows SIGKILL to stop browser workers directly, while falling back to waking atomic waiters for runtimes without targeted cancellation. Keep child-thread exits thread-local: mark killed threads as interrupted, prevent child threads from performing process-wide cleanup, publish the main thread result before broadcasting cleanup signals, and avoid tainting the runtime for non-zero child-thread exits. --- lib/wasix/src/bin_factory/exec.rs | 6 +++- lib/wasix/src/os/task/process.rs | 10 ++++++- lib/wasix/src/runtime/task_manager/mod.rs | 28 ++++++++++++++++++- lib/wasix/src/state/env.rs | 7 +++-- lib/wasix/src/syscalls/wasix/thread_signal.rs | 15 ++++++---- lib/wasix/src/syscalls/wasix/thread_spawn.rs | 6 ---- 6 files changed, 56 insertions(+), 16 deletions(-) diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index b7e99b94c36b..ecc5f310043f 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -405,12 +405,16 @@ async fn call_module( Errno::Success.into() }; + // Publish this thread's result before process cleanup broadcasts signals + // to residual worker threads. Otherwise the main thread can consume its + // own cleanup signal and replace a successful exit with a fatal signal. + handle.thread.set_status_finished(ret.map(|a| a.into())); + // Cleanup the environment ctx.data(&store).blocking_on_exit(Some(code)); unsafe { run_recycle(recycle, ctx, store) }; debug!("wasi[{pid}]::main() has exited with {code}"); - handle.thread.set_status_finished(ret.map(|a| a.into())); } #[allow(clippy::type_complexity)] diff --git a/lib/wasix/src/os/task/process.rs b/lib/wasix/src/os/task/process.rs index a0b1fc5b2b32..acb7b767bf6d 100644 --- a/lib/wasix/src/os/task/process.rs +++ b/lib/wasix/src/os/task/process.rs @@ -599,8 +599,10 @@ impl WasiProcess { let inner = self.inner.0.lock().unwrap(); - wake_atomic_waiters(&inner, signal); if let Some(thread) = inner.threads.get(&tid) { + if signal == Signal::Sigkill { + thread.set_status_finished(Ok(Errno::Intr.into())); + } thread.signal(signal); } else { trace!( @@ -612,6 +614,12 @@ impl WasiProcess { } } + /// Wake threads blocked on this process's atomic waiters. + pub(crate) fn wake_atomic_waiters(&self, signal: Signal) { + let inner = self.inner.0.lock().unwrap(); + wake_atomic_waiters(&inner, signal); + } + /// Signals all the threads in this process pub fn signal_process(&self, signal: Signal) { signal_process_internal(&self.inner, signal); diff --git a/lib/wasix/src/runtime/task_manager/mod.rs b/lib/wasix/src/runtime/task_manager/mod.rs index 2c11ef0f0357..7c1dda8640bf 100644 --- a/lib/wasix/src/runtime/task_manager/mod.rs +++ b/lib/wasix/src/runtime/task_manager/mod.rs @@ -17,7 +17,10 @@ use wasmer::{AsStoreMut, Memory, MemoryType, Module, SharedMemory, Store, StoreM use wasmer_wasix_types::wasi::{Errno, ExitCode}; use crate::os::task::thread::WasiThreadError; -use crate::{StoreSnapshot, WasiEnv, WasiFunctionEnv, WasiThread, capture_store_snapshot}; +use crate::{ + StoreSnapshot, WasiEnv, WasiFunctionEnv, WasiProcessId, WasiThread, WasiThreadId, + capture_store_snapshot, +}; use crate::{state::PreparedInstanceGroupData, syscalls::AsyncifyFuture}; pub use virtual_mio::waker::*; @@ -315,6 +318,21 @@ pub trait VirtualTaskManager: std::fmt::Debug + Send + Sync + 'static { /// the transfer of things like [`wasmer::Module`] across threads. fn task_wasm(&self, task: TaskWasm) -> Result<(), WasiThreadError>; + /// Stop the executor task currently running a WASM thread, if the task + /// manager provides externally cancellable workers. + /// + /// WASIX marks the logical thread as terminated before invoking this hook. + /// Executors which cannot independently stop a running task may leave the + /// default implementation in place. WASIX will then fall back to waking + /// the process memory's atomic waiters. + fn terminate_wasm_thread( + &self, + _pid: WasiProcessId, + _tid: WasiThreadId, + ) -> Result<(), WasiThreadError> { + Err(WasiThreadError::Unsupported) + } + /// Run a blocking operation on the thread pool. /// /// It is okay for this task to block execution and any async futures within @@ -384,6 +402,14 @@ where (**self).task_wasm(task) } + fn terminate_wasm_thread( + &self, + pid: WasiProcessId, + tid: WasiThreadId, + ) -> Result<(), WasiThreadError> { + (**self).terminate_wasm_thread(pid, tid) + } + fn task_dedicated( &self, task: Box, diff --git a/lib/wasix/src/state/env.rs b/lib/wasix/src/state/env.rs index a13e32d6e817..ab455a4d0757 100644 --- a/lib/wasix/src/state/env.rs +++ b/lib/wasix/src/state/env.rs @@ -1311,8 +1311,11 @@ impl WasiEnv { } } - // If the process wants to exit, also close all files and terminate it - if let Some(process_exit_code) = process_exit_code { + // A thread exit must not terminate the process. Only the main thread + // owns process cleanup and the process-wide exit status. + if self.thread.is_main() + && let Some(process_exit_code) = process_exit_code + { let process = self.process.clone(); let disable_fs_cleanup = self.disable_fs_cleanup; let pid = self.pid(); diff --git a/lib/wasix/src/syscalls/wasix/thread_signal.rs b/lib/wasix/src/syscalls/wasix/thread_signal.rs index 96c9c55765b9..52a02f15f11d 100644 --- a/lib/wasix/src/syscalls/wasix/thread_signal.rs +++ b/lib/wasix/src/syscalls/wasix/thread_signal.rs @@ -13,12 +13,17 @@ pub fn thread_signal( tid: Tid, sig: Signal, ) -> Result { - { - let tid: WasiThreadId = tid.into(); - ctx.data().process.signal_thread(&tid, sig); - } + let tid: WasiThreadId = tid.into(); + ctx.data().process.signal_thread(&tid, sig); - let env = ctx.data(); + if sig == Signal::Sigkill { + let env = ctx.data(); + if let Err(error) = env.tasks().terminate_wasm_thread(env.pid(), tid) { + tracing::debug!(%error, %tid, "task manager could not terminate WASM thread"); + env.process.wake_atomic_waiters(sig); + } + return Ok(Errno::Success); + } WasiEnv::do_pending_operations(&mut ctx)?; diff --git a/lib/wasix/src/syscalls/wasix/thread_spawn.rs b/lib/wasix/src/syscalls/wasix/thread_spawn.rs index 7db699c30be2..b9be1aab7682 100644 --- a/lib/wasix/src/syscalls/wasix/thread_spawn.rs +++ b/lib/wasix/src/syscalls/wasix/thread_spawn.rs @@ -255,12 +255,6 @@ fn handle_thread_result( } Ok(WasiError::Exit(code)) => { trace!(exit_code = ?code, "thread requested exit"); - if !code.is_success() { - // TODO: Why do we need to taint the runtime on a non-zero exit code? Why not also for zero? - env.data(&store) - .runtime - .on_taint(TaintReason::NonZeroExitCode(code)); - }; Ok(Some(code)) } Ok(WasiError::DeepSleep(deep)) => { From 3acacb271ae7e1cfb5c996b32f47a293e0b13b3d Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 6 Aug 2026 16:26:34 -0700 Subject: [PATCH 32/32] fix(api-js-backend): bubble js panics up so errors are inspectable --- .../src/backend/js/entities/function/mod.rs | 26 +++++++++++++++++-- lib/api/src/backend/js/error.rs | 11 +++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/api/src/backend/js/entities/function/mod.rs b/lib/api/src/backend/js/entities/function/mod.rs index 8f6150449498..58b8f243e9ad 100644 --- a/lib/api/src/backend/js/entities/function/mod.rs +++ b/lib/api/src/backend/js/entities/function/mod.rs @@ -34,6 +34,28 @@ use crate::{ use std::panic::{self, AssertUnwindSafe}; +#[derive(Debug)] +struct HostFunctionPanic(String); + +impl std::fmt::Display for HostFunctionPanic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for HostFunctionPanic {} + +fn raise_host_function_panic(payload: Box) -> ! { + let message = if let Some(message) = payload.downcast_ref::<&str>() { + (*message).to_owned() + } else if let Some(message) = payload.downcast_ref::() { + message.clone() + } else { + "host function panicked with a non-string payload".to_owned() + }; + crate::backend::js::error::raise(Box::new(HostFunctionPanic(message))) +} + #[inline] fn wasmer_array_to_js_array(values: &[Value]) -> Array { Array::from_iter(values.iter().map(wasmer_value_to_js)) @@ -736,7 +758,7 @@ macro_rules! impl_host_function { #[cfg(feature = "core")] #[allow(deprecated)] Ok(Err(trap)) => crate::backend::js::error::raise(Box::new(trap)), - Err(_panic) => unimplemented!(), + Err(panic) => raise_host_function_panic(panic), } } @@ -796,7 +818,7 @@ macro_rules! impl_host_function { #[cfg(feature = "core")] #[allow(deprecated)] Ok(Err(trap)) => crate::js::error::raise(Box::new(trap)), - Err(_panic) => unimplemented!(), + Err(panic) => raise_host_function_panic(panic), } } diff --git a/lib/api/src/backend/js/error.rs b/lib/api/src/backend/js/error.rs index 993283e0b800..6bf6b2a93adb 100644 --- a/lib/api/src/backend/js/error.rs +++ b/lib/api/src/backend/js/error.rs @@ -208,7 +208,16 @@ impl From for JsTrap { fn from(value: JsValue) -> Self { // Let's try some easy special cases first if let Some(error) = value.dyn_ref::() { - return Self::Message(error.message().into()); + let message = String::from(error.message()); + let stack = Reflect::get(&value, &JsValue::from_str("stack")) + .ok() + .and_then(|value| value.as_string()) + .unwrap_or_default(); + return Self::Message(if stack.is_empty() || stack == message { + message + } else { + format!("{message}\n{stack}") + }); } if let Some(s) = value.as_string() {