diff --git a/crates/wasmi/src/engine/mod.rs b/crates/wasmi/src/engine/mod.rs index d3d2377d50..95a82f59e8 100644 --- a/crates/wasmi/src/engine/mod.rs +++ b/crates/wasmi/src/engine/mod.rs @@ -26,7 +26,7 @@ pub use self::{ RelativeDepth, TranslationError, }, - resumable::{ResumableCall, ResumableInvocation}, + resumable::{ResumableCall, ResumableInvocation, TypedResumableCall, TypedResumableInvocation}, stack::StackLimits, traits::{CallParams, CallResults}, }; @@ -36,6 +36,7 @@ use self::{ code_map::CodeMap, executor::execute_frame, func_types::FuncTypeRegistry, + resumable::ResumableCallBase, stack::{FuncFrame, Stack, ValueStack}, }; pub(crate) use self::{ @@ -241,7 +242,7 @@ impl Engine { func: Func, params: impl CallParams, results: Results, - ) -> Result::Results>, Trap> + ) -> Result::Results>, Trap> where Results: CallResults, { @@ -277,7 +278,7 @@ impl Engine { invocation: ResumableInvocation, params: impl CallParams, results: Results, - ) -> Result::Results>, Trap> + ) -> Result::Results>, Trap> where Results: CallResults, { @@ -411,7 +412,7 @@ impl EngineInner { func: Func, params: impl CallParams, results: Results, - ) -> Result::Results>, Trap> + ) -> Result::Results>, Trap> where Results: CallResults, { @@ -426,7 +427,7 @@ impl EngineInner { match results { Ok(results) => { self.stacks.lock().recycle(stack); - Ok(ResumableCall::Finished(results)) + Ok(ResumableCallBase::Finished(results)) } Err(TaggedTrap::Wasm(trap)) => { self.stacks.lock().recycle(stack); @@ -435,7 +436,7 @@ impl EngineInner { Err(TaggedTrap::Host { host_func, host_trap, - }) => Ok(ResumableCall::Resumable(ResumableInvocation::new( + }) => Ok(ResumableCallBase::Resumable(ResumableInvocation::new( ctx.as_context().store.engine().clone(), func, host_func, @@ -451,7 +452,7 @@ impl EngineInner { mut invocation: ResumableInvocation, params: impl CallParams, results: Results, - ) -> Result::Results>, Trap> + ) -> Result::Results>, Trap> where Results: CallResults, { @@ -462,7 +463,7 @@ impl EngineInner { match results { Ok(results) => { self.stacks.lock().recycle(invocation.take_stack()); - Ok(ResumableCall::Finished(results)) + Ok(ResumableCallBase::Finished(results)) } Err(TaggedTrap::Wasm(trap)) => { self.stacks.lock().recycle(invocation.take_stack()); @@ -473,7 +474,7 @@ impl EngineInner { host_trap, }) => { invocation.update(host_func, host_trap); - Ok(ResumableCall::Resumable(invocation)) + Ok(ResumableCallBase::Resumable(invocation)) } } } diff --git a/crates/wasmi/src/engine/resumable.rs b/crates/wasmi/src/engine/resumable.rs index 76de42c010..9bc96421cf 100644 --- a/crates/wasmi/src/engine/resumable.rs +++ b/crates/wasmi/src/engine/resumable.rs @@ -1,18 +1,45 @@ use super::Func; -use crate::{engine::Stack, AsContextMut, Engine, Error}; -use core::mem::replace; +use crate::{engine::Stack, func::CallResultsTuple, AsContextMut, Engine, Error, WasmResults}; +use core::{fmt, marker::PhantomData, mem::replace, ops::Deref}; use wasmi_core::{Trap, Value}; -/// Returned by calling a function in a resumable way. +/// Returned by [`Engine`] methods for calling a function in a resumable way. +/// +/// # Note +/// +/// This is the base type for resumable call results and can be converted into +/// either the dynamically typed [`ResumableCall`] or the statically typed +/// [`TypedResumableCall`] that act as user facing API. Therefore this type +/// must provide all the information necessary to be properly converted into +/// either user facing types. #[derive(Debug)] -pub enum ResumableCall { +pub(crate) enum ResumableCallBase { /// The resumable call has finished properly and returned a result. Finished(T), /// The resumable call encountered a host error and can be resumed. Resumable(ResumableInvocation), } -/// State required to resume a function invocation. +/// Returned by calling a [`Func`] in a resumable way. +#[derive(Debug)] +pub enum ResumableCall { + /// The resumable call has finished properly and returned a result. + Finished, + /// The resumable call encountered a host error and can be resumed. + Resumable(ResumableInvocation), +} + +impl ResumableCall { + /// Creates a [`ResumableCall`] from the [`Engine`]'s base [`ResumableCallBase`]. + pub(crate) fn new(call: ResumableCallBase<()>) -> Self { + match call { + ResumableCallBase::Finished(()) => Self::Finished, + ResumableCallBase::Resumable(invocation) => Self::Resumable(invocation), + } + } +} + +/// State required to resume a [`Func`] invocation. #[derive(Debug)] pub struct ResumableInvocation { /// The engine in use for the function invokation. @@ -120,9 +147,9 @@ impl ResumableInvocation { &self.host_error } - /// Resumes the call to the Wasm or host function with the given inputs. + /// Resumes the call to the [`Func`] with the given inputs. /// - /// The result is written back into the `outputs` buffer. + /// The result is written back into the `outputs` buffer upon success. /// /// Returns a resumable handle to the function invocation upon /// enountering host errors with which it is possible to handle @@ -140,7 +167,7 @@ impl ResumableInvocation { mut ctx: impl AsContextMut, inputs: &[Value], outputs: &mut [Value], - ) -> Result, Error> { + ) -> Result { self.engine .resolve_func_type(self.host_func().signature(ctx.as_context()), |func_type| { func_type.match_results(inputs, true) @@ -155,5 +182,102 @@ impl ResumableInvocation { .clone() .resume_func(ctx.as_context_mut(), self, inputs, outputs) .map_err(Into::into) + .map(ResumableCall::new) + } +} + +/// Returned by calling a [`TypedFunc`] in a resumable way. +/// +/// [`TypedFunc`]: [`crate::TypedFunc`] +#[derive(Debug)] +pub enum TypedResumableCall { + /// The resumable call has finished properly and returned a result. + Finished(T), + /// The resumable call encountered a host error and can be resumed. + Resumable(TypedResumableInvocation), +} + +impl TypedResumableCall { + /// Creates a [`TypedResumableCall`] from the [`Engine`]'s base [`ResumableCallBase`]. + pub(crate) fn new(call: ResumableCallBase) -> Self { + match call { + ResumableCallBase::Finished(results) => Self::Finished(results), + ResumableCallBase::Resumable(invocation) => { + Self::Resumable(TypedResumableInvocation::new(invocation)) + } + } + } +} + +/// State required to resume a [`TypedFunc`] invocation. +/// +/// [`TypedFunc`]: [`crate::TypedFunc`] +pub struct TypedResumableInvocation { + invocation: ResumableInvocation, + /// The parameter and result typed encoded in Rust type system. + results: PhantomData Results>, +} + +impl TypedResumableInvocation { + /// Creates a [`TypedResumableInvocation`] wrapper for the given [`ResumableInvocation`]. + pub(crate) fn new(invocation: ResumableInvocation) -> Self { + Self { + invocation, + results: PhantomData, + } + } + + /// Resumes the call to the [`TypedFunc`] with the given inputs. + /// + /// Returns a resumable handle to the function invocation upon + /// enountering host errors with which it is possible to handle + /// the error and continue the execution as if no error occured. + /// + /// # Errors + /// + /// - If the function resumption returned a Wasm [`Trap`]. + /// - If the types or the number of values in `inputs` does not match + /// the types and number of result values of the errorneous host function. + /// + /// [`TypedFunc`]: [`crate::TypedFunc`] + pub fn resume( + self, + mut ctx: impl AsContextMut, + inputs: &[Value], + ) -> Result, Error> + where + Results: WasmResults, + { + self.engine + .resolve_func_type(self.host_func().signature(ctx.as_context()), |func_type| { + func_type.match_results(inputs, true) + })?; + self.engine + .clone() + .resume_func( + ctx.as_context_mut(), + self.invocation, + inputs, + >::default(), + ) + .map_err(Into::into) + .map(TypedResumableCall::new) + } +} + +impl Deref for TypedResumableInvocation { + type Target = ResumableInvocation; + + fn deref(&self) -> &Self::Target { + &self.invocation + } +} + +impl fmt::Debug for TypedResumableInvocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TypedResumableInvocation") + .field("invocation", &self.invocation) + .field("results", &self.results) + .finish() } } diff --git a/crates/wasmi/src/func/mod.rs b/crates/wasmi/src/func/mod.rs index de345c52b8..4205c7be91 100644 --- a/crates/wasmi/src/func/mod.rs +++ b/crates/wasmi/src/func/mod.rs @@ -3,6 +3,7 @@ mod error; mod into_func; mod typed_func; +pub(crate) use self::typed_func::CallResultsTuple; pub use self::{ caller::Caller, error::FuncError, @@ -328,7 +329,7 @@ impl Func { mut ctx: impl AsContextMut, inputs: &[Value], outputs: &mut [Value], - ) -> Result, Error> { + ) -> Result { self.verify_and_prepare_inputs_outputs(ctx.as_context(), inputs, outputs)?; // Note: Cloning an [`Engine`] is intentionally a cheap operation. ctx.as_context() @@ -337,6 +338,7 @@ impl Func { .clone() .execute_func_resumable(ctx.as_context_mut(), *self, inputs, outputs) .map_err(Into::into) + .map(ResumableCall::new) } /// Verify that the `inputs` and `outputs` value types match the function signature. diff --git a/crates/wasmi/src/func/typed_func.rs b/crates/wasmi/src/func/typed_func.rs index d8a0f71d48..414038e2e6 100644 --- a/crates/wasmi/src/func/typed_func.rs +++ b/crates/wasmi/src/func/typed_func.rs @@ -4,6 +4,7 @@ use crate::{ AsContext, AsContextMut, Error, + TypedResumableCall, }; use core::{fmt, fmt::Debug, marker::PhantomData}; use wasmi_core::{Trap, UntypedValue}; @@ -78,7 +79,7 @@ where }) } - /// Invokes this Wasm or host function with the specified parameters. + /// Calls this Wasm or host function with the specified parameters. /// /// Returns either the results of the call, or a [`Trap`] if one happened. /// @@ -101,6 +102,40 @@ where >::default(), ) } + + /// Calls this Wasm or host function with the specified parameters. + /// + /// Returns a resumable handle to the function invocation upon + /// enountering host errors with which it is possible to handle + /// the error and continue the execution as if no error occured. + /// + /// # Note + /// + /// This is a non-standard WebAssembly API and might not be available + /// at other WebAssembly engines. Please be aware that depending on this + /// feature might mean a lock-in to `wasmi` for users. + /// + /// # Errors + /// + /// If the function returned a [`Trap`] originating from WebAssembly. + pub fn call_resumable( + &self, + mut ctx: impl AsContextMut, + params: Params, + ) -> Result, Trap> { + // Note: Cloning an [`Engine`] is intentionally a cheap operation. + ctx.as_context() + .store + .engine() + .clone() + .execute_func_resumable( + ctx.as_context_mut(), + self.func, + params, + >::default(), + ) + .map(TypedResumableCall::new) + } } impl CallParams for Params diff --git a/crates/wasmi/src/lib.rs b/crates/wasmi/src/lib.rs index 5dcf34648f..a5388bbaf7 100644 --- a/crates/wasmi/src/lib.rs +++ b/crates/wasmi/src/lib.rs @@ -121,7 +121,15 @@ pub mod errors { } pub use self::{ - engine::{Config, Engine, ResumableCall, ResumableInvocation, StackLimits}, + engine::{ + Config, + Engine, + ResumableCall, + ResumableInvocation, + StackLimits, + TypedResumableCall, + TypedResumableInvocation, + }, error::Error, external::Extern, func::{Caller, Func, IntoFunc, TypedFunc, WasmParams, WasmResults, WasmRet, WasmType}, diff --git a/crates/wasmi/tests/e2e/v1/resumable_call.rs b/crates/wasmi/tests/e2e/v1/resumable_call.rs index 60ac00cd5f..4e7b6df61e 100644 --- a/crates/wasmi/tests/e2e/v1/resumable_call.rs +++ b/crates/wasmi/tests/e2e/v1/resumable_call.rs @@ -1,6 +1,16 @@ //! Test to assert that resumable call feature works as intended. -use wasmi::{Engine, Error, Extern, Func, Linker, Module, ResumableCall, Store}; +use wasmi::{ + Engine, + Error, + Extern, + Func, + Linker, + Module, + ResumableCall, + Store, + TypedResumableCall, +}; use wasmi_core::{Trap, TrapCode, Value, ValueType}; fn test_setup() -> Store<()> { @@ -26,6 +36,17 @@ fn resumable_call_host() { _ => panic!("expected Wasm trap"), }, } + // The same test for `TypedFunc`: + match host_fn + .typed::<(), ()>(&store) + .unwrap() + .call_resumable(&mut store, ()) + { + Ok(_) => panic!("expected an error since the called host function is root"), + Err(trap) => { + assert_eq!(trap.i32_exit_status(), Some(100)); + } + } } #[test] @@ -74,6 +95,8 @@ fn resumable_call() { run_test(wasm_fn, &mut store, false); run_test(wasm_fn, &mut store, true); + run_test_typed(wasm_fn, &mut store, false); + run_test_typed(wasm_fn, &mut store, true); } fn run_test(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { @@ -94,7 +117,7 @@ fn run_test(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { ); invocation } - ResumableCall::Finished(_) => panic!("expected host function trap with exit code 10"), + ResumableCall::Finished => panic!("expected host function trap with exit code 10"), }; let invocation = match invocation .resume(&mut store, &[Value::I32(2)], &mut results[..]) @@ -108,7 +131,7 @@ fn run_test(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { ); invocation } - ResumableCall::Finished(_) => panic!("expected host function trap with exit code 20"), + ResumableCall::Finished => panic!("expected host function trap with exit code 20"), }; let result = invocation.resume(&mut store, &[Value::I32(3)], &mut results[..]); if wasm_trap { @@ -129,9 +152,63 @@ fn run_test(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { Ok(ResumableCall::Resumable(_)) | Err(_) => { panic!("expected resumed function to finish") } - Ok(ResumableCall::Finished(())) => { + Ok(ResumableCall::Finished) => { assert_eq!(results, [Value::I32(4)]); } } } } + +fn run_test_typed(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { + let invocation = match wasm_fn + .typed::(&store) + .unwrap() + .call_resumable(&mut store, wasm_trap as i32) + .unwrap() + { + TypedResumableCall::Resumable(invocation) => { + assert_eq!(invocation.host_error().i32_exit_status(), Some(10)); + assert_eq!( + invocation.host_func().func_type(&store).results(), + &[ValueType::I32] + ); + invocation + } + TypedResumableCall::Finished(_) => panic!("expected host function trap with exit code 10"), + }; + let invocation = match invocation.resume(&mut store, &[Value::I32(2)]).unwrap() { + TypedResumableCall::Resumable(invocation) => { + assert_eq!(invocation.host_error().i32_exit_status(), Some(20)); + assert_eq!( + invocation.host_func().func_type(&store).results(), + &[ValueType::I32] + ); + invocation + } + TypedResumableCall::Finished(_) => panic!("expected host function trap with exit code 20"), + }; + let result = invocation.resume(&mut store, &[Value::I32(3)]); + if wasm_trap { + match result { + Ok(_) => panic!("expected resumed function to trap in Wasm"), + Err(trap) => match trap { + Error::Trap(trap) => { + assert!(matches!( + trap.trap_code(), + Some(TrapCode::UnreachableCodeReached) + )); + } + _ => panic!("expected Wasm trap"), + }, + } + } else { + match result { + Ok(TypedResumableCall::Resumable(_)) | Err(_) => { + panic!("expected resumed function to finish") + } + Ok(TypedResumableCall::Finished(result)) => { + assert_eq!(result, 4); + } + } + } +}