From 11f51bed853e239c1d239f52dba44b47f5732904 Mon Sep 17 00:00:00 2001 From: Robin Freyler Date: Thu, 30 Nov 2023 12:28:57 +0100 Subject: [PATCH] Implement resumable calls via register-machine `wasmi` engine backend (#814) * implement resumable calling via register-machine engine This is not tested properly and may not work, yet. * try to fix fuzzing CI * fix bug for older rust toolchain (bench CI) * make special smoldot tail resume tests pass under regmach * apply rustfmt * remove outdated comment Tails calls work when calling host functions in the new register-machine engine. * add TestData to resumable calls tests Also use Linker::func_wrap. New TestData is unused, yet. * test resumable calls for both engine backends --- .github/workflows/rust.yml | 2 +- crates/wasmi/src/engine/mod.rs | 93 ++++- .../src/engine/regmach/executor/instrs.rs | 15 +- .../engine/regmach/executor/instrs/call.rs | 13 +- .../wasmi/src/engine/regmach/executor/mod.rs | 65 +++- crates/wasmi/src/engine/regmach/mod.rs | 110 +++++- crates/wasmi/src/engine/regmach/trap.rs | 49 +++ crates/wasmi/src/engine/resumable.rs | 117 ++++++- crates/wasmi/tests/e2e/v1/resumable_call.rs | 327 ++++++++++-------- 9 files changed, 610 insertions(+), 181 deletions(-) create mode 100644 crates/wasmi/src/engine/regmach/trap.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fe946f09c6..902af84682 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -207,7 +207,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2023-06-15 # we must tag pinned nightly version: https://substrate.stackexchange.com/a/9069 + toolchain: nightly override: true - name: Set up Cargo cache uses: actions/cache@v3 diff --git a/crates/wasmi/src/engine/mod.rs b/crates/wasmi/src/engine/mod.rs index 8df0babf69..05255dfe0f 100644 --- a/crates/wasmi/src/engine/mod.rs +++ b/crates/wasmi/src/engine/mod.rs @@ -409,6 +409,11 @@ impl Engine { pub(crate) fn recycle_stack(&self, stack: Stack) { self.inner.recycle_stack(stack) } + + /// Recycles the given [`Stack`] for reuse in the [`Engine`]. + pub(crate) fn recycle_stack_2(&self, stack: Stack2) { + self.inner.recycle_stack_2(stack) + } } /// The internal state of the `wasmi` [`Engine`]. @@ -684,7 +689,42 @@ impl EngineInner { results } + /// Executes the given [`Func`] resumably with the given `params` and returns the `results`. + /// + /// Uses the [`StoreContextMut`] for context information about the Wasm [`Store`]. + /// + /// # Errors + /// + /// If the Wasm execution traps or runs out of resources. fn execute_func_resumable( + &self, + ctx: StoreContextMut, + func: &Func, + params: impl CallParams, + results: Results, + ) -> Result::Results>, Trap> + where + Results: CallResults, + { + match self.config().engine_backend() { + EngineBackend::StackMachine => { + self.execute_func_resumable_stackmach(ctx, func, params, results) + } + EngineBackend::RegisterMachine => { + self.execute_func_resumable_regmach(ctx, func, params, results) + } + } + } + + /// Executes the given [`Func`] resumably with the given `params` and returns the `results`. + /// + /// - Uses the `wasmi` stack-machine based engine backend. + /// - Uses the [`StoreContextMut`] for context information about the Wasm [`Store`]. + /// + /// # Errors + /// + /// If the Wasm execution traps or runs out of resources. + fn execute_func_resumable_stackmach( &self, mut ctx: StoreContextMut, func: &Func, @@ -719,12 +759,48 @@ impl EngineInner { *func, host_func, host_trap, + None, stack, ))), } } + /// Resumes the given [`Func`] with the given `params` and returns the `results`. + /// + /// - Uses the [`StoreContextMut`] for context information about the Wasm [`Store`]. + /// + /// # Errors + /// + /// If the Wasm execution traps or runs out of resources. fn resume_func( + &self, + ctx: StoreContextMut, + invocation: ResumableInvocation, + params: impl CallParams, + results: Results, + ) -> Result::Results>, Trap> + where + Results: CallResults, + { + match self.config().engine_backend() { + EngineBackend::StackMachine => { + self.resume_func_stackmach(ctx, invocation, params, results) + } + EngineBackend::RegisterMachine => { + self.resume_func_regmach(ctx, invocation, params, results) + } + } + } + + /// Resumes the given [`Func`] with the given `params` and returns the `results`. + /// + /// - Uses the `wasmi` stack-machine based engine backend. + /// - Uses the [`StoreContextMut`] for context information about the Wasm [`Store`]. + /// + /// # Errors + /// + /// If the Wasm execution traps or runs out of resources. + fn resume_func_stackmach( &self, ctx: StoreContextMut, mut invocation: ResumableInvocation, @@ -736,30 +812,37 @@ impl EngineInner { { let res = self.res.read(); let host_func = invocation.host_func(); - let results = EngineExecutor::new(&res, &mut invocation.stack) - .resume_func(ctx, host_func, params, results); + let mut stack = invocation.take_stack().into_stackmach(); + let results = + EngineExecutor::new(&res, &mut stack).resume_func(ctx, host_func, params, results); match results { Ok(results) => { - self.stacks.lock().recycle(invocation.take_stack()); + self.stacks.lock().recycle(stack); Ok(ResumableCallBase::Finished(results)) } Err(TaggedTrap::Wasm(trap)) => { - self.stacks.lock().recycle(invocation.take_stack()); + self.stacks.lock().recycle(stack); Err(trap) } Err(TaggedTrap::Host { host_func, host_trap, }) => { - invocation.update(host_func, host_trap); + invocation.update(stack, host_func, host_trap); Ok(ResumableCallBase::Resumable(invocation)) } } } + /// Recycles the given [`Stack`] for the stack-machine `wasmi` engine backend. fn recycle_stack(&self, stack: Stack) { self.stacks.lock().recycle(stack); } + + /// Recycles the given [`Stack`] for the register-machine `wasmi` engine backend. + fn recycle_stack_2(&self, stack: Stack2) { + self.stacks.lock().recycle_2(stack) + } } /// Engine resources that are immutable during function execution. diff --git a/crates/wasmi/src/engine/regmach/executor/instrs.rs b/crates/wasmi/src/engine/regmach/executor/instrs.rs index d7a5a4f5de..8368a7aaaf 100644 --- a/crates/wasmi/src/engine/regmach/executor/instrs.rs +++ b/crates/wasmi/src/engine/regmach/executor/instrs.rs @@ -1,3 +1,4 @@ +pub use self::call::CallKind; use self::{call::CallOutcome, return_::ReturnOutcome}; use crate::{ core::{TrapCode, UntypedValue}, @@ -45,8 +46,17 @@ mod unary; macro_rules! forward_call { ($expr:expr) => {{ - if let CallOutcome::Call { results, host_func } = $expr? { - return Ok(WasmOutcome::Call { results, host_func }); + if let CallOutcome::Call { + results, + host_func, + call_kind, + } = $expr? + { + return Ok(WasmOutcome::Call { + results, + host_func, + call_kind, + }); } }}; } @@ -73,6 +83,7 @@ pub enum WasmOutcome { Call { results: RegisterSpan, host_func: Func, + call_kind: CallKind, }, } diff --git a/crates/wasmi/src/engine/regmach/executor/instrs/call.rs b/crates/wasmi/src/engine/regmach/executor/instrs/call.rs index 9359220323..2188d219e1 100644 --- a/crates/wasmi/src/engine/regmach/executor/instrs/call.rs +++ b/crates/wasmi/src/engine/regmach/executor/instrs/call.rs @@ -39,6 +39,7 @@ pub enum CallOutcome { Call { results: RegisterSpan, host_func: Func, + call_kind: CallKind, }, } @@ -320,8 +321,6 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> { Ok(CallOutcome::Continue) } FuncEntity::Host(host_func) => { - // Note: host function calls cannot be implemented as tail calls. - // The Wasm spec is not mandating tail behavior for host calls. let (input_types, output_types) = self .func_types .resolve_func_type(host_func.ty_dedup()) @@ -341,13 +340,19 @@ impl<'ctx, 'engine> Executor<'ctx, 'engine> { let offset = self.value_stack.extend_zeros(max_inout); let offset_sp = unsafe { self.value_stack.stack_ptr_at(offset) }; if matches!(params, CallParams::Some) { - self.ip = self.copy_call_params(offset_sp); + let new_ip = self.copy_call_params(offset_sp); + if matches!(call_kind, CallKind::Nested) { + self.ip = new_ip; + } + } + if matches!(call_kind, CallKind::Nested) { + self.update_instr_ptr_at(1); } - self.update_instr_ptr_at(1); self.cache.reset(); Ok(CallOutcome::Call { results, host_func: *func, + call_kind, }) } } diff --git a/crates/wasmi/src/engine/regmach/executor/mod.rs b/crates/wasmi/src/engine/regmach/executor/mod.rs index 548d7125df..dd922ef6e0 100644 --- a/crates/wasmi/src/engine/regmach/executor/mod.rs +++ b/crates/wasmi/src/engine/regmach/executor/mod.rs @@ -1,6 +1,6 @@ -use self::instrs::{execute_instrs, WasmOutcome}; -use super::stack::CallFrame; +use self::instrs::{execute_instrs, CallKind, WasmOutcome}; pub use super::Stack; +use super::{stack::CallFrame, TaggedTrap}; use crate::{ engine::{ cache::InstanceCache, @@ -10,10 +10,8 @@ use crate::{ }, CallParams, CallResults, - DedupFuncType, EngineResources, FuncParams, - TaggedTrap, }, func::HostFuncEntity, AsContext, @@ -67,10 +65,8 @@ impl<'engine> EngineExecutor<'engine> { Results: CallResults, { self.stack.reset(); - let func_type; match ctx.as_context().store.inner.resolve_func(func) { FuncEntity::Wasm(wasm_func) => { - func_type = *wasm_func.ty_dedup(); // We reserve space on the stack to write the results of the root function execution. self.stack.values.extend_zeros(results.len_results()); let instance = wasm_func.instance(); @@ -92,7 +88,6 @@ impl<'engine> EngineExecutor<'engine> { self.execute_func(ctx.as_context_mut())?; } FuncEntity::Host(host_func) => { - func_type = *host_func.ty_dedup(); // The host function signature is required for properly // adjusting, inspecting and manipulating the value stack. let (input_types, output_types) = self @@ -115,7 +110,44 @@ impl<'engine> EngineExecutor<'engine> { self.dispatch_host_func(ctx.as_context_mut(), host_func, HostFuncCaller::Root)?; } }; - let results = self.write_results_back(results, func_type); + let results = self.write_results_back(results); + Ok(results) + } + + /// Resumes the execution of the given [`Func`] using `params`. + /// + /// Stores the execution result into `results` upon a successful execution. + /// + /// # Errors + /// + /// - If the given `params` do not match the expected parameters of `func`. + /// - If the given `results` do not match the the length of the expected results of `func`. + /// - When encountering a Wasm or host trap during the execution of `func`. + pub fn resume_func( + &mut self, + mut ctx: StoreContextMut, + _host_func: Func, + params: impl CallParams, + caller_results: RegisterSpan, + results: Results, + ) -> Result<::Results, TaggedTrap> + where + Results: CallResults, + { + let caller = self + .stack + .calls + .peek() + .expect("must have caller call frame on stack upon function resumption"); + let mut caller_sp = unsafe { self.stack.values.stack_ptr_at(caller.base_offset()) }; + let call_params = params.call_params(); + let len_params = call_params.len(); + for (result, param) in caller_results.iter(len_params).zip(call_params) { + let cell = unsafe { caller_sp.get_mut(result) }; + *cell = param; + } + self.execute_func(ctx.as_context_mut())?; + let results = self.write_results_back(results); Ok(results) } @@ -143,6 +175,7 @@ impl<'engine> EngineExecutor<'engine> { WasmOutcome::Call { results, ref host_func, + call_kind, } => { let instance = *self .stack @@ -150,7 +183,7 @@ impl<'engine> EngineExecutor<'engine> { .peek() .expect("caller must be on the stack") .instance(); - self.execute_host_func(&mut ctx, results, host_func, &instance)?; + self.execute_host_func(&mut ctx, results, host_func, &instance, call_kind)?; } } } @@ -162,6 +195,7 @@ impl<'engine> EngineExecutor<'engine> { results: RegisterSpan, func: &Func, instance: &Instance, + call_kind: CallKind, ) -> Result<(), TaggedTrap> { let func_entity = match ctx.as_context().store.inner.resolve_func(func) { FuncEntity::Wasm(wasm_func) => { @@ -174,12 +208,15 @@ impl<'engine> EngineExecutor<'engine> { func_entity, HostFuncCaller::wasm(results, instance), ); + if matches!(call_kind, CallKind::Tail) { + self.stack.calls.pop(); + } if self.stack.calls.peek().is_some() { // Case: There is a frame on the call stack. // // This is the default case and we can easily make host function // errors return a resumable call handle. - result.map_err(|trap| TaggedTrap::host(*func, trap))?; + result.map_err(|trap| TaggedTrap::host(*func, trap, results))?; } else { // Case: No frame is on the call stack. (edge case) // @@ -362,15 +399,11 @@ impl<'engine> EngineExecutor<'engine> { /// /// - If the `results` buffer length does not match the remaining amount of stack values. #[inline] - fn write_results_back( - &mut self, - results: Results, - ty: DedupFuncType, - ) -> ::Results + fn write_results_back(&mut self, results: Results) -> ::Results where Results: CallResults, { - let len_results = self.res.func_types.resolve_func_type(&ty).results().len(); + let len_results = results.len_results(); results.call_results(&self.stack.values.as_slice()[..len_results]) } } diff --git a/crates/wasmi/src/engine/regmach/mod.rs b/crates/wasmi/src/engine/regmach/mod.rs index 66db8e9d2a..6e435ef606 100644 --- a/crates/wasmi/src/engine/regmach/mod.rs +++ b/crates/wasmi/src/engine/regmach/mod.rs @@ -3,20 +3,25 @@ pub mod code_map; mod executor; mod stack; mod translator; +mod trap; #[cfg(test)] mod tests; -use self::executor::EngineExecutor; pub use self::{ code_map::CodeMap, stack::Stack, translator::{FuncLocalConstsIter, FuncTranslator, FuncTranslatorAllocations}, }; +use self::{executor::EngineExecutor, trap::TaggedTrap}; +use super::resumable::ResumableCallBase; use crate::{ core::Trap, - engine::{CallParams, CallResults, EngineInner, TaggedTrap}, + engine::{CallParams, CallResults, EngineInner}, + AsContext as _, + AsContextMut as _, Func, + ResumableInvocation, StoreContextMut, }; @@ -50,4 +55,105 @@ impl EngineInner { self.stacks.lock().recycle_2(stack); results } + + /// Executes the given [`Func`] resumably with the given `params` and returns the `results`. + /// + /// - Uses the `wasmi` register-machine based engine backend. + /// - Uses the [`StoreContextMut`] for context information about the Wasm [`Store`]. + /// + /// # Errors + /// + /// If the Wasm execution traps or runs out of resources. + pub(crate) fn execute_func_resumable_regmach( + &self, + mut ctx: StoreContextMut, + func: &Func, + params: impl CallParams, + results: Results, + ) -> Result::Results>, Trap> + where + Results: CallResults, + { + let res = self.res.read(); + let mut stack = self.stacks.lock().reuse_or_new_2(); + let results = EngineExecutor::new(&res, &mut stack).execute_root_func( + ctx.as_context_mut(), + func, + params, + results, + ); + match results { + Ok(results) => { + self.stacks.lock().recycle_2(stack); + Ok(ResumableCallBase::Finished(results)) + } + Err(TaggedTrap::Wasm(trap)) => { + self.stacks.lock().recycle_2(stack); + Err(trap) + } + Err(TaggedTrap::Host { + host_func, + host_trap, + caller_results, + }) => Ok(ResumableCallBase::Resumable(ResumableInvocation::new( + ctx.as_context().store.engine().clone(), + *func, + host_func, + host_trap, + Some(caller_results), + stack, + ))), + } + } + + /// Resumes the given [`Func`] with the given `params` and returns the `results`. + /// + /// - Uses the `wasmi` register-machine based engine backend. + /// - Uses the [`StoreContextMut`] for context information about the Wasm [`Store`]. + /// + /// # Errors + /// + /// If the Wasm execution traps or runs out of resources. + pub(crate) fn resume_func_regmach( + &self, + ctx: StoreContextMut, + mut invocation: ResumableInvocation, + params: impl CallParams, + results: Results, + ) -> Result::Results>, Trap> + where + Results: CallResults, + { + let res = self.res.read(); + let host_func = invocation.host_func(); + let caller_results = invocation + .caller_results() + .expect("register-machine engine required caller results for call resumption"); + let mut stack = invocation.take_stack().into_regmach(); + let results = EngineExecutor::new(&res, &mut stack).resume_func( + ctx, + host_func, + params, + caller_results, + results, + ); + match results { + Ok(results) => { + self.stacks.lock().recycle_2(stack); + Ok(ResumableCallBase::Finished(results)) + } + Err(TaggedTrap::Wasm(trap)) => { + self.stacks.lock().recycle_2(stack); + Err(trap) + } + Err(TaggedTrap::Host { + host_func, + host_trap, + caller_results, + }) => { + invocation.update_2(stack, host_func, host_trap, caller_results); + Ok(ResumableCallBase::Resumable(invocation)) + } + } + } } diff --git a/crates/wasmi/src/engine/regmach/trap.rs b/crates/wasmi/src/engine/regmach/trap.rs new file mode 100644 index 0000000000..70a8c6b569 --- /dev/null +++ b/crates/wasmi/src/engine/regmach/trap.rs @@ -0,0 +1,49 @@ +use super::bytecode::RegisterSpan; +use crate::{ + core::{Trap, TrapCode}, + Func, +}; + +/// Either a Wasm trap or a host trap with its originating host [`Func`]. +#[derive(Debug)] +pub enum TaggedTrap { + /// The trap is originating from Wasm. + Wasm(Trap), + /// The trap is originating from a host function. + Host { + host_func: Func, + host_trap: Trap, + caller_results: RegisterSpan, + }, +} + +impl TaggedTrap { + /// Creates a [`TaggedTrap`] from a host error. + pub fn host(host_func: Func, host_trap: Trap, caller_results: RegisterSpan) -> Self { + Self::Host { + host_func, + host_trap, + caller_results, + } + } + + /// Returns the [`Trap`] of the [`TaggedTrap`]. + pub fn into_trap(self) -> Trap { + match self { + TaggedTrap::Wasm(trap) => trap, + TaggedTrap::Host { host_trap, .. } => host_trap, + } + } +} + +impl From for TaggedTrap { + fn from(trap: Trap) -> Self { + Self::Wasm(trap) + } +} + +impl From for TaggedTrap { + fn from(trap_code: TrapCode) -> Self { + Self::Wasm(trap_code.into()) + } +} diff --git a/crates/wasmi/src/engine/resumable.rs b/crates/wasmi/src/engine/resumable.rs index b394e17467..45a86e0969 100644 --- a/crates/wasmi/src/engine/resumable.rs +++ b/crates/wasmi/src/engine/resumable.rs @@ -1,6 +1,6 @@ -use super::Func; +use super::{regmach::bytecode::RegisterSpan, Func}; use crate::{ - engine::Stack, + engine::{Stack, Stack2}, func::CallResultsTuple, AsContextMut, Engine, @@ -84,6 +84,12 @@ pub struct ResumableInvocation { /// actual host error. This is therefore guaranteed to never /// be a Wasm trap. host_error: Trap, + /// The registers where to put provided host function results upon resumption. + /// + /// # Note + /// + /// This is only needed for the register-machine `wasmi` engine backend. + caller_results: Option, /// The value and call stack in use by the [`ResumableInvocation`]. /// /// # Note @@ -93,7 +99,58 @@ pub struct ResumableInvocation { /// - This stack is borrowed from the engine and needs to be given /// back to the engine when the [`ResumableInvocation`] goes out /// of scope. - pub(super) stack: Stack, + pub(super) stack: ResumeStack, +} + +/// The stack of a [`ResumableInvocation`]. +#[derive(Debug)] +pub enum ResumeStack { + /// The stack of the stack-machine `wasmi` engine backend. + StackMach(Stack), + /// The stack of the register-machine `wasmi` engine backend. + RegMach(Stack2), +} + +impl From for ResumeStack { + fn from(stack: Stack) -> Self { + Self::StackMach(stack) + } +} + +impl From for ResumeStack { + fn from(stack: Stack2) -> Self { + Self::RegMach(stack) + } +} + +impl ResumeStack { + /// Returns the stack-machine [`Stack`] if possible. + /// + /// # Panics + /// + /// If this hold a register-machine stack. + pub fn into_stackmach(self) -> Stack { + match self { + Self::StackMach(stack) => stack, + Self::RegMach(_) => { + panic!("found register-machine stack but expected stack-machine stack") + } + } + } + + /// Returns the register-machine [`Stack`] if possible. + /// + /// # Panics + /// + /// If this hold a stack-machine stack. + pub fn into_regmach(self) -> Stack2 { + match self { + Self::RegMach(stack) => stack, + Self::StackMach(_) => { + panic!("found stack-machine stack but expected register-machine stack") + } + } + } } impl ResumableInvocation { @@ -103,33 +160,64 @@ impl ResumableInvocation { func: Func, host_func: Func, host_error: Trap, - stack: Stack, + caller_results: Option, + stack: impl Into, ) -> Self { Self { engine, func, host_func, host_error, - stack, + caller_results, + stack: stack.into(), } } /// Replaces the internal stack with an empty one that has no heap allocations. - pub(super) fn take_stack(&mut self) -> Stack { - replace(&mut self.stack, Stack::empty()) + pub(super) fn take_stack(&mut self) -> ResumeStack { + let empty_stack = match &self.stack { + ResumeStack::StackMach(_) => Stack::empty().into(), + ResumeStack::RegMach(_) => Stack2::empty().into(), + }; + replace(&mut self.stack, empty_stack) } /// Updates the [`ResumableInvocation`] with the new `host_func` and a `host_error`. - pub(super) fn update(&mut self, host_func: Func, host_error: Trap) { + /// + /// # Note + /// + /// This should only be called from the stack-machine `wasmi` engine backend. + pub(super) fn update(&mut self, stack: Stack, host_func: Func, host_error: Trap) { + self.stack = stack.into(); self.host_func = host_func; self.host_error = host_error; } + + /// Updates the [`ResumableInvocation`] with the new `host_func`, `host_error` and `caller_results`. + /// + /// # Note + /// + /// This should only be called from the register-machine `wasmi` engine backend. + pub(super) fn update_2( + &mut self, + stack: Stack2, + host_func: Func, + host_error: Trap, + caller_results: RegisterSpan, + ) { + self.stack = stack.into(); + self.host_func = host_func; + self.host_error = host_error; + self.caller_results = Some(caller_results); + } } impl Drop for ResumableInvocation { fn drop(&mut self) { - let stack = self.take_stack(); - self.engine.recycle_stack(stack); + match self.take_stack() { + ResumeStack::StackMach(stack) => self.engine.recycle_stack(stack), + ResumeStack::RegMach(stack) => self.engine.recycle_stack_2(stack), + } } } @@ -155,6 +243,15 @@ impl ResumableInvocation { &self.host_error } + /// Returns the caller results [`RegisterSpan`]. + /// + /// # Note + /// + /// This is `Some` only for [`ResumableInvocation`] originating from the register-machine `wasmi` engine. + pub(crate) fn caller_results(&self) -> Option { + self.caller_results + } + /// Resumes the call to the [`Func`] with the given inputs. /// /// The result is written back into the `outputs` buffer upon success. diff --git a/crates/wasmi/tests/e2e/v1/resumable_call.rs b/crates/wasmi/tests/e2e/v1/resumable_call.rs index d2d9d73824..8e088e1528 100644 --- a/crates/wasmi/tests/e2e/v1/resumable_call.rs +++ b/crates/wasmi/tests/e2e/v1/resumable_call.rs @@ -3,12 +3,13 @@ use core::slice; use wasmi::{ + Caller, Config, Engine, + EngineBackend, Error, Extern, Func, - FuncType, Linker, Module, ResumableCall, @@ -21,25 +22,41 @@ use wasmi::{ }; use wasmi_core::{Trap, TrapCode, ValueType}; -fn test_setup() -> (Store<()>, Linker<()>) { +fn test_setup(remaining: u32, backend: EngineBackend) -> (Store, Linker) { let mut config = Config::default(); config.wasm_tail_call(true); + config.set_engine_backend(backend); let engine = Engine::new(&config); - let store = Store::new(&engine, ()); - let linker = >::new(&engine); + let store = Store::new( + &engine, + TestData { + _remaining: remaining, + }, + ); + let linker = >::new(&engine); (store, linker) } -fn resumable_call_smoldot_common(wasm: &str) -> (Store<()>, TypedFunc<(), i32>) { - let (mut store, mut linker) = test_setup(); +#[derive(Debug)] +pub struct TestData { + /// How many host calls must be made before returning a non error value. + _remaining: u32, +} + +fn resumable_call_smoldot_common( + backend: EngineBackend, + wasm: &str, +) -> (Store, TypedFunc<(), i32>) { + let (mut store, mut linker) = test_setup(0, backend); // The important part about this test is that this // host function has more results than parameters. - let host_fn = Func::new( - &mut store, - FuncType::new([], [ValueType::I32]), - |_caller, _inputs, _output| Err(Trap::i32_exit(100)), - ); - linker.define("env", "host_fn", host_fn).unwrap(); + linker + .func_wrap( + "env", + "host_fn", + |mut _caller: Caller<'_, TestData>| -> Result { Err(Trap::i32_exit(100)) }, + ) + .unwrap(); // The Wasm defines a single function that calls the // host function, returns 10 if the output is 0 and // returns 20 otherwise. @@ -73,166 +90,194 @@ impl UnwrapResumable for Result, Trap> { #[test] fn resumable_call_smoldot_01() { - let (mut store, wasm_fn) = resumable_call_smoldot_common( - r#" - (module - (import "env" "host_fn" (func $host_fn (result i32))) - (func (export "test") (result i32) - (call $host_fn) + fn test_with(backend: EngineBackend) { + let (mut store, wasm_fn) = resumable_call_smoldot_common( + backend, + r#" + (module + (import "env" "host_fn" (func $host_fn (result i32))) + (func (export "test") (result i32) + (call $host_fn) + ) ) - ) - "#, - ); - let invocation = wasm_fn.call_resumable(&mut store, ()).unwrap_resumable(); - match invocation.resume(&mut store, &[Value::I32(42)]).unwrap() { - TypedResumableCall::Finished(result) => assert_eq!(result, 42), - TypedResumableCall::Resumable(_) => panic!("expected TypeResumableCall::Finished"), + "#, + ); + let invocation = wasm_fn.call_resumable(&mut store, ()).unwrap_resumable(); + match invocation.resume(&mut store, &[Value::I32(42)]).unwrap() { + TypedResumableCall::Finished(result) => assert_eq!(result, 42), + TypedResumableCall::Resumable(_) => panic!("expected TypeResumableCall::Finished"), + } } + test_with(EngineBackend::StackMachine); + test_with(EngineBackend::RegisterMachine); } #[test] fn resumable_call_smoldot_tail_01() { - let (mut store, wasm_fn) = resumable_call_smoldot_common( - r#" - (module - (import "env" "host_fn" (func $host_fn (result i32))) - (func (export "test") (result i32) - (return_call $host_fn) + fn test_with(backend: EngineBackend) { + let (mut store, wasm_fn) = resumable_call_smoldot_common( + backend, + r#" + (module + (import "env" "host_fn" (func $host_fn (result i32))) + (func (export "test") (result i32) + (return_call $host_fn) + ) ) - ) - "#, - ); - assert_eq!( - wasm_fn - .call_resumable(&mut store, ()) - .unwrap_err() - .i32_exit_status(), - Some(100), - ); + "#, + ); + assert_eq!( + wasm_fn + .call_resumable(&mut store, ()) + .unwrap_err() + .i32_exit_status(), + Some(100), + ); + } + test_with(EngineBackend::StackMachine); + test_with(EngineBackend::RegisterMachine); } #[test] fn resumable_call_smoldot_tail_02() { - let (mut store, wasm_fn) = resumable_call_smoldot_common( - r#" - (module - (import "env" "host_fn" (func $host (result i32))) - (func $wasm (result i32) - (return_call $host) - ) - (func (export "test") (result i32) - (call $wasm) + fn test_with(backend: EngineBackend) { + let (mut store, wasm_fn) = resumable_call_smoldot_common( + backend, + r#" + (module + (import "env" "host_fn" (func $host (result i32))) + (func $wasm (result i32) + (return_call $host) + ) + (func (export "test") (result i32) + (call $wasm) + ) ) - ) - "#, - ); - let invocation = wasm_fn.call_resumable(&mut store, ()).unwrap_resumable(); - match invocation.resume(&mut store, &[Value::I32(42)]).unwrap() { - TypedResumableCall::Finished(result) => assert_eq!(result, 42), - TypedResumableCall::Resumable(_) => panic!("expected TypeResumableCall::Finished"), + "#, + ); + let invocation = wasm_fn.call_resumable(&mut store, ()).unwrap_resumable(); + match invocation.resume(&mut store, &[Value::I32(42)]).unwrap() { + TypedResumableCall::Finished(result) => assert_eq!(result, 42), + TypedResumableCall::Resumable(_) => panic!("expected TypeResumableCall::Finished"), + } } + test_with(EngineBackend::StackMachine); + test_with(EngineBackend::RegisterMachine); } #[test] fn resumable_call_smoldot_02() { - let (mut store, wasm_fn) = resumable_call_smoldot_common( - r#" - (module - (import "env" "host_fn" (func $host_fn (result i32))) - (func (export "test") (result i32) - (if (result i32) (i32.ne (call $host_fn) (i32.const 0)) - (then - (i32.const 11) ;; EXPECTED - ) - (else - (i32.const 22) ;; FAILURE + fn test_with(backend: EngineBackend) { + let (mut store, wasm_fn) = resumable_call_smoldot_common( + backend, + r#" + (module + (import "env" "host_fn" (func $host_fn (result i32))) + (func (export "test") (result i32) + (if (result i32) (i32.ne (call $host_fn) (i32.const 0)) + (then + (i32.const 11) ;; EXPECTED + ) + (else + (i32.const 22) ;; FAILURE + ) ) ) ) - ) - "#, - ); - let invocation = wasm_fn.call_resumable(&mut store, ()).unwrap_resumable(); - match invocation.resume(&mut store, &[Value::I32(42)]).unwrap() { - TypedResumableCall::Finished(result) => assert_eq!(result, 11), - TypedResumableCall::Resumable(_) => panic!("expected TypeResumableCall::Finished"), + "#, + ); + let invocation = wasm_fn.call_resumable(&mut store, ()).unwrap_resumable(); + match invocation.resume(&mut store, &[Value::I32(42)]).unwrap() { + TypedResumableCall::Finished(result) => assert_eq!(result, 11), + TypedResumableCall::Resumable(_) => panic!("expected TypeResumableCall::Finished"), + } } + test_with(EngineBackend::StackMachine); + test_with(EngineBackend::RegisterMachine); } #[test] fn resumable_call_host() { - let (mut store, _linker) = test_setup(); - let host_fn = Func::wrap(&mut store, || -> Result<(), Trap> { - Err(Trap::i32_exit(100)) - }); - // Even though the called host function traps we expect a normal error - // since the host function is the root function of the call and therefore - // it would not make sense to resume it. - let error = host_fn - .call_resumable(&mut store, &[], &mut []) - .unwrap_err(); - match error { - Error::Trap(trap) => { - assert_eq!(trap.i32_exit_status(), Some(100)); + fn test_with(backend: EngineBackend) { + let (mut store, _linker) = test_setup(0, backend); + let host_fn = Func::wrap(&mut store, || -> Result<(), Trap> { + Err(Trap::i32_exit(100)) + }); + // Even though the called host function traps we expect a normal error + // since the host function is the root function of the call and therefore + // it would not make sense to resume it. + let error = host_fn + .call_resumable(&mut store, &[], &mut []) + .unwrap_err(); + match error { + Error::Trap(trap) => { + assert_eq!(trap.i32_exit_status(), Some(100)); + } + _ => panic!("expected Wasm trap"), } - _ => panic!("expected Wasm trap"), + // The same test for `TypedFunc`: + let trap = host_fn + .typed::<(), ()>(&store) + .unwrap() + .call_resumable(&mut store, ()) + .unwrap_err(); + assert_eq!(trap.i32_exit_status(), Some(100)); } - // The same test for `TypedFunc`: - let trap = host_fn - .typed::<(), ()>(&store) - .unwrap() - .call_resumable(&mut store, ()) - .unwrap_err(); - assert_eq!(trap.i32_exit_status(), Some(100)); + test_with(EngineBackend::StackMachine); + test_with(EngineBackend::RegisterMachine); } #[test] fn resumable_call() { - let (mut store, mut linker) = test_setup(); - let host_fn = Func::wrap(&mut store, |input: i32| -> Result { - match input { - 1 => Err(Trap::i32_exit(10)), - 2 => Err(Trap::i32_exit(20)), - n => Ok(n + 1), - } - }); - linker.define("env", "host_fn", host_fn).unwrap(); - let wasm = wat::parse_str( - r#" - (module - (import "env" "host_fn" (func $host_fn (param i32) (result i32))) - (func (export "wasm_fn") (param $wasm_trap i32) (result i32) - (local $i i32) - (local.set $i (i32.const 0)) - (local.set $i (call $host_fn (local.get $i))) ;; Ok - (local.set $i (call $host_fn (local.get $i))) ;; Trap::i32_exit(1) - (local.set $i (call $host_fn (local.get $i))) ;; Trap::i32_exit(2) - (local.set $i (call $host_fn (local.get $i))) ;; Ok - (if (i32.eq (local.get $wasm_trap) (i32.const 1)) - (then unreachable) ;; trap in Wasm if $wasm_trap == 1 + fn test_with(backend: EngineBackend) { + let (mut store, mut linker) = test_setup(0, backend); + let host_fn = Func::wrap(&mut store, |input: i32| -> Result { + match input { + 1 => Err(Trap::i32_exit(10)), + 2 => Err(Trap::i32_exit(20)), + n => Ok(n + 1), + } + }); + linker.define("env", "host_fn", host_fn).unwrap(); + let wasm = wat::parse_str( + r#" + (module + (import "env" "host_fn" (func $host_fn (param i32) (result i32))) + (func (export "wasm_fn") (param $wasm_trap i32) (result i32) + (local $i i32) + (local.set $i (i32.const 0)) + (local.set $i (call $host_fn (local.get $i))) ;; Ok + (local.set $i (call $host_fn (local.get $i))) ;; Trap::i32_exit(1) + (local.set $i (call $host_fn (local.get $i))) ;; Trap::i32_exit(2) + (local.set $i (call $host_fn (local.get $i))) ;; Ok + (if (i32.eq (local.get $wasm_trap) (i32.const 1)) + (then unreachable) ;; trap in Wasm if $wasm_trap == 1 + ) + (local.get $i) ;; return i == 4 ) - (local.get $i) ;; return i == 4 ) + "#, ) - "#, - ) - .unwrap(); - - let module = Module::new(store.engine(), &mut &wasm[..]).unwrap(); - let instance = linker - .instantiate(&mut store, &module) - .unwrap() - .start(&mut store) - .unwrap(); - let wasm_fn = instance - .get_export(&store, "wasm_fn") - .and_then(Extern::into_func) .unwrap(); - 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); + let module = Module::new(store.engine(), &mut &wasm[..]).unwrap(); + let instance = linker + .instantiate(&mut store, &module) + .unwrap() + .start(&mut store) + .unwrap(); + let wasm_fn = instance + .get_export(&store, "wasm_fn") + .and_then(Extern::into_func) + .unwrap(); + + 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); + } + test_with(EngineBackend::StackMachine); + test_with(EngineBackend::RegisterMachine); } trait AssertResumable { @@ -241,7 +286,7 @@ trait AssertResumable { fn assert_resumable( self, - store: &Store<()>, + store: &Store, exit_status: i32, host_results: &[ValueType], ) -> Self::Invocation; @@ -254,7 +299,7 @@ impl AssertResumable for ResumableCall { fn assert_resumable( self, - store: &Store<()>, + store: &Store, exit_status: i32, host_results: &[ValueType], ) -> Self::Invocation { @@ -276,7 +321,7 @@ impl AssertResumable for ResumableCall { } } -fn run_test(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { +fn run_test(wasm_fn: Func, mut store: &mut Store, wasm_trap: bool) { let mut results = Value::I32(0); let invocation = wasm_fn .call_resumable( @@ -313,7 +358,7 @@ impl AssertResumable for TypedResumableCall { fn assert_resumable( self, - store: &Store<()>, + store: &Store, exit_status: i32, host_results: &[ValueType], ) -> Self::Invocation { @@ -335,7 +380,7 @@ impl AssertResumable for TypedResumableCall { } } -fn run_test_typed(wasm_fn: Func, mut store: &mut Store<()>, wasm_trap: bool) { +fn run_test_typed(wasm_fn: Func, mut store: &mut Store, wasm_trap: bool) { let invocation = wasm_fn .typed::(&store) .unwrap()