diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7b8a86892b3..543d2a7b6a2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -518,6 +518,11 @@ jobs: build-cmd: "make build-wasmer && make package-wasmer && make tar-wasmer", name: "Build wasmer-cli", }, + { + key: "check", + build-cmd: "make check", + name: "Check wasmer API with all sys features enabled", + }, { key: "api-feats", build-cmd: "make check-api-features", diff --git a/Cargo.lock b/Cargo.lock index ecc81de94c5..3078758a692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5633,6 +5633,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2 0.6.1", @@ -6890,7 +6891,9 @@ dependencies = [ "bytes", "cfg-if", "cmake", + "corosensei", "derive_more 2.0.1", + "futures", "hashbrown 0.11.2", "indexmap 2.12.0", "js-sys", @@ -6907,6 +6910,7 @@ dependencies = [ "target-lexicon 0.13.3", "tempfile", "thiserror 1.0.69", + "tokio", "tracing", "ureq", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index ef987cfb06d..8505e4a4fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ smallvec = "1.6" region = "3.0" once_cell = "1.17.1" num_enum = "0.7.3" +corosensei = "0.3.0" dashmap = "6.0.1" http = "1.0.0" hyper = "1" @@ -174,9 +175,9 @@ clap = { version = "=4.5.50", default-features = false } clap_builder = { version = "=4.5.50" } clap_derive = { version = "=4.5.49" } clap_lex = { version = "=0.7.6" } -which = {version = "8.0.0"} -fs_extra = {version = "1.3.0"} -inkwell = {version = "0.7.0", default-features = false} +which = { version = "8.0.0" } +fs_extra = { version = "1.3.0" } +inkwell = { version = "0.7.0", default-features = false } phf = "0.11.2" dynasm = "4.0.0" dynasmrt = "4.0.0" @@ -274,7 +275,6 @@ iprange = "0.6.7" smoltcp = { version = "0.8", default-features = false } tokio-serde = "0.9" tokio-util = "0.7.8" -corosensei = "0.3.0" crossbeam-queue = "0.3.8" fnv = "1.0.3" scopeguard = "1.1.0" diff --git a/Makefile b/Makefile index 4935a12b91c..336ccc7e352 100644 --- a/Makefile +++ b/Makefile @@ -437,11 +437,14 @@ endif # install will go through. all: build-wasmer build-capi build-capi-headless -check: check-wasmer check-capi +check: check-wasmer check-api-no-async check-capi check-wasmer: $(CARGO_BINARY) check $(CARGO_TARGET_FLAG) --manifest-path lib/cli/Cargo.toml $(compiler_features) --bin wasmer --locked +check-api-no-async: + $(CARGO_BINARY) check $(CARGO_TARGET_FLAG) --manifest-path lib/api/Cargo.toml $(compiler_features) --locked + check-capi: RUSTFLAGS="${RUSTFLAGS}" $(CARGO_BINARY) check $(CARGO_TARGET_FLAG) --manifest-path lib/c-api/Cargo.toml \ --no-default-features --features wat,compiler,wasi,middlewares $(capi_compiler_features) @@ -495,7 +498,7 @@ else endif build-docs: - $(CARGO_BINARY) doc $(CARGO_TARGET_FLAG) --release $(compiler_features) --document-private-items --no-deps --workspace --exclude wasmer-c-api --exclude wasmer-swift --locked + $(CARGO_BINARY) doc $(CARGO_TARGET_FLAG) --release $(compiler_features) --features wasmer/experimental-async --document-private-items --no-deps --workspace --exclude wasmer-c-api --exclude wasmer-swift --locked # The tokio crate was excluded from the docs build because the code (which is not under our control) # does not currently compile its docs successfully @@ -628,8 +631,8 @@ test-stage-0-wast: # test packages test-stage-1-test-all: - $(CARGO_BINARY) nextest run $(CARGO_TARGET_FLAG) --workspace --release $(exclude_tests) --exclude wasmer-c-api-test-runner --exclude wasmer-capi-examples-runner $(compiler_features) --locked && \ - $(CARGO_BINARY) test --doc $(CARGO_TARGET_FLAG) --workspace --release $(exclude_tests) --exclude wasmer-c-api-test-runner --exclude wasmer-capi-examples-runner $(compiler_features) --locked + $(CARGO_BINARY) nextest run $(CARGO_TARGET_FLAG) --workspace --release $(exclude_tests) --exclude wasmer-c-api-test-runner --exclude wasmer-capi-examples-runner $(compiler_features) --features wasmer/experimental-async --locked && \ + $(CARGO_BINARY) test --doc $(CARGO_TARGET_FLAG) --workspace --release $(exclude_tests) --exclude wasmer-c-api-test-runner --exclude wasmer-capi-examples-runner $(compiler_features) --features wasmer/experimental-async --locked test-stage-2-test-compiler-cranelift-nostd: $(CARGO_BINARY) test $(CARGO_TARGET_FLAG) --manifest-path lib/compiler-cranelift/Cargo.toml --release --no-default-features --features=std --locked test-stage-3-test-compiler-singlepass-nostd: diff --git a/examples/throw_exception.rs b/examples/throw_exception.rs index 738842a26ba..b61a0cbe0b0 100644 --- a/examples/throw_exception.rs +++ b/examples/throw_exception.rs @@ -1,8 +1,8 @@ use std::sync::{Arc, atomic::AtomicUsize}; use wasmer::{ - Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Module, RuntimeError, Store, - Type, TypedFunction, Value, imports, sys::Target, wat2wasm, + DynamicFunctionResult, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Module, + RuntimeError, Store, Type, TypedFunction, Value, imports, sys::Target, wat2wasm, }; use wasmer_types::Features; @@ -83,10 +83,7 @@ fn main() -> Result<(), Box> { // Both Function::new_with_env and Function::new_typed_with_env can throw // exceptions if they return a Result with a RuntimeError. - fn throw1( - mut env: FunctionEnvMut, - _params: &[Value], - ) -> Result, RuntimeError> { + fn throw1(mut env: FunctionEnvMut, _params: &[Value]) -> DynamicFunctionResult { println!("Throwing exception 1"); // To "throw" an exception from native code, we create a new one and diff --git a/lib/api/Cargo.toml b/lib/api/Cargo.toml index c1ffdd5994e..a5c0ed4b562 100644 --- a/lib/api/Cargo.toml +++ b/lib/api/Cargo.toml @@ -64,6 +64,8 @@ target-lexicon = { workspace = true, default-features = false } wasmer-compiler-singlepass = { path = "../compiler-singlepass", version = "=6.1.0", optional = true } wasmer-compiler-cranelift = { path = "../compiler-cranelift", version = "=6.1.0", optional = true } wasmer-compiler-llvm = { path = "../compiler-llvm", version = "=6.1.0", optional = true } +corosensei = { workspace = true, optional = true } +futures = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true } js-sys = { workspace = true, optional = true } @@ -79,6 +81,8 @@ wat.workspace = true tempfile.workspace = true anyhow.workspace = true macro-wasmer-universal-test = { version = "6.1.0", path = "./macro-wasmer-universal-test" } +futures = "0.3" +tokio = { workspace = true, features = ["full"] } # Dependencies and Develoment Dependencies for `js`. [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -128,6 +132,9 @@ artifact-size = [ sys = ["std", "dep:wasmer-vm", "dep:wasmer-compiler"] sys-default = ["sys", "wat", "cranelift"] +# Experimental concurrent execution support +experimental-async = ["sys", "dep:futures", "dep:corosensei"] + # - Compilers. compiler = [ "sys", diff --git a/lib/api/src/backend/js/entities/function/mod.rs b/lib/api/src/backend/js/entities/function/mod.rs index d69b21d4243..d37915d03cf 100644 --- a/lib/api/src/backend/js/entities/function/mod.rs +++ b/lib/api/src/backend/js/entities/function/mod.rs @@ -10,9 +10,9 @@ use wasmer_types::{FunctionType, RawValue}; use crate::{ AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnv, BackendFunctionEnvMut, - FromToNativeWasmType, FunctionEnv, FunctionEnvMut, HostFunction, HostFunctionKind, IntoResult, - NativeWasmType, NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, - WithoutEnv, + DynamicCallResult, DynamicFunctionResult, FromToNativeWasmType, FunctionEnv, FunctionEnvMut, + HostFunction, HostFunctionKind, IntoResult, NativeWasmType, NativeWasmTypeInto, RuntimeError, + StoreMut, Value, WasmTypeList, WithEnv, WithoutEnv, js::{ utils::convert::{AsJs as _, js_value_to_wasmer, wasmer_value_to_js}, vm::{VMFuncRef, VMFunctionCallback, function::VMFunction}, @@ -59,10 +59,7 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let mut store = store.as_store_mut(); let function_type = ty.into(); @@ -191,18 +188,14 @@ impl Function { &self, _store: &mut impl AsStoreMut, _params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { // There is no optimal call_raw in JS, so we just // simply rely the call // self.call(store, params) unimplemented!(); } - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { // Annotation is here to prevent spurious IDE warnings. let arr = js_sys::Array::new_with_length(params.len() as u32); diff --git a/lib/api/src/backend/jsc/entities/function/env.rs b/lib/api/src/backend/jsc/entities/function/env.rs index 70e7c7453f0..88739f08a50 100644 --- a/lib/api/src/backend/jsc/entities/function/env.rs +++ b/lib/api/src/backend/jsc/entities/function/env.rs @@ -62,7 +62,7 @@ impl FunctionEnv { } /// Convert it into a `FunctionEnvMut` - pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut + pub fn into_mut<'a>(self, store: &'a mut impl AsStoreMut) -> FunctionEnvMut<'a, T> where T: Any + Send + 'static + Sized, { @@ -137,7 +137,7 @@ impl FunctionEnvMut<'_, T> { } /// Borrows a new mutable reference of both the attached Store and host state - pub fn data_and_store_mut(&mut self) -> (&mut T, StoreMut) { + pub fn data_and_store_mut<'a>(&'a mut self) -> (&'a mut T, StoreMut<'a>) { let data = self.func_env.as_mut(&mut self.store_mut) as *mut T; // telling the borrow check to close his eyes here // this is still relatively safe to do as func_env are diff --git a/lib/api/src/backend/jsc/entities/function/mod.rs b/lib/api/src/backend/jsc/entities/function/mod.rs index fb315e5d855..6f1ea59190c 100644 --- a/lib/api/src/backend/jsc/entities/function/mod.rs +++ b/lib/api/src/backend/jsc/entities/function/mod.rs @@ -9,9 +9,10 @@ use std::panic::{self, AssertUnwindSafe}; use wasmer_types::{FunctionType, RawValue}; use crate::{ - AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, FromToNativeWasmType, - FunctionEnv, FunctionEnvMut, HostFunction, HostFunctionKind, IntoResult, NativeWasmType, - NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, WithoutEnv, + AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, DynamicCallResult, + DynamicFunctionResult, FromToNativeWasmType, FunctionEnv, FunctionEnvMut, HostFunction, + HostFunctionKind, IntoResult, NativeWasmType, NativeWasmTypeInto, RuntimeError, StoreMut, + Value, WasmTypeList, WithEnv, WithoutEnv, jsc::{ store::{InternalStoreHandle, StoreHandle}, utils::convert::{AsJsc, jsc_value_to_wasmer}, @@ -57,10 +58,7 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let store = store.as_store_mut(); let context = store.jsc().context(); @@ -182,18 +180,14 @@ impl Function { &self, _store: &mut impl AsStoreMut, _params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { // There is no optimal call_raw in JSC, so we just // simply rely the call // self.call(store, params) unimplemented!(); } - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { let store_mut = store.as_store_mut(); let engine = store_mut.engine(); let context = engine.as_jsc().context(); @@ -294,7 +288,7 @@ impl std::fmt::Debug for Function { /// Represents a low-level Wasm static host function. See /// `super::Function::new` and `super::Function::new_env` to learn /// more. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Hash)] pub struct WasmFunction { callback: JSObjectCallAsFunctionCallback, _phantom: PhantomData<(Args, Rets)>, diff --git a/lib/api/src/backend/jsc/entities/store/obj.rs b/lib/api/src/backend/jsc/entities/store/obj.rs index e15b1cde400..e840a257a2a 100644 --- a/lib/api/src/backend/jsc/entities/store/obj.rs +++ b/lib/api/src/backend/jsc/entities/store/obj.rs @@ -80,7 +80,7 @@ impl StoreObjects { } /// Return an immutable iterator over all globals - pub fn iter_globals(&self) -> core::slice::Iter { + pub fn iter_globals<'a>(&'a self) -> core::slice::Iter<'a, VMGlobal> { self.globals.iter() } diff --git a/lib/api/src/backend/sys/async_runtime.rs b/lib/api/src/backend/sys/async_runtime.rs new file mode 100644 index 00000000000..5c3aea56cff --- /dev/null +++ b/lib/api/src/backend/sys/async_runtime.rs @@ -0,0 +1,326 @@ +use std::{ + cell::RefCell, + collections::HashMap, + future::Future, + marker::PhantomData, + pin::Pin, + ptr, + rc::Rc, + task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, +}; + +use corosensei::{Coroutine, CoroutineResult, Yielder}; + +use super::entities::function::Function as SysFunction; +use crate::{ + AsStoreMut, AsStoreRef, DynamicCallResult, DynamicFunctionResult, ForcedStoreInstallGuard, + LocalRwLockWriteGuard, RuntimeError, Store, StoreAsync, StoreContext, StoreInner, StoreMut, + StoreRef, Value, +}; +use wasmer_types::StoreId; + +type HostFuture = Pin + 'static>>; + +pub(crate) fn call_function_async( + function: SysFunction, + store: StoreAsync, + params: Vec, +) -> AsyncCallFuture { + AsyncCallFuture::new(function, store, params) +} + +struct AsyncYield(HostFuture); + +enum AsyncResume { + Start, + HostFutureReady(DynamicFunctionResult), +} + +pub(crate) struct AsyncCallFuture { + coroutine: Option>, + pending_store_install: Option>>>, + pending_future: Option, + next_resume: Option, + result: Option, + + // Store handle we can use to lock the store down + store: StoreAsync, +} + +// We can't use any of the existing AsStoreMut types here, since we keep +// changing the store context underneath us while the coroutine yields. +// To work around it, we use this dummy struct, which just grabs the store +// from the store context. Since we always have a store context installed +// when resuming the coroutine, this is safe in that it can access the store +// through the store context. HOWEVER, references returned from this struct +// CAN NOT BE HELD ACROSS A YIELD POINT. We don't do this anywhere in the +// `Function::call code. +struct AsyncCallStoreMut { + store_id: StoreId, +} + +impl AsStoreRef for AsyncCallStoreMut { + fn as_store_ref(&self) -> StoreRef<'_> { + // Safety: This is only used with Function::call, which doesn't store + // the returned reference anywhere, including when calling into WASM + // code. + unsafe { + StoreRef { + inner: StoreContext::get_current_transient(self.store_id) + .as_ref() + .unwrap(), + } + } + } +} + +impl AsStoreMut for AsyncCallStoreMut { + fn as_store_mut(&mut self) -> StoreMut<'_> { + // Safety: This is only used with Function::call, which doesn't store + // the returned reference anywhere, including when calling into WASM + // code. + unsafe { + StoreMut { + inner: StoreContext::get_current_transient(self.store_id) + .as_mut() + .unwrap(), + } + } + } + + fn objects_mut(&mut self) -> &mut crate::StoreObjects { + // Safety: This is only used with Function::call, which doesn't store + // the returned reference anywhere, including when calling into WASM + // code. + unsafe { + &mut StoreContext::get_current_transient(self.store_id) + .as_mut() + .unwrap() + .objects + } + } +} + +impl AsyncCallFuture { + pub(crate) fn new(function: SysFunction, store: StoreAsync, params: Vec) -> Self { + let store_id = store.id; + let coroutine = + Coroutine::new(move |yielder: &Yielder, resume| { + assert!(matches!(resume, AsyncResume::Start)); + + let ctx_state = CoroutineContext::new(yielder); + ctx_state.enter(); + let result = { + let mut store_mut = AsyncCallStoreMut { store_id }; + function.call(&mut store_mut, ¶ms) + }; + ctx_state.leave(); + result + }); + + Self { + coroutine: Some(coroutine), + pending_store_install: None, + pending_future: None, + next_resume: Some(AsyncResume::Start), + result: None, + store, + } + } +} + +impl Future for AsyncCallFuture { + type Output = DynamicCallResult; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + if let Some(future) = self.pending_future.as_mut() { + match future.as_mut().poll(cx) { + Poll::Ready(result) => { + self.pending_future = None; + self.next_resume = Some(AsyncResume::HostFutureReady(result)); + } + Poll::Pending => return Poll::Pending, + } + } + + // If we're ready, return early + if self.coroutine.is_none() { + return Poll::Ready(self.result.take().expect("polled after completion")); + } + + // Start a store installation if not in progress already + if self.pending_store_install.is_none() { + self.pending_store_install = Some(Box::pin(install_store_context(StoreAsync { + id: self.store.id, + inner: self.store.inner.clone(), + }))); + } + + // Acquiring a store lock should be the last step before resuming + // the coroutine, to minimize the time we hold the lock. + let store_context_guard = match self + .pending_store_install + .as_mut() + .unwrap() + .as_mut() + .poll(cx) + { + Poll::Ready(guard) => { + self.pending_store_install = None; + guard + } + Poll::Pending => return Poll::Pending, + }; + + let resume_arg = self.next_resume.take().expect("no resume arg available"); + let coroutine = self.coroutine.as_mut().unwrap(); + match coroutine.resume(resume_arg) { + CoroutineResult::Yield(AsyncYield(fut)) => { + self.pending_future = Some(fut); + } + CoroutineResult::Return(result) => { + self.coroutine = None; + self.result = Some(result); + } + } + + // Uninstall the store context to unlock the store after the coroutine + // yields or returns. + drop(store_context_guard); + } + } +} + +async fn install_store_context(store: StoreAsync) -> ForcedStoreInstallGuard { + match unsafe { crate::StoreContext::try_get_current_async(store.id) } { + crate::GetStoreAsyncGuardResult::NotInstalled => { + // We always need to acquire a new write lock on the store. + let store_guard = store.inner.write().await; + unsafe { crate::StoreContext::install_async(store_guard) } + } + _ => { + // If we're already in a store context, it is unsafe to reuse + // the existing store ref since it'll also be accessible from + // the imported function that tried to poll us, which is a + // double mutable borrow. + // Note to people who discover this code: this *would* be safe + // if we had a separate variation of call_async that just + // used the existing coroutine context instead of spawning a + // new coroutine. However, the current call_async always spawns + // a new coroutine, so we can't allow this; every coroutine + // needs to own its write lock on the store to make sure there + // are no overlapping mutable borrows. If this is something + // you're interested in, feel free to open a GitHub issue outlining + // your use-case. + panic!( + "Function::call_async futures cannot be polled recursively \ + from within another imported function. If you need to await \ + a recursive call_async, consider spawning the future into \ + your async runtime and awaiting the resulting task; \ + e.g. tokio::task::spawn(func.call_async(...)).await" + ); + } + } +} + +pub enum AsyncRuntimeError { + YieldOutsideAsyncContext, + RuntimeError(RuntimeError), +} + +pub(crate) fn block_on_host_future(future: Fut) -> Result, AsyncRuntimeError> +where + Fut: Future + 'static, +{ + CURRENT_CONTEXT.with(|cell| { + match CoroutineContext::get_current() { + None => { + // If there is no async context or we haven't entered it, + // we can still directly run a future that doesn't block + // inline. + run_immediate(future) + } + Some(context) => unsafe { context.as_ref().expect("valid context pointer") } + .block_on_future(Box::pin(future)) + .map_err(AsyncRuntimeError::RuntimeError), + } + }) +} + +thread_local! { + static CURRENT_CONTEXT: RefCell> = const { RefCell::new(Vec::new()) }; +} + +struct CoroutineContext { + yielder: *const Yielder, +} + +impl CoroutineContext { + fn new(yielder: &Yielder) -> Self { + Self { + yielder: yielder as *const _, + } + } + + fn enter(&self) { + CURRENT_CONTEXT.with(|cell| { + let mut borrow = cell.borrow_mut(); + + // Push this coroutine on top of the active stack. + borrow.push(self as *const _); + }) + } + + // Note: we don't use a drop-style guard here on purpose; if a panic + // happens while a coroutine is running, CURRENT_CONTEXT will be in + // an inconsistent state. corosensei will unwind all coroutine stacks + // anyway, and if we had a guard that would get dropped and try to + // leave its context, it'd panic again at the assert_eq! below. + fn leave(&self) { + CURRENT_CONTEXT.with(|cell| { + let mut borrow = cell.borrow_mut(); + + // Pop this coroutine from the active stack. + assert_eq!( + borrow.pop(), + Some(self as *const _), + "Active coroutine stack corrupted" + ); + }); + } + + fn get_current() -> Option<*const Self> { + CURRENT_CONTEXT.with(|cell| cell.borrow().last().copied()) + } + + fn block_on_future(&self, future: HostFuture) -> DynamicFunctionResult { + // Leave the coroutine context since we're yielding back to the + // parent stack, and will be inactive until the future is ready. + self.leave(); + + let yielder = unsafe { self.yielder.as_ref().expect("yielder pointer valid") }; + let result = match yielder.suspend(AsyncYield(future)) { + AsyncResume::HostFutureReady(result) => result, + AsyncResume::Start => unreachable!("coroutine resumed without start"), + }; + + // Once the future is ready, we restore the current coroutine + // context. + self.enter(); + + result + } +} + +fn run_immediate( + future: impl Future + 'static, +) -> Result, AsyncRuntimeError> { + let waker = futures::task::noop_waker(); + let mut cx = Context::from_waker(&waker); + let mut future = Box::pin(future); + match future.as_mut().poll(&mut cx) { + Poll::Ready(result) => result.map_err(AsyncRuntimeError::RuntimeError), + Poll::Pending => Err(AsyncRuntimeError::YieldOutsideAsyncContext), + } +} diff --git a/lib/api/src/backend/sys/entities/function/env.rs b/lib/api/src/backend/sys/entities/function/env.rs index be3be2ac5a3..db823169b83 100644 --- a/lib/api/src/backend/sys/entities/function/env.rs +++ b/lib/api/src/backend/sys/entities/function/env.rs @@ -1,11 +1,13 @@ use std::{any::Any, fmt::Debug, marker::PhantomData}; +#[cfg(feature = "experimental-async")] +use crate::{AsStoreAsync, StoreAsync, StoreAsyncReadLock, StoreAsyncWriteLock}; use crate::{ - StoreMut, + Store, StoreContext, StoreInner, StoreMut, StorePtrWrapper, store::{AsStoreMut, AsStoreRef, StoreRef}, }; -use wasmer_vm::{StoreHandle, StoreObject, StoreObjects, VMFunctionEnvironment}; +use wasmer_vm::{StoreHandle, StoreId, StoreObject, StoreObjects, VMFunctionEnvironment}; #[derive(Debug)] #[repr(transparent)] @@ -16,12 +18,9 @@ pub struct FunctionEnv { marker: PhantomData, } -impl FunctionEnv { +impl FunctionEnv { /// Make a new FunctionEnv - pub fn new(store: &mut impl AsStoreMut, value: T) -> Self - where - T: Any + Send + 'static + Sized, - { + pub fn new(store: &mut impl AsStoreMut, value: T) -> Self { Self { handle: StoreHandle::new( store.as_store_mut().objects_mut().as_sys_mut(), @@ -31,18 +30,6 @@ 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, - { - self.handle - .get(store.as_store_ref().objects().as_sys()) - .as_ref() - .downcast_ref::() - .unwrap() - } - #[allow(dead_code)] // This function is only used in js pub(crate) fn from_handle(handle: StoreHandle) -> Self { Self { @@ -51,28 +38,33 @@ impl FunctionEnv { } } + /// Convert it into a `FunctionEnvMut` + pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut<'_, T> { + FunctionEnvMut { + store_mut: store.as_store_mut(), + func_env: self, + } + } +} + +impl FunctionEnv { + /// Get the data as reference + pub fn as_ref<'a>(&self, store: &'a impl AsStoreRef) -> &'a T { + self.handle + .get(store.as_store_ref().objects().as_sys()) + .as_ref() + .downcast_ref::() + .unwrap() + } + /// 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, - { + pub fn as_mut<'a>(&self, store: &'a mut impl AsStoreMut) -> &'a mut T { self.handle .get_mut(store.objects_mut().as_sys_mut()) .as_mut() .downcast_mut::() .unwrap() } - - /// Convert it into a `FunctionEnvMut` - pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut<'_, T> - where - T: Any + Send + 'static + Sized, - { - FunctionEnvMut { - store_mut: store.as_store_mut(), - func_env: self, - } - } } impl crate::FunctionEnv { @@ -174,6 +166,16 @@ impl FunctionEnvMut<'_, T> { let data = unsafe { &mut *data }; (data, self.store_mut.as_store_mut()) } + + /// Returns a [`StoreAsync`] if the current + /// context is asynchronous. The store will be locked since + /// it's already active in the current context, but can be used + /// to spawn new coroutines via + /// [`Function::call_async`](crate::Function::call_async). + #[cfg(feature = "experimental-async")] + pub fn as_store_async(&self) -> Option { + self.store_mut.as_store_async() + } } impl AsStoreRef for FunctionEnvMut<'_, T> { @@ -207,3 +209,205 @@ impl From> for crate::FunctionEnv { Self(crate::BackendFunctionEnv::Sys(value)) } } + +/// A shared handle to a [`FunctionEnv`], suitable for use +/// in async imports. +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvMut { + pub(crate) store: AsyncFunctionEnvMutStore, + pub(crate) func_env: FunctionEnv, +} + +// We need to let async functions that *don't suspend* run +// in a sync context. To that end, `AsyncFunctionEnvMut` +// must be able to be constructed without an actual +// StoreAsync instance, hence this enum. +#[cfg(feature = "experimental-async")] +pub(crate) enum AsyncFunctionEnvMutStore { + Async(StoreAsync), + Sync(StorePtrWrapper), +} + +/// A read-only handle to the [`FunctionEnv`] in an [`AsyncFunctionEnvMut`]. +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandle { + read_lock: StoreAsyncReadLock, + pub(crate) func_env: FunctionEnv, +} + +/// A mutable handle to the [`FunctionEnv`] in an [`AsyncFunctionEnvMut`]. +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandleMut { + write_lock: StoreAsyncWriteLock, + pub(crate) func_env: FunctionEnv, +} + +#[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 { + AsyncFunctionEnvMutStore::Sync(ptr) => self.func_env.as_ref(&ptr.as_ref()).fmt(f), + AsyncFunctionEnvMutStore::Async(store) => match 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 { + match &self.store { + AsyncFunctionEnvMutStore::Sync(ptr) => ptr.as_ref().objects().id(), + AsyncFunctionEnvMutStore::Async(store) => store.id, + } + } + + /// Waits for a store lock and returns a read-only handle to the + /// function environment. + pub async fn read(&self) -> AsyncFunctionEnvHandle { + let read_lock = match &self.store { + AsyncFunctionEnvMutStore::Async(store) => store.read_lock().await, + + // We can never acquire a store lock in a sync context + AsyncFunctionEnvMutStore::Sync(_) => futures::future::pending().await, + }; + + AsyncFunctionEnvHandle { + read_lock, + func_env: self.func_env.clone(), + } + } + + /// Waits for a store lock and returns a mutable handle to the + /// function environment. + pub async fn write(&self) -> AsyncFunctionEnvHandleMut { + let write_lock = match &self.store { + AsyncFunctionEnvMutStore::Async(store) => store.write_lock().await, + + // We can never acquire a store lock in a sync context + AsyncFunctionEnvMutStore::Sync(_) => futures::future::pending().await, + }; + + AsyncFunctionEnvHandleMut { + write_lock, + func_env: self.func_env.clone(), + } + } + + /// Borrows a new immmutable reference + pub fn as_ref(&self) -> FunctionEnv { + self.func_env.clone() + } + + /// Borrows a new mutable reference + pub fn as_mut(&mut self) -> Self { + self.clone() + } + + /// Creates an [`AsStoreAsync`] from this [`AsyncFunctionEnvMut`]. + pub fn as_store_async(&self) -> impl AsStoreAsync + 'static { + match &self.store { + AsyncFunctionEnvMutStore::Sync(_) => { + panic!("Cannot build a StoreAsync within a sync context") + } + AsyncFunctionEnvMutStore::Async(store) => StoreAsync { + id: store.id, + inner: store.inner.clone(), + }, + } + } +} + +#[cfg(feature = "experimental-async")] +impl Clone for AsyncFunctionEnvMut { + fn clone(&self) -> Self { + Self { + store: self.store.clone(), + func_env: self.func_env.clone(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl Clone for AsyncFunctionEnvMutStore { + fn clone(&self) -> Self { + match self { + Self::Async(store) => Self::Async(StoreAsync { + id: store.id, + inner: store.inner.clone(), + }), + Self::Sync(ptr) => Self::Sync(ptr.clone()), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandle { + /// Returns a reference to the host state in this function environment. + pub fn data(&self) -> &T { + self.func_env.as_ref(&self.read_lock) + } + + /// Returns both the host state and the attached StoreRef + 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<'_> { + AsStoreRef::as_store_ref(&self.read_lock) + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandleMut { + /// Returns a mutable reference to the host state in this function environment. + pub fn data_mut(&mut self) -> &mut T { + self.func_env.as_mut(&mut self.write_lock) + } + + /// Returns both the host state and the attached StoreMut + pub fn data_and_store_mut(&mut self) -> (&mut T, &mut impl AsStoreMut) { + let data = self.data_mut() as *mut T; + // Wisdom of the ancients: + // telling the borrow check to close his eyes here + // this is still relatively safe to do as func_env are + // stored in a specific vec of Store, separate from the other objects + // and not really directly accessible with the StoreMut + let data = unsafe { &mut *data }; + (data, &mut self.write_lock) + } + + /// Borrows a new [`FunctionEnvMut`] from this [`AsyncFunctionEnvHandleMut`]. + 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<'_> { + AsStoreRef::as_store_ref(&self.write_lock) + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreMut for AsyncFunctionEnvHandleMut { + fn as_store_mut(&mut self) -> StoreMut<'_> { + AsStoreMut::as_store_mut(&mut self.write_lock) + } + + fn objects_mut(&mut self) -> &mut crate::StoreObjects { + AsStoreMut::objects_mut(&mut self.write_lock) + } +} diff --git a/lib/api/src/backend/sys/entities/function/mod.rs b/lib/api/src/backend/sys/entities/function/mod.rs index ca87731996d..a26f79e14ba 100644 --- a/lib/api/src/backend/sys/entities/function/mod.rs +++ b/lib/api/src/backend/sys/entities/function/mod.rs @@ -3,21 +3,34 @@ pub(crate) mod env; pub(crate) mod typed; +#[cfg(feature = "experimental-async")] use crate::{ - BackendFunction, FunctionEnv, FunctionEnvMut, FunctionType, HostFunction, RuntimeError, - StoreInner, Value, WithEnv, WithoutEnv, + AsStoreAsync, AsyncFunctionEnvMut, BackendAsyncFunctionEnvMut, StoreAsync, + entities::function::async_host::{AsyncFunctionEnv, AsyncHostFunction}, + sys::{ + async_runtime::{AsyncRuntimeError, block_on_host_future, call_function_async}, + function::env::AsyncFunctionEnvMutStore, + }, +}; +use crate::{ + BackendFunction, DynamicCallResult, DynamicFunctionResult, FunctionEnv, FunctionEnvMut, + FunctionType, HostFunction, RuntimeError, StoreContext, StoreInner, Value, WithEnv, WithoutEnv, backend::sys::{engine::NativeEngineExt, vm::VMFunctionCallback}, entities::store::{AsStoreMut, AsStoreRef, StoreMut}, utils::{FromToNativeWasmType, IntoResult, NativeWasmTypeInto, WasmTypeList}, vm::{VMExtern, VMExternFunction}, }; use std::panic::{self, AssertUnwindSafe}; -use std::{cell::UnsafeCell, cmp::max, error::Error, ffi::c_void}; -use wasmer_types::{NativeWasmType, RawValue}; +use std::{ + cell::UnsafeCell, cmp::max, error::Error, ffi::c_void, future::Future, marker::PhantomData, + pin::Pin, sync::Arc, +}; +use wasmer_types::{NativeWasmType, RawValue, StoreId}; use wasmer_vm::{ - MaybeInstanceOwned, StoreHandle, VMCallerCheckedAnyfunc, VMContext, VMDynamicFunctionContext, - VMFuncRef, VMFunction, VMFunctionBody, VMFunctionContext, VMFunctionKind, VMTrampoline, - on_host_stack, raise_user_trap, resume_panic, wasmer_call_trampoline, + MaybeInstanceOwned, StoreHandle, Trap, TrapCode, VMCallerCheckedAnyfunc, VMContext, + VMDynamicFunctionContext, VMFuncRef, VMFunction, VMFunctionBody, VMFunctionContext, + VMFunctionKind, VMTrampoline, on_host_stack, raise_lib_trap, raise_user_trap, resume_panic, + wasmer_call_trampoline, }; #[cfg_attr(feature = "artifact-size", derive(loupe::MemoryUsage))] @@ -42,56 +55,43 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let function_type = ty.into(); let func_ty = function_type.clone(); let func_env = env.clone().into_sys(); - let raw_store = store.as_store_mut().as_raw() as *mut u8; - let wrapper = move |values_vec: *mut RawValue| -> Result<(), RuntimeError> { + let store_id = store.objects_mut().id(); + let wrapper = move |values_vec: *mut RawValue| -> HostCallOutcome { unsafe { - let mut store = StoreMut::from_raw(raw_store as *mut StoreInner); + let mut store_wrapper = unsafe { StoreContext::get_current(store_id) }; + let mut store_mut = store_wrapper.as_mut(); let mut args = Vec::with_capacity(func_ty.params().len()); for (i, ty) in func_ty.params().iter().enumerate() { args.push(Value::from_raw( - &mut store, + &mut store_mut, *ty, values_vec.add(i).read_unaligned(), )); } - let store_mut = StoreMut::from_raw(raw_store as *mut StoreInner); let env = env::FunctionEnvMut { store_mut, func_env: func_env.clone(), } .into(); - let returns = func(env, &args)?; - - // We need to dynamically check that the returns - // match the expected types, as well as expected length. - let return_types = returns.iter().map(|ret| ret.ty()); - if return_types.ne(func_ty.results().iter().copied()) { - return Err(RuntimeError::new(format!( - "Dynamic function returned wrong signature. Expected {:?} but got {:?}", - func_ty.results(), - returns.iter().map(|ret| ret.ty()) - ))); - } - for (i, ret) in returns.iter().enumerate() { - values_vec.add(i).write_unaligned(ret.as_raw(&store)); + let sig = func_ty.clone(); + let result = func(env, &args); + HostCallOutcome::Ready { + func_ty: sig, + result, } } - Ok(()) }; let mut host_data = Box::new(VMDynamicFunctionContext { address: std::ptr::null(), ctx: DynamicFunction { func: wrapper, - raw_store, + store_id, }, }); host_data.address = host_data.ctx.func_body_ptr(); @@ -101,7 +101,7 @@ impl Function { // generated dynamic trampoline. let func_ptr = std::ptr::null() as VMFunctionCallback; let type_index = store - .as_store_mut() + .as_store_ref() .engine() .as_sys() .register_signature(&function_type); @@ -123,7 +123,122 @@ impl Function { host_data, }; Self { - handle: StoreHandle::new(store.as_store_mut().objects_mut().as_sys_mut(), vm_function), + handle: StoreHandle::new(store.objects_mut().as_sys_mut(), vm_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 + 'static, + { + let env = FunctionEnv::new(store, ()); + let wrapped = move |_env: AsyncFunctionEnvMut<()>, values: &[Value]| func(values); + Self::new_with_env_async(store, &env, ty, wrapped) + } + + #[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 + 'static, + { + let function_type = ty.into(); + let func_ty = function_type.clone(); + let func_env = env.clone().into_sys(); + let store_id = store.objects_mut().id(); + let wrapper = move |values_vec: *mut RawValue| -> HostCallOutcome { + unsafe { + let mut context = StoreContext::try_get_current_async(store_id); + let mut store_mut = match &mut context { + crate::GetStoreAsyncGuardResult::Ok(wrapper) => StoreMut { + inner: wrapper.guard.as_mut().unwrap(), + }, + crate::GetStoreAsyncGuardResult::NotAsync(ptr) => ptr.as_mut(), + crate::GetStoreAsyncGuardResult::NotInstalled => { + panic!("No store context installed on this thread") + } + }; + let id = store_mut.as_store_ref().objects().id(); + let mut args = Vec::with_capacity(func_ty.params().len()); + + for (i, ty) in func_ty.params().iter().enumerate() { + args.push(Value::from_raw( + &mut store_mut, + *ty, + values_vec.add(i).read_unaligned(), + )); + } + let store_async = match context { + crate::GetStoreAsyncGuardResult::Ok(wrapper) => { + AsyncFunctionEnvMutStore::Async(StoreAsync { + id, + inner: crate::LocalRwLockWriteGuard::lock_handle( + wrapper.guard.as_mut().unwrap(), + ), + }) + } + crate::GetStoreAsyncGuardResult::NotAsync(ptr) => { + AsyncFunctionEnvMutStore::Sync(ptr) + } + crate::GetStoreAsyncGuardResult::NotInstalled => unreachable!(), + }; + let env = crate::AsyncFunctionEnvMut(crate::BackendAsyncFunctionEnvMut::Sys( + env::AsyncFunctionEnvMut { + store: store_async, + func_env: func_env.clone(), + }, + )); + let sig = func_ty.clone(); + let future = func(env, &args); + HostCallOutcome::Future { + func_ty: sig, + future: Box::pin(future), + } + } + }; + let mut host_data = Box::new(VMDynamicFunctionContext { + address: std::ptr::null(), + ctx: DynamicFunction { + func: wrapper, + store_id, + }, + }); + host_data.address = host_data.ctx.func_body_ptr(); + + let func_ptr = std::ptr::null() as VMFunctionCallback; + let type_index = store + .as_store_ref() + .engine() + .as_sys() + .register_signature(&function_type); + let vmctx = VMFunctionContext { + host_env: host_data.as_ref() as *const _ as *mut c_void, + }; + let call_trampoline = host_data.ctx.call_trampoline_address(); + let anyfunc = VMCallerCheckedAnyfunc { + func_ptr, + type_index, + vmctx, + call_trampoline, + }; + + let vm_function = VMFunction { + anyfunc: MaybeInstanceOwned::Host(Box::new(UnsafeCell::new(anyfunc))), + kind: VMFunctionKind::Dynamic, + signature: function_type, + host_data, + }; + Self { + handle: StoreHandle::new(store.objects_mut().as_sys_mut(), vm_function), } } @@ -137,14 +252,14 @@ impl Function { let env = FunctionEnv::new(store, ()); let func_ptr = func.function_callback_sys().into_sys(); let host_data = Box::new(StaticFunction { - raw_store: store.as_store_mut().as_raw() as *mut u8, + store_id: store.objects_mut().id(), env, func, }); let function_type = FunctionType::new(Args::wasm_types(), Rets::wasm_types()); let type_index = store - .as_store_mut() + .as_store_ref() .engine() .as_sys() .register_signature(&function_type); @@ -167,10 +282,113 @@ impl Function { host_data, }; Self { - handle: StoreHandle::new(store.as_store_mut().objects_mut().as_sys_mut(), vm_function), + handle: StoreHandle::new(store.objects_mut().as_sys_mut(), 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>> { + let sys_env = match env_mut.0 { + BackendAsyncFunctionEnvMut::Sys(ref mut sys_env) => sys_env, + _ => panic!("Not a sys backend"), + }; + let mut store_mut_wrapper = + unsafe { StoreContext::get_current(sys_env.store_id()) }; + let mut store_mut = store_mut_wrapper.as_mut(); + let args_sig = args_sig.clone(); + let results_sig = results_sig.clone(); + let func = func.clone(); + let args = + match typed_args_from_values::(&mut store_mut, args_sig.as_ref(), values) + { + Ok(args) => args, + Err(err) => return Box::pin(async { Err(err) }), + }; + drop(store_mut_wrapper); + 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>> { + let sys_env = match env_mut.0 { + BackendAsyncFunctionEnvMut::Sys(ref mut sys_env) => sys_env, + _ => panic!("Not a sys backend"), + }; + let mut store_mut_wrapper = + unsafe { StoreContext::get_current(sys_env.store_id()) }; + let mut store_mut = store_mut_wrapper.as_mut(); + let args_sig = args_sig.clone(); + let results_sig = results_sig.clone(); + let func = func.clone(); + let args = + match typed_args_from_values::(&mut store_mut, args_sig.as_ref(), values) + { + Ok(args) => args, + Err(err) => return Box::pin(async { Err(err) }), + }; + drop(store_mut_wrapper); + let env_mut_clone = env_mut.as_mut(); + 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, + ) + }) + }, + ) + } + pub(crate) fn new_typed_with_env( store: &mut impl AsStoreMut, env: &FunctionEnv, @@ -183,14 +401,14 @@ impl Function { { let func_ptr = func.function_callback_sys().into_sys(); let host_data = Box::new(StaticFunction { - raw_store: store.as_store_mut().as_raw() as *mut u8, + store_id: store.objects_mut().id(), env: env.as_sys().clone().into(), func, }); let function_type = FunctionType::new(Args::wasm_types(), Rets::wasm_types()); let type_index = store - .as_store_mut() + .as_store_ref() .engine() .as_sys() .register_signature(&function_type); @@ -213,7 +431,7 @@ impl Function { host_data, }; Self { - handle: StoreHandle::new(store.as_store_mut().objects_mut().as_sys_mut(), vm_function), + handle: StoreHandle::new(store.objects_mut().as_sys_mut(), vm_function), } } @@ -287,15 +505,25 @@ impl Function { ) -> Result<(), RuntimeError> { // Call the trampoline. let result = { + let store_id = store.objects_mut().id(); + // Safety: the store context is uninstalled before we return, and the + // store mut is valid for the duration of the call. + let store_install_guard = + unsafe { StoreContext::ensure_installed(store.as_store_mut().inner as *mut _) }; + let mut r; // TODO: This loop is needed for asyncify. It will be refactored with https://github.com/wasmerio/wasmer/issues/3451 loop { let storeref = store.as_store_ref(); let vm_function = self.handle.get(storeref.objects().as_sys()); let config = storeref.engine().tunables().vmconfig(); + let signal_handler = storeref.signal_handler(); r = unsafe { + // Safety: This is the intended use-case for StoreContext::pause, as + // documented in the function's doc comments. + let pause_guard = StoreContext::pause(store_id); wasmer_call_trampoline( - store.as_store_ref().signal_handler(), + signal_handler, config, vm_function.anyfunc.as_ptr().as_ref().vmctx, trampoline, @@ -320,8 +548,12 @@ impl Function { } break; } + + drop(store_install_guard); + r }; + if let Err(error) = result { return Err(error.into()); } @@ -341,14 +573,10 @@ impl Function { self.ty(store).results().len() } - pub(crate) fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub(crate) fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { let trampoline = unsafe { self.handle - .get(store.as_store_ref().objects().as_sys()) + .get(store.objects_mut().as_sys()) .anyfunc .as_ptr() .as_ref() @@ -359,16 +587,27 @@ impl Function { Ok(results.into_boxed_slice()) } + #[cfg(feature = "experimental-async")] + pub(crate) fn call_async( + &self, + store: &impl AsStoreAsync, + params: Vec, + ) -> Pin + 'static>> { + let function = self.clone(); + let store = store.store(); + Box::pin(call_function_async(function, store, params)) + } + #[doc(hidden)] #[allow(missing_docs)] pub(crate) fn call_raw( &self, store: &mut impl AsStoreMut, params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { let trampoline = unsafe { self.handle - .get(store.as_store_ref().objects().as_sys()) + .get(store.objects_mut().as_sys()) .anyfunc .as_ptr() .as_ref() @@ -391,7 +630,7 @@ impl Function { let signature = { let anyfunc = unsafe { funcref.0.as_ref() }; store - .as_store_ref() + .as_store_mut() .engine() .as_sys() .lookup_signature(anyfunc.type_index) @@ -413,10 +652,7 @@ impl Function { pub(crate) fn from_vm_extern(store: &mut impl AsStoreMut, vm_extern: VMExternFunction) -> Self { Self { handle: unsafe { - StoreHandle::from_internal( - store.as_store_ref().objects().id(), - vm_extern.into_sys(), - ) + StoreHandle::from_internal(store.objects_mut().id(), vm_extern.into_sys()) }, } } @@ -440,6 +676,7 @@ enum InvocationResult { Success(T), Exception(crate::Exception), Trap(Box), + YieldOutsideAsyncContext, } fn to_invocation_result(result: Result) -> InvocationResult @@ -460,15 +697,111 @@ where } } +fn write_dynamic_results( + store_id: StoreId, + func_ty: &FunctionType, + returns: Vec, + values_vec: *mut RawValue, +) -> Result<(), RuntimeError> { + let mut store_wrapper = unsafe { StoreContext::get_current(store_id) }; + let mut store = store_wrapper.as_mut(); + let return_types = returns.iter().map(|ret| ret.ty()); + if return_types.ne(func_ty.results().iter().copied()) { + return Err(RuntimeError::new(format!( + "Dynamic function returned wrong signature. Expected {:?} but got {:?}", + func_ty.results(), + returns.iter().map(|ret| ret.ty()) + ))); + } + for (i, ret) in returns.iter().enumerate() { + unsafe { + values_vec.add(i).write_unaligned(ret.as_raw(&store)); + } + } + Ok(()) +} + +fn finalize_dynamic_call( + store_id: StoreId, + func_ty: FunctionType, + values_vec: *mut RawValue, + result: DynamicFunctionResult, +) -> Result<(), RuntimeError> { + match result { + Ok(values) => write_dynamic_results(store_id, &func_ty, values, values_vec), + Err(err) => Err(err), + } +} + +fn typed_args_from_values( + store: &mut StoreMut, + func_ty: &FunctionType, + values: &[Value], +) -> Result +where + Args: WasmTypeList, +{ + if values.len() != func_ty.params().len() { + return Err(RuntimeError::new( + "typed host function received wrong number of parameters", + )); + } + let mut raw_array = Args::empty_array(); + for ((slot, value), expected_ty) in raw_array + .as_mut() + .iter_mut() + .zip(values.iter()) + .zip(func_ty.params().iter()) + { + debug_assert_eq!( + value.ty(), + *expected_ty, + "wasm should only call host functions with matching signatures" + ); + *slot = value.as_raw(store); + } + unsafe { Ok(Args::from_array(store, raw_array)) } +} + +fn typed_results_to_values( + store: &mut StoreMut, + func_ty: &FunctionType, + rets: Rets, +) -> DynamicFunctionResult +where + Rets: WasmTypeList, +{ + let mut raw_array = unsafe { rets.into_array(store) }; + let mut values = Vec::with_capacity(func_ty.results().len()); + for (raw, ty) in raw_array.as_mut().iter().zip(func_ty.results().iter()) { + unsafe { + values.push(Value::from_raw(store, *ty, *raw)); + } + } + Ok(values) +} + +pub(crate) enum HostCallOutcome { + Ready { + func_ty: FunctionType, + result: DynamicFunctionResult, + }, + #[cfg(feature = "experimental-async")] + Future { + func_ty: FunctionType, + future: Pin>>, + }, +} + /// Host state for a dynamic function. pub(crate) struct DynamicFunction { func: F, - raw_store: *mut u8, + store_id: StoreId, } impl DynamicFunction where - F: Fn(*mut RawValue) -> Result<(), RuntimeError> + 'static, + F: Fn(*mut RawValue) -> HostCallOutcome + 'static, { // This function wraps our func, to make it compatible with the // reverse trampoline signature @@ -477,8 +810,27 @@ where values_vec: *mut RawValue, ) { let result = on_host_stack(|| { - panic::catch_unwind(AssertUnwindSafe(|| { - to_invocation_result((this.ctx.func)(values_vec)) + panic::catch_unwind(AssertUnwindSafe(|| match (this.ctx.func)(values_vec) { + HostCallOutcome::Ready { func_ty, result } => to_invocation_result( + finalize_dynamic_call(this.ctx.store_id, func_ty, values_vec, result), + ), + #[cfg(feature = "experimental-async")] + HostCallOutcome::Future { func_ty, future } => { + let awaited = block_on_host_future(future); + let result = match awaited { + Ok(value) => Ok(value), + Err(AsyncRuntimeError::RuntimeError(e)) => Err(e), + Err(AsyncRuntimeError::YieldOutsideAsyncContext) => { + return InvocationResult::YieldOutsideAsyncContext; + } + }; + to_invocation_result(finalize_dynamic_call( + this.ctx.store_id, + func_ty, + values_vec, + result, + )) + } })) }); @@ -488,13 +840,20 @@ where match result { Ok(InvocationResult::Success(())) => {} Ok(InvocationResult::Exception(exception)) => unsafe { - let store = StoreMut::from_raw(this.ctx.raw_store as *mut _); + // Note: can't acquire a proper ref-counted context ref here, since we can switch + // away from the WASM stack at any time. + // Safety: The pointer is only used for the duration of the call to `throw`. + let mut store_wrapper = StoreContext::get_current_transient(this.ctx.store_id); + let mut store = store_wrapper.as_mut().unwrap(); wasmer_vm::libcalls::throw( - store.as_store_ref().objects().as_sys(), + store.objects.as_sys(), exception.vm_exceptionref().as_sys().to_u32_exnref(), ) }, Ok(InvocationResult::Trap(trap)) => unsafe { raise_user_trap(trap) }, + Ok(InvocationResult::YieldOutsideAsyncContext) => unsafe { + raise_lib_trap(Trap::lib(TrapCode::YieldOutsideAsyncContext)) + }, Err(panic) => unsafe { resume_panic(panic) }, } } @@ -525,7 +884,7 @@ where /// [`crate::Function::new_typed`] and /// [`crate::Function::new_typed_with_env`] to learn more. pub(crate) struct StaticFunction { - pub(crate) raw_store: *mut u8, + pub(crate) store_id: StoreId, pub(crate) env: FunctionEnv, pub(crate) func: F, } @@ -573,9 +932,10 @@ macro_rules! impl_host_function { RetsAsResult: IntoResult, Func: Fn($( $x , )*) -> RetsAsResult + 'static, { - let mut store = unsafe { StoreMut::from_raw(env.raw_store as *mut _) }; let result = on_host_stack(|| { panic::catch_unwind(AssertUnwindSafe(|| { + let mut store_wrapper = unsafe { StoreContext::get_current(env.store_id) }; + let mut store = store_wrapper.as_mut(); $( let $x = unsafe { FromToNativeWasmType::from_native(NativeWasmTypeInto::from_abi(&mut store, $x)) @@ -589,18 +949,29 @@ macro_rules! impl_host_function { // AS WE ARE IN THE WASM STACK, NOT ON THE HOST ONE. // See: https://github.com/wasmerio/wasmer/pull/5700 match result { - Ok(InvocationResult::Success(result)) => { - unsafe { - return result.into_c_struct(&mut store); - } + Ok(InvocationResult::Success(result)) => unsafe { + // Note: can't acquire a proper ref-counted context ref here, since we can switch + // away from the WASM stack at any time. + // Safety: The pointer is only used for the duration of the call to `into_c_struct`. + let mut store_wrapper = StoreContext::get_current_transient(env.store_id); + let mut store = store_wrapper.as_mut().unwrap(); + return result.into_c_struct(store); }, Ok(InvocationResult::Exception(exception)) => unsafe { + // Note: can't acquire a proper ref-counted context ref here, since we can switch + // away from the WASM stack at any time. + // Safety: The pointer is only used for the duration of the call to `throw`. + let mut store_wrapper = StoreContext::get_current_transient(env.store_id); + let mut store = store_wrapper.as_mut().unwrap(); wasmer_vm::libcalls::throw( - store.as_store_ref().objects().as_sys(), + store.objects.as_sys(), exception.vm_exceptionref().as_sys().to_u32_exnref() ) } Ok(InvocationResult::Trap(trap)) => unsafe { raise_user_trap(trap) }, + Ok(InvocationResult::YieldOutsideAsyncContext) => unsafe { + raise_lib_trap(Trap::lib(TrapCode::YieldOutsideAsyncContext)) + }, Err(panic) => unsafe { resume_panic(panic) }, } } @@ -651,18 +1022,17 @@ macro_rules! impl_host_function { RetsAsResult: IntoResult, Func: Fn(FunctionEnvMut, $( $x , )*) -> RetsAsResult + 'static, { - - let mut store = unsafe { StoreMut::from_raw(env.raw_store as *mut _) }; let result = wasmer_vm::on_host_stack(|| { panic::catch_unwind(AssertUnwindSafe(|| { + let mut store_wrapper = unsafe { StoreContext::get_current(env.store_id) }; + let mut store = store_wrapper.as_mut(); $( let $x = unsafe { FromToNativeWasmType::from_native(NativeWasmTypeInto::from_abi(&mut store, $x)) }; )* - let store_mut = unsafe { StoreMut::from_raw(env.raw_store as *mut _) }; let f_env = crate::backend::sys::function::env::FunctionEnvMut { - store_mut, + store_mut: store, func_env: env.env.as_sys().clone(), }.into(); to_invocation_result((env.func)(f_env, $($x),* ).into_result()) @@ -673,18 +1043,29 @@ macro_rules! impl_host_function { // AS WE ARE IN THE WASM STACK, NOT ON THE HOST ONE. // See: https://github.com/wasmerio/wasmer/pull/5700 match result { - Ok(InvocationResult::Success(result)) => { - unsafe { - return result.into_c_struct(&mut store); - } + Ok(InvocationResult::Success(result)) => unsafe { + // Note: can't acquire a proper ref-counted context ref here, since we can switch + // away from the WASM stack at any time. + // Safety: The pointer is only used for the duration of the call to `into_c_struct`. + let mut store_wrapper = StoreContext::get_current_transient(env.store_id); + let mut store = store_wrapper.as_mut().unwrap(); + return result.into_c_struct(store); }, Ok(InvocationResult::Exception(exception)) => unsafe { + // Note: can't acquire a proper ref-counted context ref here, since we can switch + // away from the WASM stack at any time. + // Safety: The pointer is only used for the duration of the call to `throw`. + let mut store_wrapper = StoreContext::get_current_transient(env.store_id); + let mut store = store_wrapper.as_mut().unwrap(); wasmer_vm::libcalls::throw( - store.as_store_ref().objects().as_sys(), + store.objects.as_sys(), exception.vm_exceptionref().as_sys().to_u32_exnref() ) } Ok(InvocationResult::Trap(trap)) => unsafe { raise_user_trap(trap) }, + Ok(InvocationResult::YieldOutsideAsyncContext) => unsafe { + raise_lib_trap(Trap::lib(TrapCode::YieldOutsideAsyncContext)) + }, Err(panic) => unsafe { resume_panic(panic) }, } } diff --git a/lib/api/src/backend/sys/entities/function/typed.rs b/lib/api/src/backend/sys/entities/function/typed.rs index 1bae7786ab7..23f1a054c1b 100644 --- a/lib/api/src/backend/sys/entities/function/typed.rs +++ b/lib/api/src/backend/sys/entities/function/typed.rs @@ -1,7 +1,13 @@ use crate::backend::sys::engine::NativeEngineExt; -use crate::store::{AsStoreMut, AsStoreRef}; -use crate::{FromToNativeWasmType, NativeWasmTypeInto, RuntimeError, TypedFunction, WasmTypeList}; -use wasmer_types::RawValue; +use crate::{ + FromToNativeWasmType, Function, NativeWasmTypeInto, RuntimeError, StoreContext, TypedFunction, + Value, WasmTypeList, + store::{AsStoreMut, AsStoreRef}, +}; +#[cfg(feature = "experimental-async")] +use crate::{StoreAsync, store::AsStoreAsync}; +use std::future::Future; +use wasmer_types::{FunctionType, RawValue, Type}; macro_rules! impl_native_traits { ( $( $x:ident ),* ) => { @@ -46,11 +52,20 @@ macro_rules! impl_native_traits { rets_list.as_mut() }; + let store_id = store.objects_mut().id(); + // Install the store into the store context + let store_install_guard = unsafe { + StoreContext::ensure_installed(store.as_store_mut().inner as *mut _) + }; + let mut r; loop { let storeref = store.as_store_ref(); let config = storeref.engine().tunables().vmconfig(); r = unsafe { + // Safety: This is the intended use-case for StoreContext::pause, as + // documented in the function's doc comments. + let pause_guard = StoreContext::pause(store_id); wasmer_vm::wasmer_call_trampoline( store.as_store_ref().signal_handler(), config, @@ -71,6 +86,9 @@ macro_rules! impl_native_traits { } break; } + + drop(store_install_guard); + r?; let num_rets = rets_list.len(); @@ -99,6 +117,54 @@ macro_rules! impl_native_traits { // Ok(Rets::from_c_struct(results)) } + #[allow(unused_mut)] + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "experimental-async")] + pub(crate) fn call_async_sys( + &self, + store: &impl AsStoreAsync, + $( $x: $x, )* + ) -> impl Future> + 'static + where + $( $x: FromToNativeWasmType + 'static, )* + { + let func = self.func.clone(); + let store = store.store(); + Self::call_async_sys_internal(func, store, $($x),*) + } + + /// Call the typed func asynchronously. + #[allow(unused_mut)] + #[allow(clippy::too_many_arguments)] + #[cfg(feature = "experimental-async")] + pub(crate) fn call_async_sys_internal( + func: 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) + } + } + #[doc(hidden)] #[allow(missing_docs)] #[allow(unused_mut)] @@ -128,11 +194,20 @@ macro_rules! impl_native_traits { rets_list.as_mut() }; + let store_id = store.objects_mut().id(); + // Install the store into the store context + let store_install_guard = unsafe { + StoreContext::ensure_installed(store.as_store_mut().inner as *mut _) + }; + let mut r; loop { let storeref = store.as_store_ref(); let config = storeref.engine().tunables().vmconfig(); r = unsafe { + // Safety: This is the intended use-case for StoreContext::pause, as + // documented in the function's doc comments. + let pause_guard = StoreContext::pause(store_id); wasmer_vm::wasmer_call_trampoline( store.as_store_ref().signal_handler(), config, @@ -154,6 +229,9 @@ macro_rules! impl_native_traits { } break; } + + drop(store_install_guard); + r?; let num_rets = rets_list.len(); @@ -218,3 +296,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 ); + +fn convert_results( + store: &mut impl AsStoreMut, + ty: FunctionType, + results: &[Value], +) -> Result +where + Rets: WasmTypeList, +{ + if results.len() != ty.results().len() { + return Err(RuntimeError::new("result arity mismatch")); + } + let mut raw_array = Rets::empty_array(); + for ((slot, value_ty), value) in raw_array + .as_mut() + .iter_mut() + .zip(ty.results().iter()) + .zip(results.iter()) + { + debug_assert_eq!(value.ty(), *value_ty); + *slot = value.as_raw(store); + } + unsafe { Ok(Rets::from_array(store, raw_array)) } +} diff --git a/lib/api/src/backend/sys/entities/module.rs b/lib/api/src/backend/sys/entities/module.rs index 167092c6ece..cd12fd60c1b 100644 --- a/lib/api/src/backend/sys/entities/module.rs +++ b/lib/api/src/backend/sys/entities/module.rs @@ -10,7 +10,7 @@ use wasmer_types::{ }; use crate::{ - AsStoreMut, AsStoreRef, BackendModule, IntoBytes, + AsStoreMut, AsStoreRef, BackendModule, IntoBytes, StoreContext, backend::sys::entities::engine::NativeEngineExt, engine::AsEngineRef, error::InstantiationError, vm::VMInstance, }; @@ -167,6 +167,7 @@ impl Module { } let signal_handler = store.as_store_ref().signal_handler(); let mut store_mut = store.as_store_mut(); + let store_ptr = store_mut.inner as *mut _; let (engine, objects) = store_mut.engine_and_objects_mut(); let config = engine.tunables().vmconfig(); unsafe { @@ -179,6 +180,9 @@ impl Module { objects.as_sys_mut(), )?; + let store_id = objects.id(); + let store_install_guard = StoreContext::ensure_installed(store_ptr); + // After the instance handle is created, we need to initialize // the data, call the start function and so. However, if any // of this steps traps, we still need to keep the instance alive @@ -187,6 +191,8 @@ impl Module { self.artifact .finish_instantiation(config, signal_handler, &mut instance_handle)?; + drop(store_install_guard); + Ok(VMInstance::Sys(instance_handle)) } } diff --git a/lib/api/src/backend/sys/entities/store/mod.rs b/lib/api/src/backend/sys/entities/store/mod.rs index 5764b10b8aa..2763b2c08c3 100644 --- a/lib/api/src/backend/sys/entities/store/mod.rs +++ b/lib/api/src/backend/sys/entities/store/mod.rs @@ -70,18 +70,6 @@ impl NativeStoreExt for Store { } } -impl NativeStoreExt for crate::Store { - fn set_trap_handler(&mut self, handler: Option>>) { - self.inner.store.as_sys_mut().set_trap_handler(handler) - } - - /// The signal handler - #[inline] - fn signal_handler(&self) -> Option<*const TrapHandlerFn<'static>> { - self.inner.store.as_sys().signal_handler() - } -} - impl crate::BackendStore { /// Consume [`self`] into [`crate::backend::sys::store::Store`]. pub fn into_sys(self) -> crate::backend::sys::store::Store { @@ -111,25 +99,3 @@ impl crate::BackendStore { matches!(self, Self::Sys(_)) } } - -impl crate::Store { - /// Consume [`self`] into [`crate::backend::sys::store::Store`]. - pub(crate) fn into_sys(self) -> crate::backend::sys::store::Store { - self.inner.store.into_sys() - } - - /// Convert a reference to [`self`] into a reference [`crate::backend::sys::store::Store`]. - pub(crate) fn as_sys(&self) -> &crate::backend::sys::store::Store { - self.inner.store.as_sys() - } - - /// Convert a mutable reference to [`self`] into a mutable reference [`crate::backend::sys::store::Store`]. - pub(crate) fn as_sys_mut(&mut self) -> &mut crate::backend::sys::store::Store { - self.inner.store.as_sys_mut() - } - - /// Return true if [`self`] is a store from the `sys` runtime. - pub fn is_sys(&self) -> bool { - self.inner.store.is_sys() - } -} diff --git a/lib/api/src/backend/sys/entities/store/obj.rs b/lib/api/src/backend/sys/entities/store/obj.rs index ba90370dc1b..0bb27b89670 100644 --- a/lib/api/src/backend/sys/entities/store/obj.rs +++ b/lib/api/src/backend/sys/entities/store/obj.rs @@ -27,50 +27,3 @@ impl crate::StoreObjects { } } } - -//pub trait GetStoreObjects { -// /// Return a mutable reference to [`wasmer_vm::StoreObjects`] and a reference to the current -// /// engine. -// fn engine_and_objects_mut( -// &mut self, -// ) -> (&crate::Engine, &mut crate::backend::sys::store::StoreObjects); -// -// /// Return a mutable reference to [`wasmer_vm::StoreObjects`]. -// fn objects_mut(&mut self) -> &mut crate::backend::sys::store::StoreObjects; -//} -// -//impl GetStoreObjects for crate::StoreInner { -// fn objects_mut(&mut self) -> &mut crate::backend::sys::store::StoreObjects { -// self.objects.as_sys_mut() -// } -// -// fn engine_and_objects_mut( -// &mut self, -// ) -> (&crate::Engine, &mut crate::backend::sys::store::StoreObjects) { -// match (&mut self.objects, &self.store) { -// (crate::StoreObjects::Sys(o), crate::BackendStore::Sys(s)) => (&s.engine, o), -// _ => panic!("Not a `sys` store!"), -// } -// } -//} -// -//impl GetStoreObjects for T { -// fn objects_mut<'a>(&'a mut self) -> &'a mut crate::backend::sys::store::StoreObjects { -// match self.as_store_mut().inner.objects { -// crate::StoreObjects::Sys(ref mut s) => s, -// _ => panic!("Not a `sys` store!"), -// } -// } -// -// fn engine_and_objects_mut( -// &mut self, -// ) -> (&crate::Engine, &mut crate::backend::sys::store::StoreObjects) { -// let mut store = self.as_store_mut(); -// match (&mut store.inner.objects, &store.inner.store) { -// (crate::StoreObjects::Sys(ref mut o), crate::BackendStore::Sys(ref s)) => { -// (&s.engine, o) -// } -// _ => panic!("Not a `sys` store!"), -// } -// } -//} diff --git a/lib/api/src/backend/sys/mod.rs b/lib/api/src/backend/sys/mod.rs index 9417ed3227c..4e8ea874b76 100644 --- a/lib/api/src/backend/sys/mod.rs +++ b/lib/api/src/backend/sys/mod.rs @@ -1,5 +1,7 @@ //! Data types, functions and traits for the `sys` runtime. +#[cfg(feature = "experimental-async")] +pub(crate) mod async_runtime; pub(crate) mod entities; pub(crate) mod error; pub(crate) mod tunables; diff --git a/lib/api/src/backend/v8/entities/function/env.rs b/lib/api/src/backend/v8/entities/function/env.rs index 17e668e8159..cff3675a385 100644 --- a/lib/api/src/backend/v8/entities/function/env.rs +++ b/lib/api/src/backend/v8/entities/function/env.rs @@ -62,7 +62,7 @@ impl FunctionEnv { } /// Convert it into a `FunctionEnvMut` - pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut + pub fn into_mut<'a>(self, store: &'a mut impl AsStoreMut) -> FunctionEnvMut<'a, T> where T: Any + Send + 'static + Sized, { @@ -137,7 +137,7 @@ impl FunctionEnvMut<'_, T> { } /// Borrows a new mutable reference of both the attached Store and host state - pub fn data_and_store_mut(&mut self) -> (&mut T, StoreMut) { + pub fn data_and_store_mut<'a>(&'a mut self) -> (&'a mut T, StoreMut<'a>) { let data = self.func_env.as_mut(&mut self.store_mut) as *mut T; // telling the borrow check to close his eyes here // this is still relatively safe to do as func_env are diff --git a/lib/api/src/backend/v8/entities/function/mod.rs b/lib/api/src/backend/v8/entities/function/mod.rs index fd5fe497e6a..c11c2bd436c 100644 --- a/lib/api/src/backend/v8/entities/function/mod.rs +++ b/lib/api/src/backend/v8/entities/function/mod.rs @@ -6,9 +6,10 @@ use std::{ }; use crate::{ - AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, BackendTrap, - FromToNativeWasmType, FunctionEnv, FunctionEnvMut, IntoResult, NativeWasmType, - NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, WithoutEnv, + AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, BackendTrap, DynamicCallResult, + DynamicFunctionResult, FromToNativeWasmType, FunctionEnv, FunctionEnvMut, IntoResult, + NativeWasmType, NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, + WithoutEnv, v8::{ bindings::*, utils::convert::{IntoCApiType, IntoCApiValue, IntoWasmerType, IntoWasmerValue}, @@ -80,10 +81,7 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { check_isolate(store); @@ -338,18 +336,14 @@ impl Function { &self, _store: &mut impl AsStoreMut, _params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { // There is no optimal call_raw in JSC, so we just // simply rely the call // self.call(store, params) unimplemented!(); } - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { check_isolate(store); // unimplemented!(); let store_mut = store.as_store_mut(); @@ -438,10 +432,7 @@ impl Function { fn make_fn_callback(func: &F, args: usize) -> CCallback where - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { unsafe extern "C" fn fn_callback( env: *mut c_void, @@ -449,10 +440,7 @@ where rets: *mut wasm_val_vec_t, ) -> *mut wasm_trap_t where - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let r: *mut (FunctionCallbackEnv<'_, F>) = env as _; diff --git a/lib/api/src/backend/v8/entities/store/obj.rs b/lib/api/src/backend/v8/entities/store/obj.rs index d33c7bd5842..fbfb0dd9018 100644 --- a/lib/api/src/backend/v8/entities/store/obj.rs +++ b/lib/api/src/backend/v8/entities/store/obj.rs @@ -107,7 +107,7 @@ impl StoreObjects { } /// Return an immutable iterator over all globals - pub fn iter_globals(&self) -> core::slice::Iter { + pub fn iter_globals<'a>(&'a self) -> core::slice::Iter<'a, VMGlobal> { self.globals.iter() } diff --git a/lib/api/src/backend/v8/vm/mod.rs b/lib/api/src/backend/v8/vm/mod.rs index f0827da873e..7c798c2589c 100644 --- a/lib/api/src/backend/v8/vm/mod.rs +++ b/lib/api/src/backend/v8/vm/mod.rs @@ -133,7 +133,7 @@ impl VMFuncRef { } } -pub(crate) struct VMExceptionRef(*mut wasm_ref_t); +pub struct VMExceptionRef(*mut wasm_ref_t); impl VMExceptionRef { /// Converts the `VMExceptionRef` into a `RawValue`. pub fn into_raw(self) -> RawValue { diff --git a/lib/api/src/backend/wamr/entities/function/mod.rs b/lib/api/src/backend/wamr/entities/function/mod.rs index 903cbad7025..955cd7779ec 100644 --- a/lib/api/src/backend/wamr/entities/function/mod.rs +++ b/lib/api/src/backend/wamr/entities/function/mod.rs @@ -7,9 +7,10 @@ use std::{ }; use crate::{ - AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, BackendTrap, - FromToNativeWasmType, FunctionEnv, FunctionEnvMut, IntoResult, NativeWasmType, - NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, WithoutEnv, + AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, BackendTrap, DynamicCallResult, + DynamicFunctionResult, FromToNativeWasmType, FunctionEnv, FunctionEnvMut, IntoResult, + NativeWasmType, NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, + WithoutEnv, vm::{VMExtern, VMExternFunction}, wamr::{ bindings::*, @@ -81,10 +82,7 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let fn_ty: FunctionType = ty.into(); let params = fn_ty.params(); @@ -344,18 +342,14 @@ impl Function { &self, _store: &mut impl AsStoreMut, _params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { // There is no optimal call_raw in JSC, so we just // simply rely the call // self.call(store, params) unimplemented!(); } - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { // unimplemented!(); let store_mut = store.as_store_mut(); // let wasm_func_param_arity(self.handle) @@ -441,10 +435,7 @@ impl Function { fn make_fn_callback(func: &F, args: usize) -> CCallback where - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { unsafe extern "C" fn fn_callback( env: *mut c_void, @@ -452,10 +443,7 @@ where rets: *mut wasm_val_vec_t, ) -> *mut wasm_trap_t where - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let r: *mut (FunctionCallbackEnv<'_, F>) = env as _; diff --git a/lib/api/src/backend/wasmi/entities/function/env.rs b/lib/api/src/backend/wasmi/entities/function/env.rs index 5469aa6ab1c..04e691d96d0 100644 --- a/lib/api/src/backend/wasmi/entities/function/env.rs +++ b/lib/api/src/backend/wasmi/entities/function/env.rs @@ -62,7 +62,7 @@ impl FunctionEnv { } /// Convert it into a `FunctionEnvMut` - pub fn into_mut(self, store: &mut impl AsStoreMut) -> FunctionEnvMut + pub fn into_mut<'a>(self, store: &'a mut impl AsStoreMut) -> FunctionEnvMut<'a, T> where T: Any + Send + 'static + Sized, { @@ -137,7 +137,7 @@ impl FunctionEnvMut<'_, T> { } /// Borrows a new mutable reference of both the attached Store and host state - pub fn data_and_store_mut(&mut self) -> (&mut T, StoreMut) { + pub fn data_and_store_mut<'a>(&'a mut self) -> (&'a mut T, StoreMut<'a>) { let data = self.func_env.as_mut(&mut self.store_mut) as *mut T; // telling the borrow check to close his eyes here // this is still relatively safe to do as func_env are diff --git a/lib/api/src/backend/wasmi/entities/function/mod.rs b/lib/api/src/backend/wasmi/entities/function/mod.rs index 7526fd56b20..d6909fc5bb3 100644 --- a/lib/api/src/backend/wasmi/entities/function/mod.rs +++ b/lib/api/src/backend/wasmi/entities/function/mod.rs @@ -7,9 +7,10 @@ use std::{ }; use crate::{ - AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, BackendTrap, - FromToNativeWasmType, FunctionEnv, FunctionEnvMut, IntoResult, NativeWasmType, - NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, WithoutEnv, + AsStoreMut, AsStoreRef, BackendFunction, BackendFunctionEnvMut, BackendTrap, DynamicCallResult, + DynamicFunctionResult, FromToNativeWasmType, FunctionEnv, FunctionEnvMut, IntoResult, + NativeWasmType, NativeWasmTypeInto, RuntimeError, StoreMut, Value, WasmTypeList, WithEnv, + WithoutEnv, vm::{VMExtern, VMExternFunction}, wasmi::{ bindings::*, @@ -81,10 +82,7 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let fn_ty: FunctionType = ty.into(); let params = fn_ty.params(); @@ -340,18 +338,14 @@ impl Function { &self, _store: &mut impl AsStoreMut, _params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { // There is no optimal call_raw in JSC, so we just // simply rely the call // self.call(store, params) unimplemented!(); } - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { // unimplemented!(); let store_mut = store.as_store_mut(); // let wasm_func_param_arity(self.handle) @@ -442,10 +436,7 @@ impl Function { fn make_fn_callback(func: &F, args: usize) -> CCallback where - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { unsafe extern "C" fn fn_callback( env: *mut c_void, @@ -453,10 +444,7 @@ where rets: *mut wasm_val_vec_t, ) -> *mut wasm_trap_t where - F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut<'_, T>, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let r: *mut (FunctionCallbackEnv<'_, F>) = env as _; diff --git a/lib/api/src/backend/wasmi/entities/store/obj.rs b/lib/api/src/backend/wasmi/entities/store/obj.rs index 26a9d9cde51..3ee18e5bfea 100644 --- a/lib/api/src/backend/wasmi/entities/store/obj.rs +++ b/lib/api/src/backend/wasmi/entities/store/obj.rs @@ -107,7 +107,7 @@ impl StoreObjects { } /// Return an immutable iterator over all globals - pub fn iter_globals(&self) -> core::slice::Iter { + pub fn iter_globals<'a>(&'a self) -> core::slice::Iter<'a, VMGlobal> { self.globals.iter() } diff --git a/lib/api/src/backend/wasmi/vm/mod.rs b/lib/api/src/backend/wasmi/vm/mod.rs index c712232362d..055785fb8f3 100644 --- a/lib/api/src/backend/wasmi/vm/mod.rs +++ b/lib/api/src/backend/wasmi/vm/mod.rs @@ -128,7 +128,7 @@ impl VMFuncRef { } } -pub(crate) struct VMExceptionRef(*mut wasm_ref_t); +pub struct VMExceptionRef(*mut wasm_ref_t); impl VMExceptionRef { /// Converts the `VMExceptionRef` into a `RawValue`. pub fn into_raw(self) -> RawValue { diff --git a/lib/api/src/entities/function/async_host.rs b/lib/api/src/entities/function/async_host.rs new file mode 100644 index 00000000000..01702a91183 --- /dev/null +++ b/lib/api/src/entities/function/async_host.rs @@ -0,0 +1,178 @@ +use std::{future::Future, marker::PhantomData, pin::Pin}; + +use crate::{ + AsyncFunctionEnvMut, HostFunctionKind, RuntimeError, WasmTypeList, WithEnv, WithoutEnv, + utils::{FromToNativeWasmType, IntoResult}, +}; + +/// Wrapper conveying whether an async host function receives an environment. +pub enum AsyncFunctionEnv { + /// Used by host functions without an environment. + WithoutEnv(PhantomData<(T, Kind)>), + /// Used by host functions that capture an environment. + WithEnv(AsyncFunctionEnvMut), +} + +impl AsyncFunctionEnv { + /// Create an environment wrapper for functions without host state. + pub fn new() -> Self { + Self::WithoutEnv(PhantomData) + } +} + +impl AsyncFunctionEnv { + /// Create an environment wrapper carrying [`AsyncFunctionEnvMut`]. + pub fn with_env(env: AsyncFunctionEnvMut) -> Self { + Self::WithEnv(env) + } + + /// Extract the underlying [`AsyncFunctionEnvMut`]. + pub fn into_env(self) -> AsyncFunctionEnvMut { + match self { + Self::WithEnv(env) => env, + Self::WithoutEnv(_) => unreachable!("with-env async function called without env"), + } + } +} + +/// Async counterpart to [`HostFunction`](super::host::HostFunction). +pub trait AsyncHostFunction +where + Args: WasmTypeList + 'static, + Rets: WasmTypeList, + Kind: HostFunctionKind, +{ + /// Invoke the host function asynchronously. + fn call_async( + &self, + env: AsyncFunctionEnv, + args: Args, + ) -> Pin>>>; +} + +macro_rules! impl_async_host_function_without_env { + ( $( $x:ident ),* ) => { + impl<$( $x, )* Rets, RetsAsResult, F, Fut > AsyncHostFunction<(), ( $( $x ),* ), Rets, WithoutEnv> for F + where + F: Fn($( $x ),*) -> Fut + 'static, + Fut: Future + 'static, + RetsAsResult: IntoResult, + Rets: WasmTypeList, + ( $( $x ),* ): WasmTypeList + 'static, + $( $x: FromToNativeWasmType + 'static, )* + { + fn call_async( + &self, + _env: AsyncFunctionEnv<(), WithoutEnv>, + args: ( $( $x ),* ), + ) -> Pin>>> { + #[allow(non_snake_case)] + let ( $( $x ),* ) = args; + let fut = (self)( $( $x ),* ); + Box::pin(async move { + fut.await + .into_result() + .map_err(|err| RuntimeError::from_dyn(Box::new(err))) + }) + } + } + }; +} + +macro_rules! impl_async_host_function_with_env { + ( $( $x:ident ),* ) => { + impl<$( $x, )* Rets, RetsAsResult, T, F, Fut > AsyncHostFunction for F + where + T: 'static, + F: Fn(AsyncFunctionEnvMut, $( $x ),*) -> Fut + 'static, + Fut: Future + 'static, + RetsAsResult: IntoResult, + Rets: WasmTypeList, + ( $( $x ),* ): WasmTypeList + 'static, + $( $x: FromToNativeWasmType + 'static, )* + { + fn call_async( + &self, + env: AsyncFunctionEnv, + args: ( $( $x ),* ), + ) -> Pin>>> { + #[allow(non_snake_case)] + let ( $( $x ),* ) = args; + let fut = (self)(env.into_env(), $( $x ),* ); + Box::pin(async move { + fut.await + .into_result() + .map_err(|err| RuntimeError::from_dyn(Box::new(err))) + }) + } + } + }; +} + +impl_async_host_function_without_env!(); +impl_async_host_function_without_env!(A1); +impl_async_host_function_without_env!(A1, A2); +impl_async_host_function_without_env!(A1, A2, A3); +impl_async_host_function_without_env!(A1, A2, A3, A4); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13); +impl_async_host_function_without_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14); +impl_async_host_function_without_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15 +); +impl_async_host_function_without_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16 +); +impl_async_host_function_without_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17 +); +impl_async_host_function_without_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18 +); +impl_async_host_function_without_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19 +); +impl_async_host_function_without_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20 +); + +impl_async_host_function_with_env!(); +impl_async_host_function_with_env!(A1); +impl_async_host_function_with_env!(A1, A2); +impl_async_host_function_with_env!(A1, A2, A3); +impl_async_host_function_with_env!(A1, A2, A3, A4); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13); +impl_async_host_function_with_env!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14); +impl_async_host_function_with_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15 +); +impl_async_host_function_with_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16 +); +impl_async_host_function_with_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17 +); +impl_async_host_function_with_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18 +); +impl_async_host_function_with_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19 +); +impl_async_host_function_with_env!( + A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20 +); diff --git a/lib/api/src/entities/function/env/inner.rs b/lib/api/src/entities/function/env/inner.rs index 1e60b6dc03d..ee9d85410f7 100644 --- a/lib/api/src/entities/function/env/inner.rs +++ b/lib/api/src/entities/function/env/inner.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "experimental-async")] +use crate::AsStoreAsync; use crate::{ AsStoreMut, AsStoreRef, FunctionEnv, FunctionEnvMut, StoreMut, StoreRef, macros::backend::match_rt, @@ -85,11 +87,6 @@ impl BackendFunctionEnv { } } - //#[allow(dead_code)] // This function is only used in js - //pub(crate) fn from_handle(handle: StoreHandle) -> Self { - // todo!() - //} - /// Get the data as reference pub fn as_ref<'a>(&self, store: &'a impl AsStoreRef) -> &'a T where @@ -204,12 +201,26 @@ impl BackendFunctionEnvMut<'_, T> { f.data_and_store_mut() }) } + + /// Creates an [`AsStoreAsync`] from this [`BackendFunctionEnvMut`] if the current + /// 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::>(), + } + } } impl AsStoreRef for BackendFunctionEnvMut<'_, T> { fn as_store_ref(&self) -> StoreRef<'_> { - match_rt!(on &self => f { - f.as_store_ref() + match_rt!(on self => s { + s.as_store_ref() }) } } @@ -238,3 +249,197 @@ where }) } } + +/// A shared handle to a [`FunctionEnv`], suitable for use +/// in async imports. +#[derive(derive_more::From)] +#[non_exhaustive] +#[cfg(feature = "experimental-async")] +pub enum BackendAsyncFunctionEnvMut { + #[cfg(feature = "sys")] + /// The function environment for the `sys` runtime. + Sys(crate::backend::sys::function::env::AsyncFunctionEnvMut), + #[cfg(not(feature = "sys"))] + /// Placeholder for unsupported backends. + Unsupported(PhantomData), +} + +/// A read-only handle to the [`FunctionEnv`] in an [`BackendAsyncFunctionEnvMut`]. +#[non_exhaustive] +#[cfg(feature = "experimental-async")] +pub enum BackendAsyncFunctionEnvHandle { + #[cfg(feature = "sys")] + /// The function environment handle for the `sys` runtime. + Sys(crate::backend::sys::function::env::AsyncFunctionEnvHandle), + #[cfg(not(feature = "sys"))] + /// Placeholder for unsupported backends. + Unsupported(PhantomData), +} + +/// A mutable handle to the [`FunctionEnv`] in an [`BackendAsyncFunctionEnvMut`]. +#[cfg(feature = "experimental-async")] +pub enum BackendAsyncFunctionEnvHandleMut { + #[cfg(feature = "sys")] + /// The function environment handle for the `sys` runtime. + Sys(crate::backend::sys::function::env::AsyncFunctionEnvHandleMut), + #[cfg(not(feature = "sys"))] + /// Placeholder for unsupported backends. + Unsupported(PhantomData), +} + +#[cfg(feature = "experimental-async")] +impl BackendAsyncFunctionEnvMut { + /// Waits for a store lock and returns a read-only handle to the + /// function environment. + pub async fn read(&self) -> BackendAsyncFunctionEnvHandle { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => BackendAsyncFunctionEnvHandle::Sys(f.read().await), + _ => unsupported_async_backend(), + } + } + + /// Waits for a store lock and returns a mutable handle to the + /// function environment. + pub async fn write(&self) -> BackendAsyncFunctionEnvHandleMut { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => BackendAsyncFunctionEnvHandleMut::Sys(f.write().await), + _ => unsupported_async_backend(), + } + } + + /// Borrows a new immutable reference + pub fn as_ref(&self) -> BackendFunctionEnv { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => BackendFunctionEnv::Sys(f.as_ref()), + _ => unsupported_async_backend(), + } + } + + /// Borrows a new mutable reference + pub fn as_mut(&mut self) -> Self { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => Self::Sys(f.as_mut()), + _ => unsupported_async_backend(), + } + } + + /// Creates an [`AsStoreAsync`] from this [`BackendAsyncFunctionEnvMut`]. + pub fn as_store_async(&self) -> impl AsStoreAsync + 'static { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => f.as_store_async(), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend::(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl BackendAsyncFunctionEnvHandle { + /// Returns a reference to the host state in this function environment. + pub fn data(&self) -> &T { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => f.data(), + _ => unsupported_async_backend(), + } + } + + /// Returns both the host state and the attached StoreRef + pub fn data_and_store(&self) -> (&T, &impl AsStoreRef) { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => f.data_and_store(), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend::<(&T, &StoreRef)>(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for BackendAsyncFunctionEnvHandle { + fn as_store_ref(&self) -> StoreRef<'_> { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => AsStoreRef::as_store_ref(f), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl BackendAsyncFunctionEnvHandleMut { + /// Returns a mutable reference to the host state in this function environment. + pub fn data_mut(&mut self) -> &mut T { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => f.data_mut(), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend(), + } + } + + /// Returns both the host state and the attached StoreMut + pub fn data_and_store_mut(&mut self) -> (&mut T, &mut impl AsStoreMut) { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => f.data_and_store_mut(), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend::<(&mut T, &mut crate::StoreMut)>(), + } + } + + /// Borrows a new [`BackendFunctionEnvMut`] from this + /// [`BackendAsyncFunctionEnvHandleMut`]. + pub fn as_function_env_mut(&mut self) -> BackendFunctionEnvMut<'_, T> { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => BackendFunctionEnvMut::Sys(f.as_function_env_mut()), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for BackendAsyncFunctionEnvHandleMut { + fn as_store_ref(&self) -> StoreRef<'_> { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => AsStoreRef::as_store_ref(f), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend(), + } + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreMut for BackendAsyncFunctionEnvHandleMut { + fn as_store_mut(&mut self) -> StoreMut<'_> { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => AsStoreMut::as_store_mut(f), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend(), + } + } + + fn objects_mut(&mut self) -> &mut crate::StoreObjects { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => AsStoreMut::objects_mut(f), + #[cfg(not(feature = "sys"))] + _ => unsupported_async_backend(), + } + } +} + +#[cfg(feature = "experimental-async")] +fn unsupported_async_backend() -> T { + panic!("async functions are only supported with the `sys` backend"); +} diff --git a/lib/api/src/entities/function/env/mod.rs b/lib/api/src/entities/function/env/mod.rs index ed897289cf8..41b49f942b1 100644 --- a/lib/api/src/entities/function/env/mod.rs +++ b/lib/api/src/entities/function/env/mod.rs @@ -1,6 +1,8 @@ pub(crate) mod inner; pub(crate) use inner::*; +#[cfg(feature = "experimental-async")] +use crate::AsStoreAsync; use crate::{AsStoreMut, AsStoreRef, StoreMut, StoreRef, macros::backend::match_rt}; use std::{any::Any, fmt::Debug, marker::PhantomData}; @@ -83,6 +85,13 @@ impl FunctionEnvMut<'_, T> { pub fn data_and_store_mut(&mut self) -> (&mut T, StoreMut<'_>) { self.0.data_and_store_mut() } + + /// Creates an [`AsStoreAsync`] from this [`FunctionEnvMut`] if the current + /// context is async. + #[cfg(feature = "experimental-async")] + pub fn as_store_async(&self) -> Option { + self.0.as_store_async() + } } impl AsStoreRef for FunctionEnvMut<'_, T> { @@ -109,3 +118,103 @@ where self.0.fmt(f) } } + +/// A shared handle to a [`FunctionEnv`], suitable for use +/// in async imports. +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvMut(pub(crate) BackendAsyncFunctionEnvMut); + +/// A read-only handle to the [`FunctionEnv`] in an [`AsyncFunctionEnvMut`]. +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandle(pub(crate) BackendAsyncFunctionEnvHandle); + +/// A mutable handle to the [`FunctionEnv`] in an [`AsyncFunctionEnvMut`]. +#[cfg(feature = "experimental-async")] +pub struct AsyncFunctionEnvHandleMut(pub(crate) BackendAsyncFunctionEnvHandleMut); + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvMut { + /// Waits for a store lock and returns a read-only handle to the + /// function environment. + pub async fn read(&self) -> AsyncFunctionEnvHandle { + AsyncFunctionEnvHandle(self.0.read().await) + } + + /// Waits for a store lock and returns a mutable handle to the + /// function environment. + pub async fn write(&self) -> AsyncFunctionEnvHandleMut { + AsyncFunctionEnvHandleMut(self.0.write().await) + } + + /// Borrows a new immmutable reference + pub fn as_ref(&self) -> FunctionEnv { + FunctionEnv(self.0.as_ref()) + } + + /// Borrows a new mutable reference + pub fn as_mut(&mut self) -> Self { + Self(self.0.as_mut()) + } + + /// Creates an [`AsStoreAsync`] from this [`AsyncFunctionEnvMut`]. + pub fn as_store_async(&self) -> impl AsStoreAsync + 'static { + self.0.as_store_async() + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandle { + /// Returns a reference to the host state in this function environment. + pub fn data(&self) -> &T { + self.0.data() + } + + /// Returns both the host state and the attached StoreRef + pub fn data_and_store(&self) -> (&T, &impl AsStoreRef) { + self.0.data_and_store() + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for AsyncFunctionEnvHandle { + fn as_store_ref(&self) -> StoreRef<'_> { + AsStoreRef::as_store_ref(&self.0) + } +} + +#[cfg(feature = "experimental-async")] +impl AsyncFunctionEnvHandleMut { + /// Returns a mutable reference to the host state in this function environment. + pub fn data_mut(&mut self) -> &mut T { + self.0.data_mut() + } + + /// Returns both the host state and the attached StoreMut + pub fn data_and_store_mut(&mut self) -> (&mut T, &mut impl AsStoreMut) { + self.0.data_and_store_mut() + } + + /// Borrows a new [`FunctionEnvMut`] from this + /// [`AsyncFunctionEnvHandleMut`]. + pub fn as_function_env_mut(&mut self) -> FunctionEnvMut<'_, T> { + FunctionEnvMut(self.0.as_function_env_mut()) + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreRef for AsyncFunctionEnvHandleMut { + fn as_store_ref(&self) -> StoreRef<'_> { + AsStoreRef::as_store_ref(&self.0) + } +} + +#[cfg(feature = "experimental-async")] +impl AsStoreMut for AsyncFunctionEnvHandleMut { + fn as_store_mut(&mut self) -> StoreMut<'_> { + AsStoreMut::as_store_mut(&mut self.0) + } + + fn objects_mut(&mut self) -> &mut crate::StoreObjects { + AsStoreMut::objects_mut(&mut self.0) + } +} diff --git a/lib/api/src/entities/function/inner.rs b/lib/api/src/entities/function/inner.rs index 2a38d6ad1b2..b5de485b353 100644 --- a/lib/api/src/entities/function/inner.rs +++ b/lib/api/src/entities/function/inner.rs @@ -1,8 +1,13 @@ +use std::pin::Pin; + use wasmer_types::{FunctionType, RawValue}; +#[cfg(feature = "experimental-async")] +use crate::{AsStoreAsync, AsyncFunctionEnvMut, entities::function::async_host::AsyncHostFunction}; use crate::{ - AsStoreMut, AsStoreRef, ExportError, Exportable, Extern, FunctionEnv, FunctionEnvMut, - HostFunction, StoreMut, StoreRef, TypedFunction, Value, WasmTypeList, WithEnv, WithoutEnv, + AsStoreMut, AsStoreRef, DynamicCallResult, DynamicFunctionResult, ExportError, Exportable, + Extern, FunctionEnv, FunctionEnvMut, HostFunction, StoreMut, StoreRef, TypedFunction, Value, + WasmTypeList, WithEnv, WithoutEnv, error::RuntimeError, macros::backend::{gen_rt_ty, match_rt}, vm::{VMExtern, VMExternFunction, VMFuncRef}, @@ -39,12 +44,11 @@ impl BackendFunction { pub fn new(store: &mut impl AsStoreMut, ty: FT, func: F) -> Self where FT: Into, - F: Fn(&[Value]) -> Result, RuntimeError> + 'static + Send + Sync, + F: Fn(&[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { let env = FunctionEnv::new(&mut store.as_store_mut(), ()); - let wrapped_func = move |_env: FunctionEnvMut<()>, - args: &[Value]| - -> Result, RuntimeError> { func(args) }; + let wrapped_func = + move |_env: FunctionEnvMut<()>, args: &[Value]| -> DynamicFunctionResult { func(args) }; Self::new_with_env(store, &env, ty, wrapped_func) } @@ -94,10 +98,7 @@ impl BackendFunction { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { match &store.as_store_mut().inner.store { #[cfg(feature = "sys")] @@ -144,8 +145,8 @@ impl BackendFunction { pub fn new_typed(store: &mut impl AsStoreMut, func: F) -> Self where F: HostFunction<(), Args, Rets, WithoutEnv> + 'static + Send + Sync, - Args: WasmTypeList, - Rets: WasmTypeList, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, { match &store.as_store_mut().inner.store { #[cfg(feature = "sys")] @@ -203,8 +204,8 @@ impl BackendFunction { ) -> Self where F: HostFunction + 'static + Send + Sync, - Args: WasmTypeList, - Rets: WasmTypeList, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, { match &store.as_store_mut().inner.store { #[cfg(feature = "sys")] @@ -247,6 +248,148 @@ impl BackendFunction { } } + /// Creates a new async host `Function` (dynamic) with the provided + /// signature. + /// + /// If you know the signature of the host function at compile time, + /// consider using [`Self::new_typed_async`] for less runtime overhead. + /// + /// The provided closure returns a future that resolves to the function results. + /// When invoked synchronously + /// (via [`Function::call`](crate::Function::call)) the future will run to + /// completion immediately, provided it doesn't suspend. When invoked through + /// [`Function::call_async`](crate::Function::call_async), the future may suspend + /// and resume as needed. + #[inline] + #[cfg(feature = "experimental-async")] + pub fn new_async(store: &mut impl AsStoreMut, ty: FT, func: F) -> Self + where + FT: Into, + F: Fn(&[Value]) -> Fut + 'static, + Fut: Future + 'static, + { + match &store.as_store_mut().inner.store { + #[cfg(feature = "sys")] + crate::BackendStore::Sys(_) => Self::Sys( + crate::backend::sys::entities::function::Function::new_async(store, ty, func), + ), + #[cfg(feature = "wamr")] + crate::BackendStore::Wamr(_) => unsupported_async_backend("wamr"), + #[cfg(feature = "wasmi")] + crate::BackendStore::Wasmi(_) => unsupported_async_backend("wasmi"), + #[cfg(feature = "v8")] + crate::BackendStore::V8(_) => unsupported_async_backend("v8"), + #[cfg(feature = "js")] + crate::BackendStore::Js(_) => unsupported_async_backend("js"), + #[cfg(feature = "jsc")] + crate::BackendStore::Jsc(_) => unsupported_async_backend("jsc"), + } + } + + /// Creates a new async host `Function` (dynamic) with the provided + /// signature and environment. + /// + /// If you know the signature of the host function at compile time, + /// consider using [`Self::new_typed_with_env_async`] for less runtime overhead. + /// + /// Takes an [`AsyncFunctionEnvMut`] that is passed into func. If + /// that is not required, [`Self::new_async`] might be an option as well. + #[inline] + #[cfg(feature = "experimental-async")] + pub 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 + 'static, + { + match &store.as_store_mut().inner.store { + #[cfg(feature = "sys")] + crate::BackendStore::Sys(_) => Self::Sys( + crate::backend::sys::entities::function::Function::new_with_env_async( + store, env, ty, func, + ), + ), + #[cfg(feature = "wamr")] + crate::BackendStore::Wamr(_) => unsupported_async_backend("wamr"), + #[cfg(feature = "wasmi")] + crate::BackendStore::Wasmi(_) => unsupported_async_backend("wasmi"), + #[cfg(feature = "v8")] + crate::BackendStore::V8(_) => unsupported_async_backend("v8"), + #[cfg(feature = "js")] + crate::BackendStore::Js(_) => unsupported_async_backend("js"), + #[cfg(feature = "jsc")] + crate::BackendStore::Jsc(_) => unsupported_async_backend("jsc"), + } + } + + /// Creates a new async host `Function` from a native typed function. + /// + /// The future can return either the raw result tuple or any type that implements + /// [`IntoResult`](crate::IntoResult) for the result tuple (e.g. `Result`). + #[inline] + #[cfg(feature = "experimental-async")] + pub fn new_typed_async(store: &mut impl AsStoreMut, func: F) -> Self + where + F: AsyncHostFunction<(), Args, Rets, WithoutEnv> + 'static, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, + { + match &store.as_store_mut().inner.store { + #[cfg(feature = "sys")] + crate::BackendStore::Sys(_) => Self::Sys( + crate::backend::sys::entities::function::Function::new_typed_async(store, func), + ), + #[cfg(feature = "wamr")] + crate::BackendStore::Wamr(_) => unsupported_async_backend("wamr"), + #[cfg(feature = "wasmi")] + crate::BackendStore::Wasmi(_) => unsupported_async_backend("wasmi"), + #[cfg(feature = "v8")] + crate::BackendStore::V8(_) => unsupported_async_backend("v8"), + #[cfg(feature = "js")] + crate::BackendStore::Js(_) => unsupported_async_backend("js"), + #[cfg(feature = "jsc")] + crate::BackendStore::Jsc(_) => unsupported_async_backend("jsc"), + } + } + + /// Creates a new async host `Function` with an environment from a typed function. + #[inline] + #[cfg(feature = "experimental-async")] + pub fn new_typed_with_env_async( + store: &mut impl AsStoreMut, + env: &FunctionEnv, + func: F, + ) -> Self + where + F: AsyncHostFunction + 'static, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, + { + match &store.as_store_mut().inner.store { + #[cfg(feature = "sys")] + crate::BackendStore::Sys(_) => Self::Sys( + crate::backend::sys::entities::function::Function::new_typed_with_env_async( + store, env, func, + ), + ), + #[cfg(feature = "wamr")] + crate::BackendStore::Wamr(_) => unsupported_async_backend("wamr"), + #[cfg(feature = "wasmi")] + crate::BackendStore::Wasmi(_) => unsupported_async_backend("wasmi"), + #[cfg(feature = "v8")] + crate::BackendStore::V8(_) => unsupported_async_backend("v8"), + #[cfg(feature = "js")] + crate::BackendStore::Js(_) => unsupported_async_backend("js"), + #[cfg(feature = "jsc")] + crate::BackendStore::Jsc(_) => unsupported_async_backend("jsc"), + } + } + /// Returns the [`FunctionType`] of the `Function`. /// /// # Example @@ -348,11 +491,7 @@ impl BackendFunction { /// assert_eq!(sum.call(&mut store, &[Value::I32(1), Value::I32(2)]).unwrap().to_vec(), vec![Value::I32(3)]); /// ``` #[inline] - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { match_rt!(on self => f { f.call(store, params) }) @@ -365,12 +504,34 @@ impl BackendFunction { &self, store: &mut impl AsStoreMut, params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { match_rt!(on self => f { f.call_raw(store, params) }) } + #[cfg(feature = "experimental-async")] + pub fn call_async( + &self, + store: &impl AsStoreAsync, + params: Vec, + ) -> Pin + 'static>> { + match self { + #[cfg(feature = "sys")] + Self::Sys(f) => f.call_async(store, params), + #[cfg(feature = "wamr")] + Self::Wamr(_) => unsupported_async_future(), + #[cfg(feature = "wasmi")] + Self::Wasmi(_) => unsupported_async_future(), + #[cfg(feature = "v8")] + Self::V8(_) => unsupported_async_future(), + #[cfg(feature = "js")] + Self::Js(_) => unsupported_async_future(), + #[cfg(feature = "jsc")] + Self::Jsc(_) => unsupported_async_future(), + } + } + #[inline] pub(crate) fn vm_funcref(&self, store: &impl AsStoreRef) -> VMFuncRef { match self { @@ -605,6 +766,22 @@ impl BackendFunction { } } +#[cold] +fn unsupported_async_backend(backend: &str) -> ! { + panic!( + "async host functions are only supported with the `sys` backend (attempted on {backend})" + ) +} + +pub(super) fn unsupported_async_future<'a>() -> Pin + 'a>> +{ + Box::pin(async { + Err(RuntimeError::new( + "async calls are only supported with the `sys` backend", + )) + }) +} + impl<'a> Exportable<'a> for BackendFunction { fn get_self_from_extern(_extern: &'a Extern) -> Result<&'a Self, ExportError> { match _extern { diff --git a/lib/api/src/entities/function/mod.rs b/lib/api/src/entities/function/mod.rs index 76ef91e7dfa..10179263ce7 100644 --- a/lib/api/src/entities/function/mod.rs +++ b/lib/api/src/entities/function/mod.rs @@ -10,8 +10,17 @@ pub use host::*; pub(crate) mod env; pub use env::*; +#[cfg(feature = "experimental-async")] +pub(crate) mod async_host; +#[cfg(feature = "experimental-async")] +pub use async_host::{AsyncFunctionEnv, AsyncHostFunction}; + +use std::{future::Future, pin::Pin}; + use wasmer_types::{FunctionType, RawValue}; +#[cfg(feature = "experimental-async")] +use crate::AsStoreAsync; use crate::{ AsStoreMut, AsStoreRef, ExportError, Exportable, Extern, StoreMut, StoreRef, TypedFunction, Value, WasmTypeList, @@ -19,6 +28,12 @@ use crate::{ vm::{VMExtern, VMExternFunction, VMFuncRef}, }; +/// The return type from dynamic imported functions. +pub type DynamicFunctionResult = Result, RuntimeError>; + +/// The return type from dynamically calling Wasm functions. +pub type DynamicCallResult = Result, RuntimeError>; + /// A WebAssembly `function` instance. /// /// A function instance is the runtime representation of a function. @@ -48,7 +63,7 @@ impl Function { pub fn new(store: &mut impl AsStoreMut, ty: FT, func: F) -> Self where FT: Into, - F: Fn(&[Value]) -> Result, RuntimeError> + 'static + Send + Sync, + F: Fn(&[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { Self(BackendFunction::new(store, ty, func)) } @@ -98,10 +113,7 @@ impl Function { ) -> Self where FT: Into, - F: Fn(FunctionEnvMut, &[Value]) -> Result, RuntimeError> - + 'static - + Send - + Sync, + F: Fn(FunctionEnvMut, &[Value]) -> DynamicFunctionResult + 'static + Send + Sync, { Self(BackendFunction::new_with_env(store, env, ty, func)) } @@ -110,8 +122,8 @@ impl Function { pub fn new_typed(store: &mut impl AsStoreMut, func: F) -> Self where F: HostFunction<(), Args, Rets, WithoutEnv> + 'static + Send + Sync, - Args: WasmTypeList, - Rets: WasmTypeList, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, { Self(BackendFunction::new_typed(store, func)) } @@ -141,12 +153,84 @@ impl Function { ) -> Self where F: HostFunction + 'static + Send + Sync, - Args: WasmTypeList, - Rets: WasmTypeList, + Args: WasmTypeList + 'static, + Rets: WasmTypeList + 'static, { Self(BackendFunction::new_typed_with_env(store, env, func)) } + /// Creates a new async host `Function` (dynamic) with the provided + /// signature. + /// + /// If you know the signature of the host function at compile time, + /// consider using [`Self::new_typed_async`] for less runtime overhead. + /// + /// The provided closure returns a future that resolves to the function results. + /// When invoked synchronously (via [`Function::call`]) the future will run to + /// completion immediately, provided it doesn't suspend. When invoked through + /// [`Function::call_async`], the future may suspend and resume as needed. + #[cfg(feature = "experimental-async")] + pub fn new_async(store: &mut impl AsStoreMut, ty: FT, func: F) -> Self + where + FT: Into, + F: Fn(&[Value]) -> Fut + 'static, + Fut: Future + 'static, + { + Self(BackendFunction::new_async(store, ty, func)) + } + + /// Creates a new async host `Function` (dynamic) with the provided + /// signature and environment. + /// + /// If you know the signature of the host function at compile time, + /// consider using [`Self::new_typed_with_env_async`] for less runtime overhead. + /// + /// Takes an [`AsyncFunctionEnvMut`] that is passed into func. If + /// that is not required, [`Self::new_async`] might be an option as well. + #[cfg(feature = "experimental-async")] + pub 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 + 'static, + { + Self(BackendFunction::new_with_env_async(store, env, ty, func)) + } + + /// Creates a new async host `Function` from a native typed function. + /// + /// The future can return either the raw result tuple or any type that implements + /// [`IntoResult`](crate::IntoResult) for the result tuple (e.g. `Result`). + #[cfg(feature = "experimental-async")] + pub fn new_typed_async(store: &mut impl AsStoreMut, func: F) -> Self + where + Rets: WasmTypeList + 'static, + Args: WasmTypeList + 'static, + F: AsyncHostFunction<(), Args, Rets, WithoutEnv> + 'static, + { + Self(BackendFunction::new_typed_async(store, func)) + } + + /// Creates a new async host `Function` with an environment from a typed function. + #[cfg(feature = "experimental-async")] + pub fn new_typed_with_env_async( + store: &mut impl AsStoreMut, + env: &FunctionEnv, + func: F, + ) -> Self + where + Rets: WasmTypeList + 'static, + Args: WasmTypeList + 'static, + F: AsyncHostFunction + 'static, + { + Self(BackendFunction::new_typed_with_env_async(store, env, func)) + } + /// Returns the [`FunctionType`] of the `Function`. /// /// # Example @@ -242,21 +326,33 @@ impl Function { /// /// assert_eq!(sum.call(&mut store, &[Value::I32(1), Value::I32(2)]).unwrap().to_vec(), vec![Value::I32(3)]); /// ``` - pub fn call( - &self, - store: &mut impl AsStoreMut, - params: &[Value], - ) -> Result, RuntimeError> { + pub fn call(&self, store: &mut impl AsStoreMut, params: &[Value]) -> DynamicCallResult { self.0.call(store, params) } + /// Calls the function asynchronously. + /// + /// The returned future drives execution of the WebAssembly function on a + /// coroutine stack. Host functions created with [`Function::new_async`] may + /// suspend execution by awaiting futures, and their completion will resume + /// the Wasm instance according to the JSPI proposal. + #[must_use = "This function spawns a future that must be awaited to produce results"] + #[cfg(feature = "experimental-async")] + pub fn call_async( + &self, + store: &impl AsStoreAsync, + params: Vec, + ) -> impl Future + 'static { + self.0.call_async(store, params) + } + #[doc(hidden)] #[allow(missing_docs)] pub fn call_raw( &self, store: &mut impl AsStoreMut, params: Vec, - ) -> Result, RuntimeError> { + ) -> DynamicCallResult { self.0.call_raw(store, params) } diff --git a/lib/api/src/entities/module/inner.rs b/lib/api/src/entities/module/inner.rs index 6c9fd76242f..56b7c542788 100644 --- a/lib/api/src/entities/module/inner.rs +++ b/lib/api/src/entities/module/inner.rs @@ -438,8 +438,8 @@ impl BackendModule { /// /// ```ignore /// # use wasmer::*; - /// # let mut store = Store::default(); /// # fn main() -> anyhow::Result<()> { + /// # let mut store = Store::default(); /// let module = Module::deserialize_from_file(&store, path)?; /// # Ok(()) /// # } @@ -525,8 +525,8 @@ impl BackendModule { /// /// ```ignore /// # use wasmer::*; - /// # let mut store = Store::default(); /// # fn main() -> anyhow::Result<()> { + /// # let mut store = Store::default(); /// let module = Module::deserialize_from_file_unchecked(&store, path)?; /// # Ok(()) /// # } diff --git a/lib/api/src/entities/module/mod.rs b/lib/api/src/entities/module/mod.rs index 15c2cbe3490..daa3de03121 100644 --- a/lib/api/src/entities/module/mod.rs +++ b/lib/api/src/entities/module/mod.rs @@ -276,8 +276,8 @@ impl Module { /// /// ```ignore /// # use wasmer::*; - /// # let mut store = Store::default(); /// # fn main() -> anyhow::Result<()> { + /// # let mut store = Store::default(); /// let module = Module::deserialize_from_file(&store, path)?; /// # Ok(()) /// # } @@ -306,8 +306,8 @@ impl Module { /// /// ```ignore /// # use wasmer::*; - /// # let mut store = Store::default(); /// # fn main() -> anyhow::Result<()> { + /// # let mut store = Store::default(); /// let module = Module::deserialize_from_file_unchecked(&store, path)?; /// # Ok(()) /// # } diff --git a/lib/api/src/entities/store/async_.rs b/lib/api/src/entities/store/async_.rs new file mode 100644 index 00000000000..fa557c174ff --- /dev/null +++ b/lib/api/src/entities/store/async_.rs @@ -0,0 +1,160 @@ +use std::{fmt::Debug, marker::PhantomData}; + +use crate::{ + AsStoreMut, AsStoreRef, LocalRwLock, LocalRwLockReadGuard, LocalRwLockWriteGuard, Store, + StoreContext, StoreInner, StoreMut, StorePtrWrapper, StoreRef, +}; + +use wasmer_types::StoreId; + +/// A store that can be used to invoke +/// [`Function::call_async`](crate::Function::call_async). +pub struct StoreAsync { + pub(crate) id: StoreId, + // We use a box inside the RW lock because the StoreInner shouldn't be moved + pub(crate) inner: LocalRwLock>, +} + +impl StoreAsync { + pub(crate) fn from_context(id: StoreId) -> Option { + // Safety: we don't keep the guard around, it's just used to + // build a safe lock handle. + match unsafe { StoreContext::try_get_current_async(id) } { + crate::GetStoreAsyncGuardResult::Ok(guard) => Some(Self { + id, + inner: crate::LocalRwLockWriteGuard::lock_handle(unsafe { + guard.guard.as_ref().unwrap() + }), + }), + _ => None, + } + } + + /// Transform this [`StoreAsync`] back into a [`Store`] + /// if this is the only clone of it and is unlocked. + pub fn into_store(self) -> Result { + match self.inner.consume() { + Ok(unwrapped) => Ok(Store { inner: unwrapped }), + Err(lock) => Err(Self { + id: self.id, + inner: lock, + }), + } + } + + /// Acquire a read lock on the store. Panics if the store is + /// locked for writing. + pub fn read(&self) -> StoreAsyncReadLock { + if !StoreContext::is_empty() { + panic!("This method cannot be called from inside imported functions"); + } + + let store_ref = self + .inner + .try_read() + .expect("StoreAsync is locked for write"); + StoreAsyncReadLock { inner: store_ref } + } + + /// Acquire a write lock on the store. Panics if the store is + /// locked. + pub fn write(self) -> StoreAsyncWriteLock { + if !StoreContext::is_empty() { + panic!("This method cannot be called from inside imported functions"); + } + + let store_guard = self.inner.try_write().expect("StoreAsync is locked"); + StoreAsyncWriteLock { inner: store_guard } + } +} + +impl Debug for StoreAsync { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StoreAsync").field("id", &self.id).finish() + } +} + +/// A trait for types that can be used with +/// [`Function::call_async`](crate::Function::call_async). +pub trait AsStoreAsync { + /// Returns a reference to the inner store. + fn store_ref(&self) -> &StoreAsync; + + /// Returns a copy of the store. + fn store(&self) -> StoreAsync { + let store = self.store_ref(); + StoreAsync { + id: store.id, + inner: store.inner.clone(), + } + } + + /// Returns the store id. + fn store_id(&self) -> StoreId { + self.store().id + } + + /// Acquires a read lock on the store. + fn read_lock(&self) -> impl Future { + StoreAsyncReadLock::acquire(self.store_ref()) + } + + /// Acquires a write lock on the store. + fn write_lock(&self) -> impl Future { + StoreAsyncWriteLock::acquire(self.store_ref()) + } +} + +impl AsStoreAsync for StoreAsync { + fn store_ref(&self) -> &StoreAsync { + self + } +} + +/// A read lock on an async store. +pub struct StoreAsyncReadLock { + pub(crate) inner: LocalRwLockReadGuard>, +} + +impl StoreAsyncReadLock { + pub(crate) async fn acquire(store: &StoreAsync) -> Self { + let store_ref = store.inner.read().await; + Self { inner: store_ref } + } +} + +impl AsStoreRef for StoreAsyncReadLock { + fn as_store_ref(&self) -> StoreRef<'_> { + StoreRef { inner: &self.inner } + } +} + +/// A write lock on an async store. +pub struct StoreAsyncWriteLock { + pub(crate) inner: LocalRwLockWriteGuard>, +} + +impl StoreAsyncWriteLock { + pub(crate) async fn acquire(store: &StoreAsync) -> Self { + let store_guard = store.inner.write().await; + Self { inner: store_guard } + } +} + +impl AsStoreRef for StoreAsyncWriteLock { + fn as_store_ref(&self) -> StoreRef<'_> { + StoreRef { inner: &self.inner } + } +} + +impl AsStoreMut for StoreAsyncWriteLock { + fn as_store_mut(&mut self) -> StoreMut<'_> { + StoreMut { + inner: &mut self.inner, + } + } + + fn objects_mut(&mut self) -> &mut super::StoreObjects { + &mut self.inner.objects + } +} diff --git a/lib/api/src/entities/store/context.rs b/lib/api/src/entities/store/context.rs new file mode 100644 index 00000000000..3c4285bf3bd --- /dev/null +++ b/lib/api/src/entities/store/context.rs @@ -0,0 +1,459 @@ +//! Thread-local storage for storing the current store context, +//! i.e. the currently active [`Store`](crate::Store)(s). When +//! a function is called, a pointer to the [`StoreInner`] in placed +//! inside the store context so it can be retrieved when needed. +//! This lets code that needs access to the store get it with +//! just the store ID. +//! +//! The currently active store context can be a sync or async +//! context. +//! +//! For sync contexts, we just store a raw pointer +//! to the `StoreInner`, which is owned by the embedder's stack. +//! +//! For async contexts, we store a write guard taken from the +//! [`StoreAsync`](crate::StoreAsync); This achieves two goals: +//! * Makes the [`StoreAsync`](crate::StoreAsync) available +//! to whoever needs it, including when code needs to spawn +//! new coroutines +//! * Makes sure a write lock is held on the store as long as +//! the context is active, preventing other tasks from +//! accessing the store concurrently. +//! +//! We maintain a stack because it is technically possible to +//! have nested `Function::call` invocations that use different +//! stores, such as: +//! call(store1, func1) -> wasm code -> imported func -> +//! call(store2, func2) +//! +//! Note that this stack is maintained by both function +//! calls and the async_runtime to reflect the exact WASM +//! functions running on a given thread at any moment in +//! time. If a function suspends, its store context is +//! cleared and later reinstalled when it resumes. This lets +//! us use thread-local storage for the context without +//! requiring that async tasks are tied to specific threads. +//! +//! When something needs the "currently active" store context, +//! they will only look at the top entry in the stack. It is +//! always an error for code to try to access a store that's +//! "paused", i.e. not the top entry. This should be impossible +//! due to how the function call code is structured, but we +//! guard against it anyway. + +use std::{ + borrow::BorrowMut, + cell::{RefCell, UnsafeCell}, + mem::MaybeUninit, +}; + +#[cfg(feature = "experimental-async")] +use crate::LocalRwLockWriteGuard; + +use super::{AsStoreMut, AsStoreRef, StoreInner, StoreMut, StoreRef}; + +use wasmer_types::StoreId; + +enum StoreContextEntry { + Sync(*mut StoreInner), + + #[cfg(feature = "experimental-async")] + Async(LocalRwLockWriteGuard>), +} + +impl StoreContextEntry { + fn as_ptr(&self) -> *mut StoreInner { + match self { + Self::Sync(ptr) => *ptr, + #[cfg(feature = "experimental-async")] + Self::Async(guard) => &***guard as *const _ as *mut _, + } + } +} + +pub(crate) struct StoreContext { + id: StoreId, + + // StoreContexts can be used recursively when Function::call + // is used in an imported function. In the scenario, we're + // essentially passing a mutable borrow of the store into + // Function::call. However, entering the WASM code loses the + // reference, and it needs to be re-acquired from the + // StoreContext. This is why we use an UnsafeCell to allow + // multiple mutable references to the StoreMut; we do however + // keep track of how many borrows there are so we don't drop + // it prematurely. + borrow_count: u32, + entry: UnsafeCell, +} + +pub(crate) struct StorePtrWrapper { + store_ptr: *mut StoreInner, +} + +#[cfg(feature = "experimental-async")] +pub(crate) struct StoreAsyncGuardWrapper { + pub(crate) guard: *mut LocalRwLockWriteGuard>, +} + +pub(crate) struct StorePtrPauseGuard { + store_id: StoreId, + ptr: *mut StoreInner, + ref_count_decremented: bool, +} + +#[cfg(feature = "experimental-async")] +pub(crate) enum GetStoreAsyncGuardResult { + Ok(StoreAsyncGuardWrapper), + NotAsync(StorePtrWrapper), + NotInstalled, +} + +pub(crate) struct ForcedStoreInstallGuard { + store_id: StoreId, +} + +pub(crate) enum StoreInstallGuard { + Installed(StoreId), + NotInstalled, +} + +thread_local! { + static STORE_CONTEXT_STACK: RefCell> = const { RefCell::new(Vec::new()) }; +} + +impl StoreContext { + fn is_active(id: StoreId) -> bool { + STORE_CONTEXT_STACK.with(|cell| { + let stack = cell.borrow(); + stack.last().is_some_and(|ctx| ctx.id == id) + }) + } + + fn is_suspended(id: StoreId) -> bool { + !Self::is_active(id) + && STORE_CONTEXT_STACK.with(|cell| { + let stack = cell.borrow(); + stack.iter().rev().skip(1).any(|ctx| ctx.id == id) + }) + } + + fn install(id: StoreId, entry: StoreContextEntry) { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + stack.push(Self { + id, + borrow_count: 0, + entry: UnsafeCell::new(entry), + }); + }) + } + + /// Returns true if there are no active store context entries. + pub(crate) fn is_empty() -> bool { + STORE_CONTEXT_STACK.with(|cell| { + let stack = cell.borrow(); + stack.is_empty() + }) + } + + /// The write guard ensures this is the only reference to the store, + /// so installation can never fail. + #[cfg(feature = "experimental-async")] + pub(crate) fn install_async( + guard: LocalRwLockWriteGuard>, + ) -> ForcedStoreInstallGuard { + let store_id = guard.objects.id(); + Self::install(store_id, StoreContextEntry::Async(guard)); + ForcedStoreInstallGuard { store_id } + } + + /// Install the store context as sync if it is not already installed. + /// + /// # Safety + /// The pointer must be dereferenceable and remain valid until the + /// store context is uninstalled. + pub(crate) unsafe fn ensure_installed(store_ptr: *mut StoreInner) -> StoreInstallGuard { + let store_id = unsafe { store_ptr.as_ref().unwrap().objects.id() }; + if Self::is_active(store_id) { + let current_ptr = STORE_CONTEXT_STACK.with(|cell| { + let stack = cell.borrow(); + unsafe { stack.last().unwrap().entry.get().as_ref().unwrap().as_ptr() } + }); + assert_eq!(store_ptr, current_ptr, "Store context pointer mismatch"); + StoreInstallGuard::NotInstalled + } else { + Self::install(store_id, StoreContextEntry::Sync(store_ptr)); + StoreInstallGuard::Installed(store_id) + } + } + + /// "Pause" one borrow of the store context. + /// + /// # Safety + /// Code must ensure it does not use the StorePtrWrapper or + /// StoreAsyncGuardWrapper that it owns, or any StoreRef/StoreMut + /// derived from them, while the store context is paused. + /// + /// The safe, correct use-case for this method is to + /// pause the store context while executing WASM code, which + /// cannot use the store context directly. This allows an async + /// context to uninstall the store context when suspending if it's + /// called from a sync imported function. The imported function + /// will have borrowed the store context in its trampoline, which + /// will prevent the async context from uninstalling the store. + /// However, since the imported function passes a mutable borrow + /// of its store into `Function::call`, it will expect the store + /// to change before the call returns. + pub(crate) unsafe fn pause(id: StoreId) -> StorePtrPauseGuard { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + assert_eq!(top.id, id, "Mismatched store context access"); + let ref_count_decremented = if top.borrow_count > 0 { + top.borrow_count -= 1; + true + } else { + false + }; + StorePtrPauseGuard { + store_id: id, + ptr: unsafe { top.entry.get().as_ref().unwrap().as_ptr() }, + ref_count_decremented, + } + }) + } + + /// Safety: This method lets you borrow multiple mutable references + /// to the currently active store context. The caller must ensure that: + /// * there is only one mutable reference alive, or + /// * all but one mutable reference are inaccessible and passed + /// into a function that lost the reference (e.g. into WASM code) + /// + /// The intended, valid use-case for this method is from within + /// imported function trampolines. + pub(crate) unsafe fn get_current(id: StoreId) -> StorePtrWrapper { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + assert_eq!(top.id, id, "Mismatched store context access"); + top.borrow_count += 1; + StorePtrWrapper { + store_ptr: unsafe { top.entry.get().as_mut().unwrap().as_ptr() }, + } + }) + } + + /// Safety: In addition to the safety requirements of [`Self::get_current`], + /// the pointer returned from this function will become invalid if + /// the store context is changed in any way (via installing or uninstalling + /// a store context). The caller must ensure that the store context + /// remains unchanged as long as the pointer is being accessed. + pub(crate) unsafe fn get_current_transient(id: StoreId) -> *mut StoreInner { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + assert_eq!(top.id, id, "Mismatched store context access"); + unsafe { top.entry.get().as_mut().unwrap().as_ptr() } + }) + } + + /// Safety: See [`Self::get_current`]. + pub(crate) unsafe fn try_get_current(id: StoreId) -> Option { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack.last_mut()?; + if top.id != id { + return None; + } + top.borrow_count += 1; + Some(StorePtrWrapper { + store_ptr: unsafe { top.entry.get().as_mut().unwrap().as_ptr() }, + }) + }) + } + + /// Safety: See [`Self::get_current`]. + #[cfg(feature = "experimental-async")] + pub(crate) unsafe fn try_get_current_async(id: StoreId) -> GetStoreAsyncGuardResult { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let Some(top) = stack.last_mut() else { + return GetStoreAsyncGuardResult::NotInstalled; + }; + if top.id != id { + return GetStoreAsyncGuardResult::NotInstalled; + } + top.borrow_count += 1; + match unsafe { top.entry.get().as_mut().unwrap() } { + StoreContextEntry::Async(guard) => { + GetStoreAsyncGuardResult::Ok(StoreAsyncGuardWrapper { + guard: guard as *mut _, + }) + } + StoreContextEntry::Sync(ptr) => { + GetStoreAsyncGuardResult::NotAsync(StorePtrWrapper { store_ptr: *ptr }) + } + } + }) + } +} + +impl StorePtrWrapper { + pub(crate) fn as_ref(&self) -> StoreRef<'_> { + // Safety: the store_mut is always initialized unless the StoreMutWrapper + // is dropped, at which point it's impossible to call this function + unsafe { self.store_ptr.as_ref().unwrap().as_store_ref() } + } + + pub(crate) fn as_mut(&mut self) -> StoreMut<'_> { + // Safety: the store_mut is always initialized unless the StoreMutWrapper + // is dropped, at which point it's impossible to call this function + unsafe { self.store_ptr.as_mut().unwrap().as_store_mut() } + } +} + +impl Clone for StorePtrWrapper { + fn clone(&self) -> Self { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + match unsafe { top.entry.get().as_ref().unwrap() } { + StoreContextEntry::Sync(ptr) if *ptr == self.store_ptr => (), + _ => panic!("Mismatched store context access"), + } + top.borrow_count += 1; + Self { + store_ptr: self.store_ptr, + } + }) + } +} + +impl Drop for StorePtrWrapper { + fn drop(&mut self) { + if std::thread::panicking() { + return; + } + let id = self.as_mut().objects_mut().id(); + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + assert_eq!(top.id, id, "Mismatched store context reinstall"); + top.borrow_count -= 1; + }) + } +} + +#[cfg(feature = "experimental-async")] +impl Drop for StoreAsyncGuardWrapper { + fn drop(&mut self) { + if std::thread::panicking() { + return; + } + let id = unsafe { self.guard.as_ref().unwrap().objects.id() }; + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + assert_eq!(top.id, id, "Mismatched store context reinstall"); + top.borrow_count -= 1; + }) + } +} + +impl Drop for StoreInstallGuard { + fn drop(&mut self) { + if let Self::Installed(store_id) = self { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + match (stack.pop(), std::thread::panicking()) { + (Some(top), false) => { + assert_eq!(top.id, *store_id, "Mismatched store context uninstall"); + assert_eq!( + top.borrow_count, 0, + "Cannot uninstall store context while it is still borrowed" + ); + } + (Some(top), true) => { + // If we're panicking and there's a store ID mismatch, just + // put the store back in the hope that its own install guard + // take care of uninstalling it later. + if top.id != *store_id { + stack.push(top); + } + } + (None, false) => panic!("Store context stack underflow"), + (None, true) => { + // Nothing to do if we're panicking; panics can put the context + // in an invalid state, and we don't to cause another panic here. + } + } + }) + } + } +} + +impl Drop for ForcedStoreInstallGuard { + fn drop(&mut self) { + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + match (stack.pop(), std::thread::panicking()) { + (Some(top), false) => { + assert_eq!(top.id, self.store_id, "Mismatched store context uninstall"); + assert_eq!( + top.borrow_count, 0, + "Cannot uninstall store context while it is still borrowed" + ); + } + (Some(top), true) => { + // If we're panicking and there's a store ID mismatch, just + // put the store back in the hope that its own install guard + // take care of uninstalling it later. + if top.id != self.store_id { + stack.push(top); + } + } + (None, false) => panic!("Store context stack underflow"), + (None, true) => { + // Nothing to do if we're panicking; panics can put the context + // in an invalid state, and we don't to cause another panic here. + } + } + }) + } +} + +impl Drop for StorePtrPauseGuard { + fn drop(&mut self) { + if std::thread::panicking() { + return; + } + STORE_CONTEXT_STACK.with(|cell| { + let mut stack = cell.borrow_mut(); + let top = stack + .last_mut() + .expect("No store context installed on this thread"); + assert_eq!(top.id, self.store_id, "Mismatched store context access"); + assert_eq!( + unsafe { top.entry.get().as_ref().unwrap() }.as_ptr(), + self.ptr, + "Mismatched store context access" + ); + if self.ref_count_decremented { + top.borrow_count += 1; + } + }) + } +} diff --git a/lib/api/src/entities/store/inner.rs b/lib/api/src/entities/store/inner.rs index 875b8ec203c..c977b01acbf 100644 --- a/lib/api/src/entities/store/inner.rs +++ b/lib/api/src/entities/store/inner.rs @@ -1,5 +1,5 @@ use crate::{ - AsStoreMut, + AsStoreMut, AsStoreRef, StoreRef, entities::{ engine::{AsEngineRef, Engine}, store::{StoreMut, StoreObjects}, @@ -29,6 +29,21 @@ impl std::fmt::Debug for StoreInner { } } +impl AsStoreRef for StoreInner { + fn as_store_ref(&self) -> StoreRef<'_> { + StoreRef { inner: self } + } +} +impl AsStoreMut for StoreInner { + fn as_store_mut(&mut self) -> StoreMut<'_> { + StoreMut { inner: self } + } + + fn objects_mut(&mut self) -> &mut StoreObjects { + &mut self.objects + } +} + /// Call handler for a store. // TODO: better documentation! pub type OnCalledHandler = Box< diff --git a/lib/api/src/entities/store/local_rwlock.rs b/lib/api/src/entities/store/local_rwlock.rs new file mode 100644 index 00000000000..8eb6c53a87e --- /dev/null +++ b/lib/api/src/entities/store/local_rwlock.rs @@ -0,0 +1,648 @@ +//! A single-threaded async-aware RwLock implementation. +//! +//! This module provides [`LocalRwLock`], which is similar to `async_lock::RwLock` +//! but optimized for single-threaded async runtimes. It avoids atomic operations +//! and is `!Send + !Sync`, making it more efficient when thread safety is not needed. +//! +//! Like `async_lock::RwLock`, it provides `read_rc()` and `write_rc()` methods +//! that allow callers to asynchronously wait for the lock to become available, +//! and return guards with `'static` lifetimes by holding an `Rc` to the lock. + +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::future::Future; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll, Waker}; + +/// The main lock type. +pub struct LocalRwLock { + inner: Rc>, +} + +struct LocalRwLockInner { + value: UnsafeCell, + state: Cell, + read_waiters: RefCell>>, + write_waiters: RefCell>>, +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum LockState { + Unlocked, + Reading(usize), // count of readers + Writing, +} + +impl LocalRwLock { + /// Creates a new `LocalRwLock` with the given value. + pub fn new(value: T) -> Self { + Self { + inner: Rc::new(LocalRwLockInner { + value: UnsafeCell::new(value), + state: Cell::new(LockState::Unlocked), + read_waiters: RefCell::new(Vec::new()), + write_waiters: RefCell::new(Vec::new()), + }), + } + } + + /// Acquires a read lock, waiting asynchronously if necessary. + /// + /// The returned guard holds an `Rc` clone, allowing it to have a `'static` lifetime. + pub fn read(&self) -> ReadFuture { + ReadFuture { + inner: self.inner.clone(), + waiter_index: Cell::new(None), + } + } + + /// Acquires a write lock, waiting asynchronously if necessary. + /// + /// The returned guard holds an `Rc` clone, allowing it to have a `'static` lifetime. + pub fn write(&self) -> WriteFuture { + WriteFuture { + inner: self.inner.clone(), + waiter_index: Cell::new(None), + } + } + + /// Attempts to acquire a read lock with a `'static` lifetime without waiting. + pub fn try_read(&self) -> Option> { + if self.inner.try_read() { + Some(LocalRwLockReadGuard { + inner: self.inner.clone(), + }) + } else { + None + } + } + + /// Attempts to acquire a write lock with a `'static` lifetime without waiting. + pub fn try_write(&self) -> Option> { + if self.inner.try_write() { + Some(LocalRwLockWriteGuard { + inner: self.inner.clone(), + }) + } else { + None + } + } + + /// Attempts to consume the lock if there are no active or waiting + /// readers or writers, and returns the inner value if successful. + pub fn consume(self) -> Result { + if self.inner.state.get() == LockState::Unlocked + && self.inner.read_waiters.borrow().is_empty() + && self.inner.write_waiters.borrow().is_empty() + { + match Rc::try_unwrap(self.inner) { + Ok(inner) => Ok(inner.value.into_inner()), + Err(rc) => Err(Self { inner: rc }), + } + } else { + Err(self) + } + } +} + +impl Clone for LocalRwLock { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl LocalRwLockInner { + fn try_read(&self) -> bool { + match self.state.get() { + LockState::Unlocked => { + self.state.set(LockState::Reading(1)); + true + } + LockState::Reading(n) => { + self.state.set(LockState::Reading(n + 1)); + true + } + LockState::Writing => false, + } + } + + fn try_write(&self) -> bool { + match self.state.get() { + LockState::Unlocked => { + self.state.set(LockState::Writing); + true + } + _ => false, + } + } + + fn release_read(&self) { + match self.state.get() { + LockState::Reading(1) => { + self.state.set(LockState::Unlocked); + self.wake_waiters(); + } + LockState::Reading(n) if n > 1 => { + self.state.set(LockState::Reading(n - 1)); + } + _ => panic!("LocalRwLock: release_read called but not in Reading state"), + } + } + + fn release_write(&self) { + match self.state.get() { + LockState::Writing => { + self.state.set(LockState::Unlocked); + self.wake_waiters(); + } + _ => panic!("LocalRwLock: release_write called but not in Writing state"), + } + } + + fn wake_waiters(&self) { + // Wake writers first (priority) + let mut write_waiters = self.write_waiters.borrow_mut(); + let has_writers = write_waiters.iter().any(|w| w.is_some()); + + if has_writers { + // If there are waiting writers, only wake them (they need exclusive access) + for waker in write_waiters.drain(..).flatten() { + waker.wake(); + } + } else { + // No writers waiting, wake all readers (they can share the lock) + drop(write_waiters); // Release borrow before borrowing read_waiters + let mut read_waiters = self.read_waiters.borrow_mut(); + for waker in read_waiters.drain(..).flatten() { + waker.wake(); + } + } + } + + /// Shared polling logic for all futures. + /// Returns Poll::Ready(()) if the lock was acquired, Poll::Pending otherwise. + fn poll_lock( + &self, + waiter_index: &Cell>, + cx: &mut Context<'_>, + try_lock: impl FnOnce(&Self) -> bool, + is_write: bool, + ) -> Poll<()> { + if try_lock(self) { + // If we successfully acquired the lock, remove our waiter slot if we registered one + if let Some(index) = waiter_index.get() { + let waiters = if is_write { + &self.write_waiters + } else { + &self.read_waiters + }; + let mut waiters = waiters.borrow_mut(); + if index < waiters.len() { + waiters[index] = None; + } + } + return Poll::Ready(()); + } + + // Register or update our waker in the appropriate waiters list + let waiters = if is_write { + &self.write_waiters + } else { + &self.read_waiters + }; + let mut waiters = waiters.borrow_mut(); + + if let Some(index) = waiter_index.get() { + // We already have a slot, check if we need to update it + if index < waiters.len() { + if let Some(existing) = &waiters[index] { + if !existing.will_wake(cx.waker()) { + waiters[index] = Some(cx.waker().clone()); + } + } else { + waiters[index] = Some(cx.waker().clone()); + } + } else { + // Our slot was somehow removed, register a new one + let new_index = waiters.len(); + waiters.push(Some(cx.waker().clone())); + waiter_index.set(Some(new_index)); + } + } else { + // First time registering + let index = waiters.len(); + waiters.push(Some(cx.waker().clone())); + waiter_index.set(Some(index)); + } + + Poll::Pending + } + + /// Cleanup waiter slot on drop + fn cleanup_waiter(&self, waiter_index: &Cell>, is_write: bool) { + if let Some(index) = waiter_index.get() { + let waiters = if is_write { + &self.write_waiters + } else { + &self.read_waiters + }; + let mut waiters = waiters.borrow_mut(); + if index < waiters.len() { + waiters[index] = None; + } + } + } +} + +// Guards with 'static lifetime (Rc-like) + +/// A read guard with a `'static` lifetime, holding an `Rc` to the lock. +pub struct LocalRwLockReadGuard { + inner: Rc>, +} + +impl LocalRwLockReadGuard { + /// Rebuild a handle to the lock from this [`LocalRwLockReadGuard`]. + pub fn lock_handle(me: &Self) -> LocalRwLock { + LocalRwLock { + inner: me.inner.clone(), + } + } +} + +impl Deref for LocalRwLockReadGuard { + type Target = T; + + fn deref(&self) -> &T { + unsafe { &*self.inner.value.get() } + } +} + +impl Drop for LocalRwLockReadGuard { + fn drop(&mut self) { + self.inner.release_read(); + } +} + +/// A write guard with a `'static` lifetime, holding an `Rc` to the lock. +pub struct LocalRwLockWriteGuard { + inner: Rc>, +} + +impl LocalRwLockWriteGuard { + /// Rebuild a handle to the lock from this [`LocalRwLockWriteGuard`]. + pub fn lock_handle(me: &Self) -> LocalRwLock { + LocalRwLock { + inner: me.inner.clone(), + } + } +} + +impl Deref for LocalRwLockWriteGuard { + type Target = T; + + fn deref(&self) -> &T { + unsafe { &*self.inner.value.get() } + } +} + +impl DerefMut for LocalRwLockWriteGuard { + fn deref_mut(&mut self) -> &mut T { + unsafe { &mut *self.inner.value.get() } + } +} + +impl Drop for LocalRwLockWriteGuard { + fn drop(&mut self) { + self.inner.release_write(); + } +} + +// Futures + +/// Future returned by `read_rc()`. +pub struct ReadFuture { + inner: Rc>, + waiter_index: Cell>, +} + +impl Future for ReadFuture { + type Output = LocalRwLockReadGuard; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self + .inner + .poll_lock(&self.waiter_index, cx, |inner| inner.try_read(), false) + .is_ready() + { + Poll::Ready(LocalRwLockReadGuard { + inner: self.inner.clone(), + }) + } else { + Poll::Pending + } + } +} + +impl Drop for ReadFuture { + fn drop(&mut self) { + self.inner.cleanup_waiter(&self.waiter_index, false); + } +} + +/// Future returned by `write_rc()`. +pub struct WriteFuture { + inner: Rc>, + waiter_index: Cell>, +} + +impl Future for WriteFuture { + type Output = LocalRwLockWriteGuard; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self + .inner + .poll_lock(&self.waiter_index, cx, |inner| inner.try_write(), true) + .is_ready() + { + Poll::Ready(LocalRwLockWriteGuard { + inner: self.inner.clone(), + }) + } else { + Poll::Pending + } + } +} + +impl Drop for WriteFuture { + fn drop(&mut self) { + self.inner.cleanup_waiter(&self.waiter_index, true); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_read_write() { + let lock = LocalRwLock::new(42); + + // Can acquire read lock + let guard = lock.try_read().unwrap(); + assert_eq!(*guard, 42); + drop(guard); + + // Can acquire write lock + let mut guard = lock.try_write().unwrap(); + *guard = 100; + drop(guard); + + // Value was updated + let guard = lock.try_read().unwrap(); + assert_eq!(*guard, 100); + } + + #[test] + fn test_multiple_readers() { + let lock = LocalRwLock::new(42); + + let guard1 = lock.try_read().unwrap(); + let guard2 = lock.try_read().unwrap(); + let guard3 = lock.try_read().unwrap(); + + assert_eq!(*guard1, 42); + assert_eq!(*guard2, 42); + assert_eq!(*guard3, 42); + + // Cannot acquire write lock while readers exist + assert!(lock.try_write().is_none()); + + drop(guard1); + assert!(lock.try_write().is_none()); + + drop(guard2); + assert!(lock.try_write().is_none()); + + drop(guard3); + // Now we can acquire write lock + assert!(lock.try_write().is_some()); + } + + #[test] + fn test_exclusive_writer() { + let lock = LocalRwLock::new(42); + + let guard = lock.try_write().unwrap(); + + // Cannot acquire another write lock + assert!(lock.try_write().is_none()); + + // Cannot acquire read lock + assert!(lock.try_read().is_none()); + + drop(guard); + + // Now we can acquire locks again + assert!(lock.try_read().is_some()); + } + + #[test] + fn test_arc_guards() { + let lock = LocalRwLock::new(42); + + // Try read_rc + let guard = lock.try_read().unwrap(); + assert_eq!(*guard, 42); + drop(guard); + + // Try write_rc + let mut guard = lock.try_write().unwrap(); + *guard = 100; + drop(guard); + + let guard = lock.try_read().unwrap(); + assert_eq!(*guard, 100); + } + + #[test] + fn test_writer_priority() { + use std::sync::{Arc, Mutex}; + use std::task::{Poll, Wake}; + + // Custom waker that tracks when it's woken + struct TrackingWaker { + name: &'static str, + wake_order: Arc>>, + } + + impl Wake for TrackingWaker { + fn wake(self: Arc) { + self.wake_order.lock().unwrap().push(self.name); + } + } + + let lock = LocalRwLock::new(0); + let wake_order = Arc::new(Mutex::new(Vec::new())); + + // Acquire write lock + let write_guard = lock.try_write().unwrap(); + + // Create read and write futures that will need to wait + let mut read_future = Box::pin(lock.read()); + let mut write_future = Box::pin(lock.write()); + + // Create tracking wakers + let read_waker = Arc::new(TrackingWaker { + name: "reader", + wake_order: wake_order.clone(), + }) + .into(); + let mut read_cx = std::task::Context::from_waker(&read_waker); + + let write_waker = Arc::new(TrackingWaker { + name: "writer", + wake_order: wake_order.clone(), + }) + .into(); + let mut write_cx = std::task::Context::from_waker(&write_waker); + + // Poll both to register them as waiters + assert!(matches!( + write_future.as_mut().poll(&mut write_cx), + Poll::Pending + )); + assert!(matches!( + read_future.as_mut().poll(&mut read_cx), + Poll::Pending + )); + + // Verify they're in separate queues + assert_eq!( + lock.inner + .write_waiters + .borrow() + .iter() + .filter(|w| w.is_some()) + .count(), + 1 + ); + assert_eq!( + lock.inner + .read_waiters + .borrow() + .iter() + .filter(|w| w.is_some()) + .count(), + 1 + ); + + // No wakers called yet + assert_eq!(wake_order.lock().unwrap().len(), 0); + + // Release the write lock - this should wake waiters + drop(write_guard); + + // Verify ONLY writer was woken (not reader, since writer has priority) + let order = wake_order.lock().unwrap(); + assert_eq!( + order.len(), + 1, + "Only writer should be woken when writers are waiting" + ); + assert_eq!(order[0], "writer", "Writer should be woken, not reader"); + } + + #[test] + fn test_readers_woken_when_no_writers() { + use std::sync::{Arc, Mutex}; + use std::task::{Poll, Wake}; + + // Custom waker that tracks when it's woken + struct TrackingWaker { + name: String, + wake_order: Arc>>, + } + + impl Wake for TrackingWaker { + fn wake(self: Arc) { + self.wake_order.lock().unwrap().push(self.name.clone()); + } + } + + let lock = LocalRwLock::new(0); + let wake_order = Arc::new(Mutex::new(Vec::new())); + + // Acquire write lock + let write_guard = lock.try_write().unwrap(); + + // Create multiple read futures (no writers) + let mut read_future1 = Box::pin(lock.read()); + let mut read_future2 = Box::pin(lock.read()); + + // Create tracking wakers + let read_waker1 = Arc::new(TrackingWaker { + name: "reader1".to_string(), + wake_order: wake_order.clone(), + }) + .into(); + let mut read_cx1 = std::task::Context::from_waker(&read_waker1); + + let read_waker2 = Arc::new(TrackingWaker { + name: "reader2".to_string(), + wake_order: wake_order.clone(), + }) + .into(); + let mut read_cx2 = std::task::Context::from_waker(&read_waker2); + + // Poll both to register them as waiters + assert!(matches!( + read_future1.as_mut().poll(&mut read_cx1), + Poll::Pending + )); + assert!(matches!( + read_future2.as_mut().poll(&mut read_cx2), + Poll::Pending + )); + + // Verify they're in read queue + assert_eq!( + lock.inner + .read_waiters + .borrow() + .iter() + .filter(|w| w.is_some()) + .count(), + 2 + ); + assert_eq!( + lock.inner + .write_waiters + .borrow() + .iter() + .filter(|w| w.is_some()) + .count(), + 0 + ); + + // No wakers called yet + assert_eq!(wake_order.lock().unwrap().len(), 0); + + // Release the write lock - this should wake all readers since no writers are waiting + drop(write_guard); + + // Verify both readers were woken + let order = wake_order.lock().unwrap(); + assert_eq!( + order.len(), + 2, + "Both readers should be woken when no writers are waiting" + ); + assert!(order.contains(&"reader1".to_string())); + assert!(order.contains(&"reader2".to_string())); + } +} diff --git a/lib/api/src/entities/store/mod.rs b/lib/api/src/entities/store/mod.rs index cc403861f41..e1aee0709fa 100644 --- a/lib/api/src/entities/store/mod.rs +++ b/lib/api/src/entities/store/mod.rs @@ -1,17 +1,39 @@ //! Defines the [`Store`] data type and various useful traits and data types to interact with a //! store. +/// Defines the [`AsStoreAsync`] trait and its supporting types. +#[cfg(feature = "experimental-async")] +mod async_; +#[cfg(feature = "experimental-async")] +pub use async_::*; + +/// Defines the [`StoreContext`] type. +mod context; + /// Defines the [`StoreInner`] data type. mod inner; /// Create temporary handles to engines. mod store_ref; + +/// Single-threaded async-aware RwLock. +#[cfg(feature = "experimental-async")] +mod local_rwlock; +#[cfg(feature = "experimental-async")] +pub(crate) use local_rwlock::*; + +use std::{ + boxed::Box, + ops::{Deref, DerefMut}, +}; + pub use store_ref::*; mod obj; pub use obj::*; use crate::{AsEngineRef, BackendEngine, Engine, EngineRef}; +pub(crate) use context::*; pub(crate) use inner::*; use wasmer_types::StoreId; @@ -108,6 +130,16 @@ impl Store { pub fn id(&self) -> StoreId { self.inner.objects.id() } + + #[cfg(feature = "experimental-async")] + /// Transforms this store into a [`StoreAsync`] which can be used + /// to invoke [`Function::call_async`](crate::Function::call_async). + pub fn into_async(self) -> StoreAsync { + StoreAsync { + id: self.id(), + inner: LocalRwLock::new(self.inner), + } + } } impl PartialEq for Store { diff --git a/lib/api/src/entities/store/store_ref.rs b/lib/api/src/entities/store/store_ref.rs index 6ec36ece852..42ad59ec125 100644 --- a/lib/api/src/entities/store/store_ref.rs +++ b/lib/api/src/entities/store/store_ref.rs @@ -2,6 +2,8 @@ use std::ops::{Deref, DerefMut}; use super::{StoreObjects, inner::StoreInner}; use crate::entities::engine::{AsEngineRef, Engine, EngineRef}; +#[cfg(feature = "experimental-async")] +use crate::{AsStoreAsync, StoreAsync}; use wasmer_types::{ExternType, OnCalledAction}; //use wasmer_vm::{StoreObjects, TrapHandlerFn}; @@ -90,6 +92,17 @@ impl StoreMut<'_> { pub trait AsStoreRef { /// Returns a `StoreRef` pointing to the underlying context. fn as_store_ref(&self) -> StoreRef<'_>; + + /// Returns a [`StoreAsync`] if the current + /// context is asynchronous. The store will be locked since + /// it's already active in the current context, but can be used + /// to spawn new coroutines via + /// [`Function::call_async`](crate::Function::call_async). + #[cfg(feature = "experimental-async")] + fn as_store_async(&self) -> Option { + let id = self.as_store_ref().inner.objects.id(); + StoreAsync::from_context(id) + } } /// Helper trait for a value that is convertible to a [`StoreMut`]. diff --git a/lib/api/src/entities/value.rs b/lib/api/src/entities/value.rs index e50da08c10e..06ec15ec82b 100644 --- a/lib/api/src/entities/value.rs +++ b/lib/api/src/entities/value.rs @@ -208,7 +208,7 @@ impl Value { crate::BackendStore::Sys(_) => Self::ExceptionRef( unsafe { crate::backend::sys::vm::VMExceptionRef::from_raw( - store.as_store_ref().objects().id(), + store.objects_mut().id(), raw, ) } diff --git a/lib/api/src/error.rs b/lib/api/src/error.rs index c283b8c106f..f80ffb67da8 100644 --- a/lib/api/src/error.rs +++ b/lib/api/src/error.rs @@ -265,6 +265,13 @@ impl RuntimeError { } Ok(()) } + + pub(crate) fn from_dyn(err: Box) -> Self { + match err.downcast::() { + Ok(runtime_error) => *runtime_error, + Err(error) => Trap::user(error), + } + } } impl std::fmt::Debug for RuntimeError { diff --git a/lib/api/src/utils/native/typed_func.rs b/lib/api/src/utils/native/typed_func.rs index edc32439c32..6368db56ca1 100644 --- a/lib/api/src/utils/native/typed_func.rs +++ b/lib/api/src/utils/native/typed_func.rs @@ -7,10 +7,13 @@ //! let add_one = instance.exports.get_function("function_name")?; //! let add_one_native: TypedFunction = add_one.native().unwrap(); //! ``` +#[cfg(feature = "experimental-async")] +use crate::AsStoreAsync; use crate::{ AsStoreMut, BackendStore, FromToNativeWasmType, Function, NativeWasmTypeInto, RuntimeError, WasmTypeList, store::AsStoreRef, }; +use std::future::Future; use std::marker::PhantomData; use wasmer_types::RawValue; @@ -78,6 +81,45 @@ macro_rules! impl_native_traits { } } + /// Call the typed func asynchronously. + #[cfg(feature = "experimental-async")] + #[allow(unused_mut)] + #[allow(clippy::too_many_arguments)] + pub fn call_async( + &self, + store: &impl AsStoreAsync, + $( $x: $x, )* + ) -> impl Future> + Sized + 'static + where + $( $x: FromToNativeWasmType + 'static, )* + { + $( + let [] = $x; + )* + let store = store.store(); + let func = self.func.clone(); + async move { + let read_lock = store.read_lock().await; + match read_lock.as_store_ref().inner.store { + #[cfg(feature = "sys")] + BackendStore::Sys(_) => { + drop(read_lock); + Self::call_async_sys_internal(func, store, $([]),*).await + } + #[cfg(feature = "wamr")] + BackendStore::Wamr(_) => async_backend_error(), + #[cfg(feature = "wasmi")] + BackendStore::Wasmi(_) => async_backend_error(), + #[cfg(feature = "v8")] + BackendStore::V8(_) => async_backend_error(), + #[cfg(feature = "js")] + BackendStore::Js(_) => async_backend_error(), + #[cfg(feature = "jsc")] + BackendStore::Jsc(_) => async_backend_error(), + } + } + } + #[doc(hidden)] #[allow(missing_docs)] #[allow(unused_mut)] @@ -136,3 +178,9 @@ 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 ); + +fn async_backend_error() -> Result { + Err(RuntimeError::new( + "async calls are only supported with the `sys` backend", + )) +} diff --git a/lib/api/tests/instance.rs b/lib/api/tests/instance.rs index a0c5e48ed3a..e6e954d47f5 100644 --- a/lib/api/tests/instance.rs +++ b/lib/api/tests/instance.rs @@ -61,7 +61,7 @@ fn unit_native_function_env() -> Result<(), String> { multiplier: u32, } - fn imported_fn(env: FunctionEnvMut, args: &[Value]) -> Result, RuntimeError> { + fn imported_fn(env: FunctionEnvMut, args: &[Value]) -> DynamicFunctionResult { let value = env.data().multiplier * args[0].unwrap_i32() as u32; Ok(vec![Value::I32(value as _)]) } diff --git a/lib/api/tests/jspi_async.rs b/lib/api/tests/jspi_async.rs new file mode 100644 index 00000000000..5eec79d31b9 --- /dev/null +++ b/lib/api/tests/jspi_async.rs @@ -0,0 +1,305 @@ +#![cfg(feature = "experimental-async")] + +use std::{cell::RefCell, sync::OnceLock}; + +use anyhow::Result; +use futures::future; +use wasmer::{ + AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Module, + Store, StoreAsync, Type, TypedFunction, Value, imports, +}; +use wasmer_vm::TrapCode; + +#[derive(Default)] +struct DeltaState { + deltas: Vec, + index: usize, +} + +impl DeltaState { + fn next(&mut self) -> f64 { + let value = self.deltas.get(self.index).copied().unwrap_or(0.0); + self.index += 1; + value + } +} + +fn jspi_module() -> &'static [u8] { + static BYTES: OnceLock> = OnceLock::new(); + const JSPI_WAT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/examples/jspi.wat"); + BYTES.get_or_init(|| wat::parse_file(JSPI_WAT).expect("valid example module")) +} + +#[test] +fn async_state_updates_follow_jspi_example() -> Result<()> { + let wasm = jspi_module(); + let mut store = Store::default(); + let module = Module::new(&store, wasm)?; + + let init_state = Function::new_async( + &mut store, + FunctionType::new(vec![], vec![Type::F64]), + |_values| async move { + // Note: future::ready doesn't actually suspend. It's important + // to note that, while we're in an async import here, it's + // impossible to suspend during module instantiation, which is + // where this import is called. + // To see this in action, uncomment the following line: + // tokio::task::yield_now().await; + future::ready(()).await; + Ok(vec![Value::F64(1.0)]) + }, + ); + + let delta_env = FunctionEnv::new( + &mut store, + DeltaState { + deltas: vec![0.5, -1.0, 2.5], + index: 0, + }, + ); + let compute_delta = Function::new_with_env_async( + &mut store, + &delta_env, + FunctionType::new(vec![], vec![Type::F64]), + |env: AsyncFunctionEnvMut, _values| async move { + // Note: holding a lock across an await point prevents + // other coroutines from progressing, so it's a good + // idea to drop the lock before awaiting. + let delta = { + let mut env_write = env.write().await; + env_write.data_mut().next() + }; + // We can, however, actually suspend whenever + // `Function::call_async` is used to call WASM functions. + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + Ok(vec![Value::F64(delta)]) + }, + ); + + let import_object = imports! { + "js" => { + "init_state" => init_state, + "compute_delta" => compute_delta, + } + }; + + let instance = Instance::new(&mut store, &module, &import_object)?; + let get_state = instance.exports.get_function("get_state")?; + let update_state = instance.exports.get_function("update_state")?; + + fn as_f64(values: &[Value]) -> f64 { + match &values[0] { + Value::F64(v) => *v, + other => panic!("expected f64 value, got {other:?}"), + } + } + + assert_eq!(as_f64(&get_state.call(&mut store, &[])?), 1.0); + + let step = |store: &StoreAsync, func: &wasmer::Function| -> Result { + let result = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(func.call_async(store, vec![]))?; + Ok(as_f64(&result)) + }; + + let store_async = store.into_async(); + + assert_eq!(step(&store_async, update_state)?, 1.5); + assert_eq!(step(&store_async, update_state)?, 0.5); + assert_eq!(step(&store_async, update_state)?, 3.0); + + Ok(()) +} + +#[test] +fn typed_async_host_and_calls_work() -> Result<()> { + let wasm = wat::parse_str( + r#" + (module + (import "host" "async_add" (func $async_add (param i32 i32) (result i32))) + (import "host" "async_double" (func $async_double (param i32) (result i32))) + (func (export "compute") (param i32) (result i32) + local.get 0 + i32.const 10 + call $async_add + local.get 0 + call $async_double + i32.add)) + "#, + )?; + + #[derive(Clone, Copy)] + struct AddBias { + bias: i32, + } + + let mut store = Store::default(); + let module = Module::new(&store, wasm)?; + + let add_env = FunctionEnv::new(&mut store, AddBias { bias: 5 }); + let async_add = Function::new_typed_with_env_async( + &mut store, + &add_env, + async move |env: AsyncFunctionEnvMut, a: i32, b: i32| { + let env_read = env.read().await; + let bias = env_read.data().bias; + tokio::task::yield_now().await; + a + b + bias + }, + ); + let async_double = Function::new_typed_async(&mut store, async move |value: i32| { + tokio::task::yield_now().await; + value * 2 + }); + + let import_object = imports! { + "host" => { + "async_add" => async_add, + "async_double" => async_double, + } + }; + + let instance = Instance::new(&mut store, &module, &import_object)?; + let compute: TypedFunction = + instance.exports.get_typed_function(&mut store, "compute")?; + + let store_async = store.into_async(); + + let result = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(compute.call_async(&store_async, 4))?; + assert_eq!(result, 27); + + Ok(()) +} + +#[test] +fn cannot_yield_when_not_in_async_context() -> Result<()> { + const WAT: &str = r#" + (module + (import "env" "yield_now" (func $yield_now)) + (func (export "yield_outside") + call $yield_now + ) + ) + "#; + let wasm = wat::parse_str(WAT).expect("valid WAT module"); + + let mut store = Store::default(); + let module = Module::new(&store, wasm)?; + + let yield_now = Function::new_async( + &mut store, + FunctionType::new(vec![], vec![]), + |_values| async move { + // Attempting to yield when not in an async context should trap. + tokio::task::yield_now().await; + Ok(vec![]) + }, + ); + + let import_object = imports! { + "env" => { + "yield_now" => yield_now, + } + }; + let instance = Instance::new(&mut store, &module, &import_object)?; + let yield_outside = instance.exports.get_function("yield_outside")?; + + let trap = yield_outside + .call(&mut store, &[]) + .expect_err("expected trap calling yield outside async context"); + + // TODO: wasm trace generation appears to be broken? + // assert!(!trap.trace().is_empty(), "should have a stack trace"); + let trap_code = trap.to_trap().expect("expected trap code"); + assert_eq!( + trap_code, + TrapCode::YieldOutsideAsyncContext, + "expected YieldOutsideAsyncContext trap code" + ); + + Ok(()) +} + +#[test] +fn nested_async_in_sync() -> Result<()> { + const WAT: &str = r#" + (module + (import "env" "sync" (func $sync (result i32))) + (import "env" "async" (func $async (result i32))) + (func (export "entry") (result i32) + call $sync + ) + (func (export "inner_async") (result i32) + call $async + ) + ) + "#; + let wasm = wat::parse_str(WAT).expect("valid WAT module"); + + let mut store = Store::default(); + let module = Module::new(&store, wasm)?; + + struct Env { + inner_async: RefCell>>, + } + let env = FunctionEnv::new( + &mut store, + Env { + inner_async: RefCell::new(None), + }, + ); + + let sync = Function::new_typed_with_env(&mut store, &env, |mut env: FunctionEnvMut| { + let (env, mut store) = env.data_and_store_mut(); + env.inner_async + .borrow() + .as_ref() + .expect("inner_async function to be set") + .call(&mut store) + .expect("inner async call to succeed") + }); + + let async_ = Function::new_typed_async(&mut store, async || { + tokio::task::yield_now().await; + 42 + }); + + let imports = imports! { + "env" => { + "sync" => sync, + "async" => async_, + } + }; + + let instance = Instance::new(&mut store, &module, &imports)?; + + let inner_async = instance + .exports + .get_typed_function::<(), i32>(&mut store, "inner_async") + .unwrap(); + env.as_mut(&mut store) + .inner_async + .borrow_mut() + .replace(inner_async); + + let entry = instance + .exports + .get_typed_function::<(), i32>(&mut store, "entry")?; + let result = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(entry.call_async(&store.into_async()))?; + + assert_eq!(result, 42); + + Ok(()) +} diff --git a/lib/api/tests/reference_types.rs b/lib/api/tests/reference_types.rs index 3350873bfe4..e3df626cb8a 100644 --- a/lib/api/tests/reference_types.rs +++ b/lib/api/tests/reference_types.rs @@ -82,10 +82,7 @@ pub mod reference_types { )"#; let module = Module::new(&store, wat)?; let env = FunctionEnv::new(&mut store, ()); - fn func_ref_call( - mut env: FunctionEnvMut<()>, - values: &[Value], - ) -> Result, RuntimeError> { + fn func_ref_call(mut env: FunctionEnvMut<()>, values: &[Value]) -> DynamicFunctionResult { // TODO: look into `Box<[Value]>` being returned breakage let f = values[0].unwrap_funcref().as_ref().unwrap(); let f: TypedFunction<(i32, i32), i32> = f.typed(&env)?; diff --git a/lib/api/tests/simple_greenthread.rs b/lib/api/tests/simple_greenthread.rs new file mode 100644 index 00000000000..be8ee26c390 --- /dev/null +++ b/lib/api/tests/simple_greenthread.rs @@ -0,0 +1,275 @@ +#![cfg(feature = "experimental-async")] + +use std::collections::BTreeMap; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use futures::task::LocalSpawnExt; +use futures::{FutureExt, channel::oneshot}; +use wasmer::{ + AsyncFunctionEnvMut, Function, FunctionEnv, FunctionEnvMut, FunctionType, Instance, Memory, + Module, RuntimeError, Store, Type, Value, imports, +}; + +struct GreenEnv { + logs: Vec, + memory: Option, + greenthreads: Arc>>, + current_greenthread_id: Arc>, + next_free_id: AtomicU32, + entrypoint: Option, + spawner: Option, +} + +// Required for carrying the spawner around. Safe because we don't do threads. +// It worked before with a thread-local! spawner, so this is equivalent. +// The thread-local version does not work with multiple tests +unsafe impl Send for GreenEnv {} +unsafe impl Sync for GreenEnv {} + +impl GreenEnv { + fn new() -> Self { + Self { + logs: Vec::new(), + memory: None, + greenthreads: Arc::new(RwLock::new(BTreeMap::new())), + current_greenthread_id: Arc::new(RwLock::new(0)), + next_free_id: AtomicU32::new(1), + entrypoint: None, + spawner: None, + } + } +} + +struct Greenthread { + entrypoint: Option, + resumer: Option>, +} + +impl Clone for Greenthread { + fn clone(&self) -> Self { + if self.resumer.is_some() { + panic!("Cannot clone a greenthread with a resumer"); + } + Self { + entrypoint: self.entrypoint, + resumer: None, + } + } +} + +async fn greenthread_new( + env: AsyncFunctionEnvMut, + entrypoint_data: u32, +) -> core::result::Result { + let async_store = env.as_store_async(); + let mut env_write = env.write().await; + let data = env_write.data_mut(); + let new_greenthread_id = data + .next_free_id + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + let function = data.entrypoint.clone().expect("entrypoint set"); + let (sender, receiver) = oneshot::channel::<()>(); + + let new_greenthread = Greenthread { + entrypoint: Some(entrypoint_data), + resumer: Some(sender), + }; + + data.greenthreads + .write() + .unwrap() + .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(); + + Ok(new_greenthread_id) +} + +async fn greenthread_switch(env: AsyncFunctionEnvMut, next_greenthread_id: u32) { + let (receiver, current_id_arc, current_greenthread_id) = { + let mut write_lock = env.write().await; + let data = write_lock.data_mut(); + + let current_greenthread_id = { + let mut current = data.current_greenthread_id.write().unwrap(); + let old = *current; + *current = next_greenthread_id; + old + }; + + if current_greenthread_id == next_greenthread_id { + panic!("Switching to self is not allowed"); + } + + let (sender, receiver) = oneshot::channel::<()>(); + + { + let mut greenthreads = data.greenthreads.write().unwrap(); + let this_one = greenthreads.get_mut(¤t_greenthread_id).unwrap(); + if this_one.resumer.is_some() { + panic!("Switching from a greenthread that is already switched out"); + } + this_one.resumer = Some(sender); + } + + { + let mut greenthreads = data.greenthreads.write().unwrap(); + let next_one = greenthreads.get_mut(&next_greenthread_id).unwrap(); + let Some(resumer) = next_one.resumer.take() else { + panic!("Switching to greenthread that has no resumer"); + }; + resumer.send(()).unwrap(); + } + let current_id_arc = data.current_greenthread_id.clone(); + + (receiver, current_id_arc, current_greenthread_id) + }; + + let _ = receiver.map(|_| ()).await; + + *current_id_arc.write().unwrap() = current_greenthread_id; +} + +fn run_greenthread_test(wat: &[u8]) -> Result> { + let mut store = Store::default(); + let module = Module::new(&store.engine(), wat)?; + + let env = FunctionEnv::new(&mut store, GreenEnv::new()); + + // log(ptr, len) + let log_fn = Function::new_with_env( + &mut store, + &env, + FunctionType::new(vec![Type::I32, Type::I32], vec![]), + |mut env: FunctionEnvMut, params: &[Value]| { + let ptr = params[0].unwrap_i32() as u32; + let len = params[1].unwrap_i32() as u32; + let (data, storemut) = env.data_and_store_mut(); + let memory = data.memory.as_ref().expect("memory set"); + let view = memory.view(&storemut); + let mut bytes = Vec::with_capacity(len as usize); + for i in ptr..ptr + len { + bytes.push(view.read_u8(i as u64).expect("in bounds")); + } + let s = String::from_utf8_lossy(&bytes).to_string(); + data.logs.push(s.trim_matches('\0').to_string()); + Ok(vec![]) + }, + ); + + let greenthread_new = Function::new_typed_with_env_async(&mut store, &env, greenthread_new); + + let greenthread_switch = + Function::new_typed_with_env_async(&mut store, &env, greenthread_switch); + + let import_object = imports! { + "test" => { + "log" => log_fn, + "greenthread_new" => greenthread_new, + "greenthread_switch" => greenthread_switch, + } + }; + + let instance = Instance::new(&mut store, &module, &import_object)?; + + let entrypoint = instance.exports.get_function("entrypoint")?.clone(); + env.as_mut(&mut store).entrypoint = Some(entrypoint); + + let memory = instance.exports.get_memory("memory")?.clone(); + env.as_mut(&mut store).memory = Some(memory); + + let main_fn = instance.exports.get_function("_main")?; + + let main_greenthread = Greenthread { + entrypoint: None, + resumer: None, + }; + + env.as_mut(&mut store) + .greenthreads + .write() + .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); + + let store_async = store.into_async(); + + localpool + .run_until(main_fn.call_async(&store_async, vec![])) + .unwrap(); + + // 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 store = store_async.into_store().ok().unwrap(); + return Ok(env.as_ref(&store).logs.clone()); +} + +#[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); + } + + Ok(()) +} + +#[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); + } + + Ok(()) +} diff --git a/lib/c-api/src/wasm_c_api/externals/function.rs b/lib/c-api/src/wasm_c_api/externals/function.rs index ed6c55d4830..c64c0741d7f 100644 --- a/lib/c-api/src/wasm_c_api/externals/function.rs +++ b/lib/c-api/src/wasm_c_api/externals/function.rs @@ -8,7 +8,7 @@ use libc::c_void; use std::convert::TryInto; use std::mem::MaybeUninit; use std::sync::{Arc, Mutex}; -use wasmer_api::{Extern, Function, FunctionEnv, FunctionEnvMut, RuntimeError, Value}; +use wasmer_api::{DynamicFunctionResult, Extern, Function, FunctionEnv, FunctionEnvMut, Value}; #[derive(Clone)] #[allow(non_camel_case_types)] @@ -57,7 +57,7 @@ pub unsafe extern "C" fn wasm_func_new( let num_rets = func_sig.results().len(); let inner_callback = move |mut _env: FunctionEnvMut<'_, FunctionCEnv>, args: &[Value]| - -> Result, RuntimeError> { + -> DynamicFunctionResult { let processed_args: wasm_val_vec_t = args .iter() .map(TryInto::try_into) @@ -134,40 +134,39 @@ pub unsafe extern "C" fn wasm_func_new_with_env( } } } - let inner_callback = move |env: FunctionEnvMut<'_, WrapperEnv>, - args: &[Value]| - -> Result, RuntimeError> { - let processed_args: wasm_val_vec_t = args - .iter() - .map(TryInto::try_into) - .collect::, _>>() - .expect("Argument conversion failed") + let inner_callback = + move |env: FunctionEnvMut<'_, WrapperEnv>, args: &[Value]| -> DynamicFunctionResult { + let processed_args: wasm_val_vec_t = args + .iter() + .map(TryInto::try_into) + .collect::, _>>() + .expect("Argument conversion failed") + .into(); + + let mut results: wasm_val_vec_t = vec![ + wasm_val_t { + kind: wasm_valkind_enum::WASM_I64 as _, + of: wasm_val_inner { int64_t: 0 }, + }; + num_rets + ] .into(); - let mut results: wasm_val_vec_t = vec![ - wasm_val_t { - kind: wasm_valkind_enum::WASM_I64 as _, - of: wasm_val_inner { int64_t: 0 }, - }; - num_rets - ] - .into(); + let trap = unsafe { callback(env.data().env.as_ptr(), &processed_args, &mut results) }; - let trap = unsafe { callback(env.data().env.as_ptr(), &processed_args, &mut results) }; + if let Some(trap) = trap { + return Err(trap.inner); + } - if let Some(trap) = trap { - return Err(trap.inner); - } + let processed_results = results + .take() + .into_iter() + .map(TryInto::try_into) + .collect::, _>>() + .expect("Result conversion failed"); - let processed_results = results - .take() - .into_iter() - .map(TryInto::try_into) - .collect::, _>>() - .expect("Result conversion failed"); - - Ok(processed_results) - }; + Ok(processed_results) + }; let env = FunctionEnv::new( &mut store_mut, WrapperEnv { diff --git a/lib/cli/src/commands/run/mod.rs b/lib/cli/src/commands/run/mod.rs index 881681a8967..87deae32795 100644 --- a/lib/cli/src/commands/run/mod.rs +++ b/lib/cli/src/commands/run/mod.rs @@ -29,8 +29,8 @@ use url::Url; #[cfg(feature = "sys")] use wasmer::sys::NativeEngineExt; use wasmer::{ - AsStoreMut, DeserializeError, Engine, Function, Imports, Instance, Module, RuntimeError, Store, - Type, TypedFunction, Value, + AsStoreMut, DeserializeError, DynamicCallResult, Engine, Function, Imports, Instance, Module, + RuntimeError, Store, Type, TypedFunction, Value, }; use wasmer_types::{Features, target::Target}; @@ -645,7 +645,7 @@ fn invoke_function( store: &mut Store, func: &Function, args: &[String], -) -> anyhow::Result, RuntimeError>> { +) -> anyhow::Result { let func_ty = func.ty(store); let required_arguments = func_ty.params().len(); let provided_arguments = args.len(); diff --git a/lib/compiler-singlepass/src/codegen.rs b/lib/compiler-singlepass/src/codegen.rs index b76f8260b63..08606da51c2 100644 --- a/lib/compiler-singlepass/src/codegen.rs +++ b/lib/compiler-singlepass/src/codegen.rs @@ -3641,7 +3641,7 @@ impl<'a, M: Machine> FuncGen<'a, M> { && (return_type == Type::F32 || return_type == Type::F64) { self.machine.emit_function_return_float()?; - }; + } self.machine.emit_ret()?; } else { let released = &self.value_stack.clone()[frame.value_stack_depth_after()..]; diff --git a/lib/compiler/src/engine/artifact.rs b/lib/compiler/src/engine/artifact.rs index 2189bf80e4a..23a8be9fdeb 100644 --- a/lib/compiler/src/engine/artifact.rs +++ b/lib/compiler/src/engine/artifact.rs @@ -454,7 +454,7 @@ impl Artifact { get_got_address(RelocationTarget::LibCall(wasmer_vm::LibCall::EHPersonality)), )?; - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "compiler"))] { engine_inner.register_perfmap(&finished_functions, module_info)?; } diff --git a/lib/compiler/src/engine/inner.rs b/lib/compiler/src/engine/inner.rs index abf898c2cec..62355ed5dfe 100644 --- a/lib/compiler/src/engine/inner.rs +++ b/lib/compiler/src/engine/inner.rs @@ -13,11 +13,15 @@ use wasmer_types::{CompileError, HashAlgorithm, target::Target}; #[cfg(not(target_arch = "wasm32"))] use shared_buffer::OwnedBuffer; +#[cfg(all(not(target_arch = "wasm32"), feature = "compiler"))] +use std::io::Write; #[cfg(not(target_arch = "wasm32"))] -use std::{io::Write, path::Path}; +use std::path::Path; +#[cfg(all(not(target_arch = "wasm32"), feature = "compiler"))] +use wasmer_types::ModuleInfo; #[cfg(not(target_arch = "wasm32"))] use wasmer_types::{ - DeserializeError, FunctionIndex, FunctionType, LocalFunctionIndex, ModuleInfo, SignatureIndex, + DeserializeError, FunctionIndex, FunctionType, LocalFunctionIndex, SignatureIndex, entity::PrimaryMap, }; @@ -540,7 +544,7 @@ impl EngineInner { .register_frame_info(frame_info); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "compiler"))] pub(crate) fn register_perfmap( &self, finished_functions: &PrimaryMap, diff --git a/lib/compiler/src/misc.rs b/lib/compiler/src/misc.rs index d6c2b9ed8ad..3511b6baac0 100644 --- a/lib/compiler/src/misc.rs +++ b/lib/compiler/src/misc.rs @@ -1,17 +1,18 @@ //! A common functionality used among various compilers. use core::fmt::Display; -use std::{ - collections::HashMap, - path::PathBuf, - process::{Command, Stdio}, -}; +use std::{collections::HashMap, path::PathBuf}; + +#[cfg(not(target_arch = "wasm32"))] +use std::process::{Command, Stdio}; use itertools::Itertools; use target_lexicon::Architecture; -use tempfile::NamedTempFile; use wasmer_types::{CompileError, FunctionType, LocalFunctionIndex, Type}; +#[cfg(not(target_arch = "wasm32"))] +use tempfile::NamedTempFile; + /// Represents the kind of compiled function or module, used for debugging and identification /// purposes across multiple compiler backends (e.g., LLVM, Cranelift). #[derive(Debug, Clone)] diff --git a/lib/types/src/trapcode.rs b/lib/types/src/trapcode.rs index f1bee87d1a4..2e85a785a83 100644 --- a/lib/types/src/trapcode.rs +++ b/lib/types/src/trapcode.rs @@ -66,6 +66,10 @@ pub enum TrapCode { /// A throw_ref was executed but the exnref was not initialized. UninitializedExnRef = 12, + + /// An async imported function tried to yield when not called + /// via `Function::call_async`. + YieldOutsideAsyncContext = 13, } impl TrapCode { @@ -85,6 +89,9 @@ impl TrapCode { Self::UnalignedAtomic => "unaligned atomic access", Self::UncaughtException => "uncaught exception", Self::UninitializedExnRef => "uninitialized exnref", + Self::YieldOutsideAsyncContext => { + "async imported function yielded when not called via `Function::call_async`" + } } } } @@ -105,6 +112,7 @@ impl Display for TrapCode { Self::UnalignedAtomic => "unalign_atom", Self::UncaughtException => "uncaught_exception", Self::UninitializedExnRef => "uninitialized_exnref", + Self::YieldOutsideAsyncContext => "yield_outside_async_context", }; f.write_str(identifier) } @@ -128,6 +136,7 @@ impl FromStr for TrapCode { "unalign_atom" => Ok(Self::UnalignedAtomic), "uncaught_exception" => Ok(Self::UncaughtException), "uninitialized_exnref" => Ok(Self::UninitializedExnRef), + "yield_outside_async_context" => Ok(Self::YieldOutsideAsyncContext), _ => Err(()), } } diff --git a/lib/vm/src/store.rs b/lib/vm/src/store.rs index a4ded1927b9..0611e74912a 100644 --- a/lib/vm/src/store.rs +++ b/lib/vm/src/store.rs @@ -241,7 +241,7 @@ pub struct InternalStoreHandle { #[cfg(feature = "artifact-size")] impl loupe::MemoryUsage for InternalStoreHandle { fn size_of_val(&self, _tracker: &mut dyn loupe::MemoryUsageTracker) -> usize { - std::mem::size_of_val(&self) + std::mem::size_of_val(self) } } diff --git a/lib/wasix/Cargo.toml b/lib/wasix/Cargo.toml index 4be81db5fce..a46dbf9402f 100644 --- a/lib/wasix/Cargo.toml +++ b/lib/wasix/Cargo.toml @@ -173,7 +173,7 @@ wasm-bindgen-test.workspace = true tracing-wasm.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tracing-subscriber = { workspace = true} +tracing-subscriber = { workspace = true } wasmer = { path = "../api", version = "=6.1.0", default-features = false, features = [ "wat", "js-serializable-module", @@ -221,6 +221,7 @@ sys = [ "ctrlc", "wasmer/wat", "wasmer/js-serializable-module", + "wasmer/experimental-async", ] sys-default = ["sys", "wasmer/sys"] sys-poll = [] diff --git a/lib/wasix/src/bin_factory/exec.rs b/lib/wasix/src/bin_factory/exec.rs index d077394ddb8..b51ad958ae7 100644 --- a/lib/wasix/src/bin_factory/exec.rs +++ b/lib/wasix/src/bin_factory/exec.rs @@ -18,7 +18,7 @@ use crate::{ }; use tracing::*; use virtual_mio::block_on; -use wasmer::{Function, Memory32, Memory64, Module, RuntimeError, Store, Value}; +use wasmer::{DynamicCallResult, Function, Memory32, Memory64, Module, RuntimeError, Store}; use wasmer_wasix_types::wasi::Errno; use super::{BinaryPackage, BinaryPackageCommand}; @@ -397,8 +397,8 @@ fn resume_vfork( ctx: &WasiFunctionEnv, store: &mut Store, start: &Function, - call_ret: &Result, RuntimeError>, -) -> Result, RuntimeError>>, Errno> { + call_ret: &DynamicCallResult, +) -> Result, Errno> { let (err, code) = match call_ret { Ok(_) => (None, wasmer_wasix_types::wasi::ExitCode::from(0u16)), Err(err) => match err.downcast_ref::() { diff --git a/tests/compilers/typed_functions.rs b/tests/compilers/typed_functions.rs index 17754291b48..f93a3b8e612 100644 --- a/tests/compilers/typed_functions.rs +++ b/tests/compilers/typed_functions.rs @@ -22,7 +22,7 @@ fn long_f(a: u32, b: u32, c: u32, d: u32, e: u32, f: u16, g: u64, h: u64, i: u16 + a as u64 * 1000000000 } -fn long_f_dynamic(values: &[Value]) -> Result, RuntimeError> { +fn long_f_dynamic(values: &[Value]) -> DynamicFunctionResult { Ok(vec![Value::I64( values[9].unwrap_i32() as i64 + values[8].unwrap_i32() as i64 * 10 diff --git a/tests/examples/jspi.wat b/tests/examples/jspi.wat new file mode 100644 index 00000000000..71ed9b57181 --- /dev/null +++ b/tests/examples/jspi.wat @@ -0,0 +1,14 @@ +(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" (func $compute_delta (result f64))) + (global $state (mut f64) (f64.const 0)) + (func $init (global.set $state (call $init_state))) + (start $init) + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func $update_state (export "update_state") (result f64) + (local $delta f64) + (local.set $delta (call $compute_delta)) + (global.set $state (f64.add (global.get $state) (local.get $delta) )) + (global.get $state) + ) +) \ No newline at end of file diff --git a/tests/examples/simple-greenthread.wat b/tests/examples/simple-greenthread.wat new file mode 100644 index 00000000000..5984086a542 --- /dev/null +++ b/tests/examples/simple-greenthread.wat @@ -0,0 +1,101 @@ +(module + (import "test" "log" (func $log (param i32 i32))) + (import "test" "greenthread_new" (func $greenthread_new (param i32) (result i32))) + (import "test" "greenthread_switch" (func $greenthread_switch (param i32))) + + + (memory (export "memory") 1) + + (data (i32.const 0) "[gr1] main -> test1") + (data (i32.const 100) "[gr1] test1 <- test2") + (data (i32.const 200) "[gr2] test1 -> test2") + (data (i32.const 300) "[main] main <- test1") + + (global $main (mut i32) (i32.const 0)) + (global $gr1 (mut i32) (i32.const 0)) + (global $gr2 (mut i32) (i32.const 0)) + + (func (export "_main") + i32.const 0 + global.set $main + (call $greenthread_new (i32.const 0)) + global.set $gr1 + (call $greenthread_new (i32.const 1)) + global.set $gr2 + + ;; Switch to gr1 + global.get $gr1 + (call $greenthread_switch) + + ;; Print [main] main <- test1 + i32.const 300 + i32.const 20 + call $log + ) + (func $gr1 + ;; Print [gr1] main -> test1 + i32.const 0 + i32.const 20 + call $log + + ;; Switch to gr2 + global.get $gr2 + call $greenthread_switch + + ;; Print [gr1] test1 <- test2 + i32.const 100 + i32.const 20 + call $log + + ;; Switch to gr2 + global.get $gr2 + call $greenthread_switch + + ;; Print [gr1] test1 <- test2 + i32.const 100 + i32.const 20 + call $log + + ;; Switch back to main + global.get $main + call $greenthread_switch + unreachable + ) + (func $gr2 + ;; Print [gr2] test1 -> test2 + i32.const 200 + i32.const 20 + call $log + + ;; Switch to gr1 + global.get $gr1 + call $greenthread_switch + + ;; Print [gr2] test1 -> test2 + i32.const 200 + i32.const 20 + call $log + + ;; Switch to gr1 + global.get $gr1 + call $greenthread_switch + + unreachable + ) + (func (export "entrypoint") (param i32) + local.get 0 + i32.const 0 + i32.eq + (if + (then + (call $gr1) + ) + (else + (call $gr2) + )) + unreachable + + + ) + +) diff --git a/tests/examples/simple-greenthread2.wat b/tests/examples/simple-greenthread2.wat new file mode 100644 index 00000000000..064bb671d1b --- /dev/null +++ b/tests/examples/simple-greenthread2.wat @@ -0,0 +1,75 @@ +;; This example crashed in early versions of our async API because switching from the first context to another one worked only once. +(module + (import "test" "log" (func $log (param i32 i32))) + (import "test" "greenthread_new" (func $greenthread_new (param i32) (result i32))) + (import "test" "greenthread_switch" (func $greenthread_switch (param i32))) + + (memory (export "memory") 1) + + (data (i32.const 0) "[main] switching to side") + (data (i32.const 100) "[side] switching to main") + (data (i32.const 200) "[main] returned") + + (global $main (mut i32) (i32.const 0)) + (global $side (mut i32) (i32.const 0)) + + (func (export "_main") + ;; Setup thread ids + i32.const 0 + global.set $main + (call $greenthread_new (i32.const 0)) + global.set $side + + ;; Print [main] switching to side + i32.const 0 + i32.const 30 + call $log + + ;; Switch to side + global.get $side + (call $greenthread_switch) + + ;; Print [main] switching to side + i32.const 0 + i32.const 30 + call $log + + ;; Switch to side + global.get $side + (call $greenthread_switch) + + ;; Print [main] returned + i32.const 200 + i32.const 30 + call $log + ) + (func $side + ;; Print [side] switching to main + i32.const 100 + i32.const 30 + call $log + + ;; Switch to main + global.get $main + call $greenthread_switch + + ;; Print [side] switching to main + i32.const 100 + i32.const 30 + call $log + + ;; Switch to main + global.get $main + call $greenthread_switch + + unreachable + ) + (func (export "entrypoint") (param i32) + local.get 0 + i32.const 0 + i32.eq + call $side + unreachable + ) + +) diff --git a/tests/wasix/test.sh b/tests/wasix/test.sh index bf0be0bd9a2..dc197060231 100755 --- a/tests/wasix/test.sh +++ b/tests/wasix/test.sh @@ -7,21 +7,24 @@ printf "\n\nStarting WASIX Test Suite:\n" status=0 while read dir; do dir=$(basename "$dir") - printf "Testing $dir..." + printf "Testing $dir...\r" if [ -e "$dir/.no-build" ]; then cmd="cd $dir; \ + find . -name 'output*' | xargs rm -f; \ ./run.sh" else cmd="cd $dir; \ + find . -name 'output*' | xargs rm -f; \ + find . -name '*.wasm' | xargs rm -f; \ wasixcc main.c -o main.wasm; \ ./run.sh" fi if bash -c "$cmd"; then - printf "\rTesting $dir ✅\n" + printf "Testing $dir ✅\n" else - printf "\rTesting $dir ❌\n" + printf "Testing $dir ❌\n" status=1 fi done < <(find . -mindepth 1 -maxdepth 1 -type d | sort) diff --git a/tests/wasix/vfork/main.c b/tests/wasix/vfork/main.c index 25ba4054178..c1b1de2720f 100644 --- a/tests/wasix/vfork/main.c +++ b/tests/wasix/vfork/main.c @@ -269,9 +269,7 @@ int subprocess_trap(int fd) return 1; } - // A bad function pointer is guaranteed to trap one way or another - void (*f)(void) = (void (*)(void))0x12345678; - f(); + __builtin_trap(); return 1; } @@ -295,9 +293,7 @@ int trap_before_exec() return 1; } - // A bad function pointer is guaranteed to trap one way or another - void (*f)(void) = (void (*)(void))0x12345678; - f(); + __builtin_trap(); return 100; }