diff --git a/Cargo.lock b/Cargo.lock index 8abc5ae77bc2..0cc4bc977ac9 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", @@ -7310,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", @@ -7963,6 +7967,7 @@ dependencies = [ "wcgi", "wcgi-host", "web-sys", + "web-time", "webc", "windows-sys 0.61.2", "xxhash-rust", 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/api/Cargo.toml b/lib/api/Cargo.toml index a8ef7e6de5cc..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 = [ @@ -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" } @@ -100,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" } @@ -125,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 = [] @@ -158,7 +163,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/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/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..58b8f243e9ad 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,9 +25,37 @@ 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}; +#[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)) @@ -50,6 +84,213 @@ 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 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 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); + + 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, @@ -72,13 +313,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(()) }) @@ -86,13 +329,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])) }) @@ -100,13 +345,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)) }) @@ -247,7 +494,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 => { @@ -255,13 +502,72 @@ 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()) } } } + #[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)?; + 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, + )] + .into_boxed_slice()), + _ => { + let result: Array = result.into(); + Ok(function_type + .results() + .iter() + .enumerate() + .map(|(index, ty)| { + js_value_to_wasmer( + &mut write_lock, + 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 +598,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() @@ -408,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), } } @@ -468,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/entities/function/typed.rs b/lib/api/src/backend/js/entities/function/typed.rs index 229fab787dab..518a8cdad3d9 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 @@ -71,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 => { @@ -79,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); } @@ -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/entities/memory/mod.rs b/lib/api/src/backend/js/entities/memory/mod.rs index 4d8d02b162fa..249dc1730f73 100644 --- a/lib/api/src/backend/js/entities/memory/mod.rs +++ b/lib/api/src/backend/js/entities/memory/mod.rs @@ -16,6 +16,11 @@ use crate::{ vm::{VMExtern, VMExternMemory}, }; +// 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 { pub(crate) handle: VMMemory, @@ -37,8 +42,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 +219,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/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/js/entities/module.rs b/lib/api/src/backend/js/entities/module.rs index 74926a736eaa..e4c34a8f6f25 100644 --- a/lib/api/src/backend/js/entities/module.rs +++ b/lib/api/src/backend/js/entities/module.rs @@ -1,13 +1,18 @@ -use std::path::Path; +use std::{collections::HashMap, 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, - SerializeError, 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::{ @@ -40,12 +45,49 @@ 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")] 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`]. @@ -60,6 +102,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], @@ -91,23 +165,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 +192,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), } @@ -205,8 +282,72 @@ 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_table_functions(store, imports, &instance); + Ok(instance) + } + + #[cfg(feature = "wasm-types-polyfill")] + fn annotate_table_functions( + &self, + store: &mut impl AsStoreMut, + imports: &Imports, + instance: &WebAssembly::Instance, + ) { + 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.as_js().handle.table.clone()); + } + (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); + } + _ => {} + } + } + 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; + }; + 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.get(start.saturating_add(offset as u32)) else { + continue; + }; + crate::js::vm::VMFunction::annotate_type(&function, function_type); + } + } } pub fn name(&self) -> Option<&str> { @@ -458,7 +599,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 +617,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/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 40d224b3d2a7..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,16 +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 = 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, } } @@ -90,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 { @@ -107,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/error.rs b/lib/api/src/backend/js/error.rs index f5d9e2c68aac..6bf6b2a93adb 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 { @@ -160,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() { 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/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/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/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/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/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/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/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/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/src/utils/polyfill.rs b/lib/api/src/utils/polyfill.rs index 320c8316047b..082b47ca73a5 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)?; } @@ -369,55 +375,104 @@ 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) } -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) +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)?)); + } } - 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 + 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:?}" + )); + } + } + } } - 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 + } + + 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" + )); + } + } } - 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) + 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:?}" + )); } - }; - - if uses_externref { - return Err("ExternRef is not supported by this backend yet".to_string()); } } - - Ok(()) + Ok(InitExpr::new(operations.into_boxed_slice())) } /// Helper function translating wasmparser types to Wasm Type. 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 31969694d0c1..8c0676997c5e 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::*; @@ -9,6 +11,97 @@ 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(|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]) + ); +} + +#[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(); diff --git a/lib/api/tests/simple_greenthread.rs b/lib/api/tests/simple_greenthread.rs index 61f8e512be9b..e691c2047d64 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,14 +199,15 @@ 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)?; + let wasm = wat::parse_bytes(wat)?; + let module = Module::new(&store.engine(), wasm)?; let env = FunctionEnv::new(&mut store, GreenEnv::new()); @@ -203,73 +266,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); +} diff --git a/lib/backend-api/Cargo.toml b/lib/backend-api/Cargo.toml index 6911f87e449c..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 +wasmer-package = { workspace = true, features = ["execution", "webc-v1", "webc-v2", "webc-v3"] } webc.workspace = true # crates.io dependencies. diff --git a/lib/c-api-imports/Cargo.toml b/lib/c-api-imports/Cargo.toml index 1cab0fac44c8..2444e963ec4f 100644 --- a/lib/c-api-imports/Cargo.toml +++ b/lib/c-api-imports/Cargo.toml @@ -16,10 +16,20 @@ 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] +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 } + [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..50c71b63e97c 100644 --- a/lib/c-api-imports/src/lib.rs +++ b/lib/c-api-imports/src/lib.rs @@ -1,8 +1,22 @@ use anyhow::{Context, Result, bail}; use std::{ - collections::HashMap, fmt::Display, mem::size_of, num::NonZeroI32, ptr, slice, sync::Arc, + 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, FunctionEnvMut, FunctionType, Global, GlobalType, Imports, Instance, Memory, Memory32, @@ -10,6 +24,9 @@ use wasmer_api::{ TypedFunction, Value, WasmPtr, namespace, }; +#[cfg(not(target_arch = "wasm32"))] +use wasmer_api::SharedMemory; + #[cfg(feature = "wasix")] mod wasix; @@ -88,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" @@ -118,6 +140,7 @@ impl WasmCapiSession { imported_memory_type, imported_table_type, resolve_module_sync, + shared_objects, func_env: None, } } @@ -145,6 +168,7 @@ impl WasmCapiSession { &mut *store, WasmCapiEnv { resolve_module_sync: self.resolve_module_sync.clone(), + shared_objects: self.shared_objects.clone(), ..WasmCapiEnv::default() }, ); @@ -266,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 { @@ -281,6 +307,7 @@ impl std::fmt::Debug for WasmCapiRuntimeHooks { "resolve_module_sync", &self.resolve_module_sync.as_ref().map(|_| ".."), ) + .field("shared_objects", &"..") .finish() } } @@ -318,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 }); } @@ -372,10 +403,21 @@ 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>, } +// 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; @@ -438,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 { @@ -668,11 +844,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(_) => { + return None; + } + } }; if guest_ptr <= INVALID_HANDLE { return None; @@ -739,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 { @@ -1090,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(), @@ -1259,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, @@ -1293,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()), @@ -1302,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 { @@ -1322,32 +2046,28 @@ 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; }; - 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(memory_offset, guest_offset as u64, len as u64, &guest_view) + .is_ok() } fn copy_guest_memory_to_wasmer( env: &mut FunctionEnvMut, guest_ptr: i32, memory: &Memory, + memory_offset: u64, len: usize, ) -> bool { if len == 0 { @@ -1361,23 +2081,18 @@ 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; }; - let guest_base = guest_view.data_ptr(); - let target_base = target_view.data_ptr(); - if ptr::eq(guest_base, target_base) { + if target_end > target_view.data_size() { 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, - ); + if memory == &guest_memory { + return false; } - true + guest_view + .copy_range_to_memory(guest_offset as u64, memory_offset, len as u64, &target_view) + .is_ok() } fn wasm_memory_data_size(env: FunctionEnvMut, memory_handle: i32) -> i32 { @@ -1461,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); } } @@ -1481,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); } } @@ -2336,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), @@ -2358,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), @@ -2430,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, @@ -2802,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(); @@ -2816,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-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/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 diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index e48f19707c83..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 +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/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/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs b/lib/compiler/src/engine/unwind/systemv/compact_unwind/mod.rs index 1ca37e499129..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 - 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)?; } diff --git a/lib/package/Cargo.toml b/lib/package/Cargo.toml index 91bcfb805e0e..12d77a8a6801 100644 --- a/lib/package/Cargo.toml +++ b/lib/package/Cargo.toml @@ -14,34 +14,59 @@ 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 -insta = { workspace = true, features = ["filters", "yaml"] } -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 [dev-dependencies] +insta = { workspace = true, features = ["filters", "yaml"] } pretty_assertions.workspace = true tempfile.workspace = true regex.workspace = true ureq.workspace = true hexdump.workspace = true + +[features] +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 d4df84327ffe..a98df013b928 100644 --- a/lib/package/src/package/mod.rs +++ b/lib/package/src/package/mod.rs @@ -7,28 +7,29 @@ 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 { + #[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 +219,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/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 55d6e62dda95..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)?; @@ -97,6 +123,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 +133,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 +148,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 +162,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 +269,33 @@ 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()); + } +} + +#[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 32b175a201ab..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 +wasmer-package = { workspace = true, features = ["execution", "webc-v1", "webc-v2", "webc-v3"] } [build-dependencies] 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/virtual-fs/Cargo.toml b/lib/virtual-fs/Cargo.toml index 5dc7c30d0ad9..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 +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 @@ -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/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/Cargo.toml b/lib/wasix/Cargo.toml index abb5e71b5694..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 +wasmer-package = { workspace = true, default-features = false, features = ["execution", "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 @@ -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 @@ -211,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", @@ -226,9 +228,16 @@ 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", + "memory64", + "wasmer-package/webc-v1", + "wasmer-package/webc-v2", + "webc/v1", + "webc/v2", ] +package-authoring = ["wasmer-package/authoring"] sys-default = ["sys"] sys-poll = [] extra-logging = [] @@ -238,6 +247,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"] @@ -252,7 +264,6 @@ js = [ "web-sys", "wasmer/js-default", "wasmer/wasm-types-polyfill", - "wasmer/wat", "wasmer/js-serializable-module", ] js-default = ["js"] 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/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index 76b938aeceaf..ecc5f310043f 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -10,7 +10,8 @@ use crate::{ ModuleInput, TaintReason, module_cache::HashedModuleData, task_manager::{ - TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, TaskWasmRunProperties, + LocalTaskSpawner, TaskWasm, TaskWasmRecycle, TaskWasmRecycleProperties, + TaskWasmRunProperties, WasmTaskFuture, }, }, state::context_switching::ContextSwitchingEnvironment, @@ -139,17 +140,15 @@ pub fn spawn_exec_module( // Create a thread that will run this process let tasks_outer = tasks.clone(); + let task = TaskWasm::new(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,9 +179,10 @@ 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; + let local_tasks = props.local_tasks; // Create the WasiFunctionEnv let thread = WasiThreadRunGuard::new(ctx.data(&store).thread.clone()); @@ -232,7 +232,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, local_tasks).await; } fn get_start(ctx: &WasiFunctionEnv, store: &Store) -> Option { @@ -247,12 +247,13 @@ 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, rewind_state: Option<(RewindState, RewindResultType)>, recycle: Option>, + local_tasks: LocalTaskSpawner, ) { let env = ctx.data(&store); let pid = env.pid(); @@ -302,12 +303,18 @@ fn call_module( return; }; - 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; @@ -337,15 +344,15 @@ 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 - call_module( + move |ctx, store, rewind_result, local_tasks| -> WasmTaskFuture { + Box::pin(call_module( ctx, store, handle, Some((rewind, RewindResultType::RewindWithResult(rewind_result))), recycle, - ); + local_tasks, + )) } }; @@ -398,20 +405,25 @@ 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)] -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>, @@ -519,7 +531,9 @@ fn resume_vfork( store, start.clone(), vec![], - ); + local_tasks, + ) + .await; (store, Ok(Some(result))) } err => { diff --git a/lib/wasix/src/bin_factory/mod.rs b/lib/wasix/src/bin_factory/mod.rs index a39afd46cd55..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, @@ -101,7 +102,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 +113,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 +215,175 @@ 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_from_wasi_fs(&name, env) + .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_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 { + 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 +397,162 @@ 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")?; + 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 + } +} - return Ok(Executable::BinaryPackage(Arc::new(pkg))); +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"), } + }; - Ok(Executable::Wasm(buf)) + if let Some(buffer) = buffer { + load_executable_from_buffer(buffer, rt).await } else { - let mut data = Vec::with_capacity(f.size() as usize); - f.read_to_end(&mut data).await.context("Read failed")?; - - let bytes: bytes::Bytes = data.into(); + load_executable_from_filesystem(&env.state.fs.root_fs, &backing_path, rt).await + } +} - 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 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() { + 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"]); + } + + #[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(_))); + } } 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( 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, 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/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/runtime/package_loader/builtin_loader.rs b/lib/wasix/src/runtime/package_loader/builtin_loader.rs index 0ea50d245c3f..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; @@ -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::{ diff --git a/lib/wasix/src/runtime/task_manager/mod.rs b/lib/wasix/src/runtime/task_manager/mod.rs index ebd83c386c57..7c1dda8640bf 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; @@ -14,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::*; @@ -41,7 +47,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 +122,8 @@ pub type WasmResumeTrigger = dyn FnOnce() -> Pin FnOnce( ) -> Pin + Send + 'a>>) + Send; -/// Callback that will be invoked -pub type TaskWasmRun = dyn FnOnce(TaskWasmRunProperties) + Send + 'static; +/// 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; @@ -103,17 +178,21 @@ pub struct TaskWasm { } impl TaskWasm { - pub fn new( - run: Box, + pub fn new( + run: F, env: WasiEnv, module: Module, update_layout: bool, call_initialize: bool, - ) -> Self { + ) -> Self + where + F: FnOnce(TaskWasmRunProperties) -> Fut + Send + 'static, + Fut: Future + 'static, + { 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, @@ -239,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 @@ -308,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, @@ -376,7 +478,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"); @@ -387,8 +489,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/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/context_switching.rs b/lib/wasix/src/state/context_switching.rs index c913e4c1e8d9..cfbad70c7d00 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, @@ -39,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 @@ -92,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()), @@ -105,12 +102,13 @@ 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( + /// 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, + local_tasks: LocalTaskSpawner, ) -> (Store, Result, RuntimeError>) { if !ctx .data(&store) @@ -122,7 +120,6 @@ impl ContextSwitchingEnvironment { return (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 +130,27 @@ 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 { + // 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); } - // 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(local_tasks); 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 result = entrypoint.call_async(&store_async, params).await; - // Process if this was terminated by a context entrypoint returning 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 +162,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 +171,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."); }) }) @@ -319,7 +300,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 /// @@ -367,8 +348,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; } }; @@ -418,8 +399,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 @@ -464,32 +445,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/state/env.rs b/lib/wasix/src/state/env.rs index be2e04b13ee6..ab455a4d0757 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, @@ -18,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, @@ -42,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, @@ -75,6 +99,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 +156,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 +200,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 +261,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 +304,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, @@ -298,10 +328,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 @@ -441,6 +472,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, @@ -542,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 { @@ -663,6 +682,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() @@ -1287,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(); @@ -1332,22 +1359,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 { @@ -1363,3 +1375,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(), + ] + ); + } +} 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/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()); - } - } -} 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")); + } +} 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; 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_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 b6df120a51ce..b9be1aab7682 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); @@ -251,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)) => { @@ -294,12 +292,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 +319,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 +328,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/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); 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 74914f29dc17..000000000000 --- a/lib/wasix/src/utils/thread_local_executor.rs +++ /dev/null @@ -1,96 +0,0 @@ -use futures::{ - executor::{LocalPool, 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 - 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 - 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 { - 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) - } -}