From ca6c66af87106b097a599e344d5c0cb538250abb Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 3 Apr 2026 00:36:57 +0200 Subject: feat: add resumable calls / fuel tracking Signed-off-by: Henry --- crates/tinywasm/Cargo.toml | 4 + crates/tinywasm/benches/tinywasm.rs | 10 +- crates/tinywasm/benches/tinywasm_modes.rs | 102 ++++++++++++ crates/tinywasm/src/engine.rs | 25 +++ crates/tinywasm/src/func.rs | 236 ++++++++++++++++++++++++---- crates/tinywasm/src/imports.rs | 15 ++ crates/tinywasm/src/interpreter/executor.rs | 93 +++++++++-- crates/tinywasm/src/interpreter/mod.rs | 21 ++- crates/tinywasm/src/interpreter/value128.rs | 2 +- crates/tinywasm/src/lib.rs | 2 +- crates/tinywasm/src/store/mod.rs | 10 +- crates/tinywasm/tests/resume_execution.rs | 116 ++++++++++++++ examples/resumable.rs | 46 ++++++ examples/rust/Cargo.toml | 1 + examples/rust/build.sh | 15 +- examples/wasm-rust.rs | 14 +- 16 files changed, 651 insertions(+), 61 deletions(-) create mode 100644 crates/tinywasm/benches/tinywasm_modes.rs create mode 100644 crates/tinywasm/tests/resume_execution.rs create mode 100644 examples/resumable.rs diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index 18f0365..f0f8ab0 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -116,3 +116,7 @@ harness=false [[bench]] name="tinywasm" harness=false + +[[bench]] +name="tinywasm_modes" +harness=false diff --git a/crates/tinywasm/benches/tinywasm.rs b/crates/tinywasm/benches/tinywasm.rs index 82a3878..f30ad5d 100644 --- a/crates/tinywasm/benches/tinywasm.rs +++ b/crates/tinywasm/benches/tinywasm.rs @@ -34,11 +34,13 @@ fn tinywasm_run(module: TinyWasmModule) -> Result<()> { fn criterion_benchmark(c: &mut Criterion) { let module = tinywasm_parse().expect("tinywasm_parse"); let _twasm = tinywasm_to_twasm(&module).expect("tinywasm_to_twasm"); + let mut group = c.benchmark_group("tinywasm"); + group.measurement_time(std::time::Duration::from_secs(10)); - // c.bench_function("tinywasm_parse", |b| b.iter(tinywasm_parse)); - // c.bench_function("tinywasm_to_twasm", |b| b.iter(|| tinywasm_to_twasm(&module))); - // c.bench_function("tinywasm_from_twasm", |b| b.iter(|| tinywasm_from_twasm(&twasm))); - c.bench_function("tinywasm", |b| b.iter(|| tinywasm_run(module.clone()))); + // group.bench_function("tinywasm_parse", |b| b.iter(tinywasm_parse)); + // group.bench_function("tinywasm_to_twasm", |b| b.iter(|| tinywasm_to_twasm(&module))); + // group.bench_function("tinywasm_from_twasm", |b| b.iter(|| tinywasm_from_twasm(&twasm))); + group.bench_function("tinywasm", |b| b.iter(|| tinywasm_run(module.clone()))); } criterion_group!(benches, criterion_benchmark); diff --git a/crates/tinywasm/benches/tinywasm_modes.rs b/crates/tinywasm/benches/tinywasm_modes.rs new file mode 100644 index 0000000..bf6cbfc --- /dev/null +++ b/crates/tinywasm/benches/tinywasm_modes.rs @@ -0,0 +1,102 @@ +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use eyre::Result; +use tinywasm::engine::{Config, FuelPolicy}; +use tinywasm::types::TinyWasmModule; +use tinywasm::{Engine, ExecProgress, Extern, FuncContext, FuncHandleTyped, Imports, ModuleInstance, Store}; + +const WASM: &[u8] = include_bytes!("../../../examples/rust/out/tinywasm.wasm"); +const FUEL_PER_ROUND: u32 = 512; +const TIME_BUDGET_PER_ROUND: core::time::Duration = core::time::Duration::from_micros(50); +const BENCH_MEASUREMENT_TIME: core::time::Duration = core::time::Duration::from_secs(10); + +fn tinywasm_parse() -> Result { + let parser = tinywasm_parser::Parser::new(); + Ok(parser.parse_module_bytes(WASM)?) +} + +fn setup_typed_func(module: TinyWasmModule, engine: Option) -> Result<(Store, FuncHandleTyped<(), ()>)> { + let mut store = match engine { + Some(engine) => Store::new(engine), + None => Store::default(), + }; + + let mut imports = Imports::default(); + imports.define("env", "printi32", Extern::typed_func(|_: FuncContext<'_>, _: i32| Ok(())))?; + + let instance = ModuleInstance::instantiate(&mut store, module.into(), Some(imports))?; + let func = instance.exported_func::<(), ()>(&store, "hello")?; + Ok((store, func)) +} + +fn run_call(store: &mut Store, func: &FuncHandleTyped<(), ()>) -> Result<()> { + func.call(store, ())?; + Ok(()) +} + +fn run_resume_with_fuel(store: &mut Store, func: &FuncHandleTyped<(), ()>) -> Result<()> { + let mut execution = func.call_resumable(store, ())?; + loop { + match execution.resume_with_fuel(FUEL_PER_ROUND)? { + ExecProgress::Completed(_) => return Ok(()), + ExecProgress::Suspended => {} + } + } +} + +fn run_resume_with_time_budget(store: &mut Store, func: &FuncHandleTyped<(), ()>) -> Result<()> { + let mut execution = func.call_resumable(store, ())?; + loop { + match execution.resume_with_time_budget(TIME_BUDGET_PER_ROUND)? { + ExecProgress::Completed(_) => return Ok(()), + ExecProgress::Suspended => {} + } + } +} + +fn criterion_benchmark(c: &mut Criterion) { + let module = tinywasm_parse().expect("tinywasm_parse"); + let mut group = c.benchmark_group("tinywasm_modes"); + group.measurement_time(BENCH_MEASUREMENT_TIME); + + group.bench_function("call", |b| { + b.iter_batched_ref( + || setup_typed_func(module.clone(), None).expect("setup call"), + |(store, func)| run_call(store, func).expect("run call"), + BatchSize::LargeInput, + ) + }); + + let per_instruction_engine = Engine::new(Config::new().fuel_policy(FuelPolicy::PerInstruction)); + group.bench_function("resume_fuel_per_instruction", |b| { + b.iter_batched_ref( + || { + setup_typed_func(module.clone(), Some(per_instruction_engine.clone())) + .expect("setup fuel per-instruction") + }, + |(store, func)| run_resume_with_fuel(store, func).expect("run fuel per-instruction"), + BatchSize::LargeInput, + ) + }); + + let weighted_engine = Engine::new(Config::new().fuel_policy(FuelPolicy::Weighted)); + group.bench_function("resume_fuel_weighted", |b| { + b.iter_batched_ref( + || setup_typed_func(module.clone(), Some(weighted_engine.clone())).expect("setup fuel weighted"), + |(store, func)| run_resume_with_fuel(store, func).expect("run fuel weighted"), + BatchSize::LargeInput, + ) + }); + + group.bench_function("resume_time_budget", |b| { + b.iter_batched_ref( + || setup_typed_func(module.clone(), None).expect("setup time budget"), + |(store, func)| run_resume_with_time_budget(store, func).expect("run time budget"), + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/crates/tinywasm/src/engine.rs b/crates/tinywasm/src/engine.rs index f03f749..e1ea044 100644 --- a/crates/tinywasm/src/engine.rs +++ b/crates/tinywasm/src/engine.rs @@ -39,6 +39,22 @@ pub(crate) struct EngineInner { // pub(crate) allocator: Box, } +/// Fuel accounting policy for budgeted execution. +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum FuelPolicy { + /// Charge one fuel unit per retired instruction. + PerInstruction, + /// Charge one fuel unit per instruction plus predefined extra cost for specific operations. + Weighted, +} + +impl Default for FuelPolicy { + fn default() -> Self { + Self::PerInstruction + } +} + /// Default initial size for the 32-bit value stack (i32, f32 values). pub const DEFAULT_VALUE_STACK_32_SIZE: usize = 64 * 1024; // 64k slots @@ -68,6 +84,8 @@ pub struct Config { pub stack_ref_size: usize, /// Initial size of the call stack. pub call_stack_size: usize, + /// Fuel accounting policy used by budgeted execution. + pub fuel_policy: FuelPolicy, } impl Config { @@ -75,6 +93,12 @@ impl Config { pub fn new() -> Self { Self::default() } + + /// Set the fuel accounting policy for budgeted execution. + pub fn fuel_policy(mut self, fuel_policy: FuelPolicy) -> Self { + self.fuel_policy = fuel_policy; + self + } } impl Default for Config { @@ -85,6 +109,7 @@ impl Default for Config { stack_128_size: DEFAULT_VALUE_STACK_128_SIZE, stack_ref_size: DEFAULT_VALUE_STACK_REF_SIZE, call_stack_size: DEFAULT_CALL_STACK_SIZE, + fuel_policy: FuelPolicy::default(), } } } diff --git a/crates/tinywasm/src/func.rs b/crates/tinywasm/src/func.rs index 69d6dd2..74610f5 100644 --- a/crates/tinywasm/src/func.rs +++ b/crates/tinywasm/src/func.rs @@ -4,6 +4,20 @@ use crate::{Function, unlikely}; use alloc::{boxed::Box, format, string::ToString, vec, vec::Vec}; use tinywasm_types::{ExternRef, FuncRef, FuncType, ModuleInstanceAddr, ValType, WasmValue}; +#[derive(Debug, Clone, PartialEq, Eq)] +/// Progress for fuel-limited function execution. +pub enum ExecProgress { + /// Execution completed and produced a result. + Completed(T), + /// Execution suspended after exhausting fuel or time budget. + Suspended, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct ExecutionState { + pub(crate) callframe: CallFrame, +} + #[derive(Debug)] /// A function handle pub struct FuncHandle { @@ -12,31 +26,33 @@ pub struct FuncHandle { pub(crate) ty: FuncType, } +#[derive(Debug)] +/// Resumable execution for an untyped function call. +pub struct FuncExecution<'store> { + store: &'store mut Store, + state: FuncExecutionState, +} + +#[derive(Debug)] +enum FuncExecutionState { + Running { exec_state: ExecutionState, root_func_addr: u32 }, + Completed { result: Option> }, +} + +#[derive(Debug)] +/// Resumable execution for a typed function call. +pub struct FuncExecutionTyped<'store, R> { + execution: FuncExecution<'store>, + marker: core::marker::PhantomData, +} + impl FuncHandle { /// Call a function (Invocation) /// /// See #[inline] pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result> { - // Comments are ordered by the steps in the spec - // In this implementation, some steps are combined and ordered differently for performance reasons - - // 3. Let func_ty be the function type - let func_ty = &self.ty; - - // 4. If the length of the provided argument values is different from the number of expected arguments, then fail - if unlikely(func_ty.params.len() != params.len()) { - return Err(Error::Other(format!( - "param count mismatch: expected {}, got {}", - func_ty.params.len(), - params.len() - ))); - } - - // 5. For each value type and the corresponding value, check if types match - if !(func_ty.params.iter().zip(params).all(|(ty, param)| ty == ¶m.val_type())) { - return Err(Error::Other("Type mismatch".into())); - } + validate_call_params(&self.ty, params)?; let func_inst = store.state.get_func(self.addr); let wasm_func = match &func_inst.func { @@ -46,29 +62,153 @@ impl FuncHandle { Function::Wasm(wasm_func) => wasm_func.clone(), }; - // 6. Let f be the dummy frame - // 7. Push the frame f to the call stack - // & 8. Push the values to the stack + // Reset stack, push args, allocate locals, create entry frame. store.stack.clear(); store.stack.values.extend_from_wasmvalues(params)?; let (locals_base, _stack_base, stack_offset) = store.stack.values.enter_locals(wasm_func.params, wasm_func.locals)?; let callframe = CallFrame::new(self.addr, func_inst.owner, locals_base, stack_offset); - // 9. Invoke the function instance + // Execute until completion and then collect result values from the stack. InterpreterRuntime::exec(store, callframe)?; - // Once the function returns: - // 1. Assert: m values are on the top of the stack (Ensured by validation) - debug_assert!(store.stack.values.len() >= func_ty.results.len()); + collect_call_results(store, &self.ty) + } + + /// Call a function and return a resumable execution handle. + /// + /// The returned handle keeps a mutable borrow of the [`Store`] until it + /// completes. Use [`FuncExecution::resume_with_fuel`] (or + /// [`FuncExecution::resume_with_time_budget`] with `std`) to continue. + pub fn call_resumable<'store>( + &self, + store: &'store mut Store, + params: &[WasmValue], + ) -> Result> { + validate_call_params(&self.ty, params)?; + + let func_inst = store.state.get_func(self.addr); + let func_inst_owner = func_inst.owner; + let func = func_inst.func.clone(); + + match func { + Function::Host(host_func) => { + let result = host_func.call(FuncContext { store, module_addr: self.module_addr }, params)?; + Ok(FuncExecution { store, state: FuncExecutionState::Completed { result: Some(result) } }) + } + Function::Wasm(wasm_func) => { + store.stack.clear(); + store.stack.values.extend_from_wasmvalues(params)?; + let (locals_base, _stack_base, stack_offset) = + store.stack.values.enter_locals(wasm_func.params, wasm_func.locals)?; + let callframe = CallFrame::new(self.addr, func_inst_owner, locals_base, stack_offset); + + Ok(FuncExecution { + store, + state: FuncExecutionState::Running { + exec_state: ExecutionState { callframe }, + root_func_addr: self.addr, + }, + }) + } + } + } +} + +impl<'store> FuncExecution<'store> { + /// Resume execution with up to `fuel` units of fuel. + /// + /// Fuel is accounted in chunks, so execution may overshoot the requested + /// fuel before returning [`ExecProgress::Suspended`]. + /// + /// Returns [`ExecProgress::Suspended`] when fuel is exhausted, or + /// [`ExecProgress::Completed`] with the final values once the invocation + /// returns. + pub fn resume_with_fuel(&mut self, fuel: u32) -> Result>> { + let FuncExecutionState::Running { exec_state, root_func_addr } = &mut self.state else { + let FuncExecutionState::Completed { result } = &mut self.state else { + unreachable!("invalid function execution state") + }; + return result + .take() + .map(ExecProgress::Completed) + .ok_or_else(|| Error::Other("execution already completed".to_string())); + }; + + match InterpreterRuntime::exec_with_fuel(self.store, exec_state.callframe, fuel)? { + crate::interpreter::ExecState::Completed => { + let result_ty = self.store.state.get_func(*root_func_addr).func.ty().clone(); + let result = collect_call_results(self.store, &result_ty)?; + self.state = FuncExecutionState::Completed { result: None }; + Ok(ExecProgress::Completed(result)) + } + crate::interpreter::ExecState::Suspended(callframe) => { + exec_state.callframe = callframe; + Ok(ExecProgress::Suspended) + } + } + } + + #[cfg(feature = "std")] + /// Resume execution for at most `time_budget` wall-clock time. + /// + /// Time is checked periodically, so execution may overshoot the requested + /// time budget before returning [`ExecProgress::Suspended`]. + /// + /// Returns [`ExecProgress::Suspended`] when the budget is exhausted, or + /// [`ExecProgress::Completed`] with the final values once the invocation + /// returns. + pub fn resume_with_time_budget( + &mut self, + time_budget: crate::std::time::Duration, + ) -> Result>> { + let FuncExecutionState::Running { exec_state, root_func_addr } = &mut self.state else { + let FuncExecutionState::Completed { result } = &mut self.state else { + unreachable!("invalid function execution state") + }; + return result + .take() + .map(ExecProgress::Completed) + .ok_or_else(|| Error::Other("execution already completed".to_string())); + }; + + match InterpreterRuntime::exec_with_time_budget(self.store, exec_state.callframe, time_budget)? { + crate::interpreter::ExecState::Completed => { + let result_ty = self.store.state.get_func(*root_func_addr).func.ty().clone(); + let result = collect_call_results(self.store, &result_ty)?; + self.state = FuncExecutionState::Completed { result: None }; + Ok(ExecProgress::Completed(result)) + } + crate::interpreter::ExecState::Suspended(callframe) => { + exec_state.callframe = callframe; + Ok(ExecProgress::Suspended) + } + } + } +} - // 2. Pop m values from the stack - let mut res: Vec<_> = store.stack.values.pop_types(func_ty.results.iter().rev()).collect(); // pop in reverse order since the stack is LIFO - res.reverse(); // reverse to get the original order +fn validate_call_params(func_ty: &FuncType, params: &[WasmValue]) -> Result<()> { + if unlikely(func_ty.params.len() != params.len()) { + return Err(Error::Other(format!( + "param count mismatch: expected {}, got {}", + func_ty.params.len(), + params.len() + ))); + } - // The values are returned as the results of the invocation. - Ok(res) + if !(func_ty.params.iter().zip(params).all(|(ty, param)| ty == ¶m.val_type())) { + return Err(Error::Other("Type mismatch".into())); } + + Ok(()) +} + +fn collect_call_results(store: &mut Store, func_ty: &FuncType) -> Result> { + // m values are on the top of the stack (Ensured by validation) + debug_assert!(store.stack.values.len() >= func_ty.results.len()); + let mut res: Vec<_> = store.stack.values.pop_types(func_ty.results.iter().rev()).collect(); // pop in reverse order since the stack is LIFO + res.reverse(); // reverse to get the original order + Ok(res) } #[derive(Debug)] @@ -101,6 +241,40 @@ impl FuncHandleTyped { // Convert the Vec back to R R::from_wasm_value_tuple(&result) } + + /// Call a typed function and return a resumable execution handle. + /// + /// The handle keeps a mutable borrow of the [`Store`] until completion. + pub fn call_resumable<'store>(&self, store: &'store mut Store, params: P) -> Result> { + let wasm_values = params.into_wasm_value_tuple(); + let execution = self.func.call_resumable(store, &wasm_values)?; + Ok(FuncExecutionTyped { execution, marker: core::marker::PhantomData }) + } +} + +impl<'store, R: FromWasmValueTuple> FuncExecutionTyped<'store, R> { + /// Resume typed execution with up to `fuel` units of fuel. + /// + /// Fuel is accounted in chunks, so execution may overshoot the requested + /// fuel before returning [`ExecProgress::Suspended`]. + pub fn resume_with_fuel(&mut self, fuel: u32) -> Result> { + match self.execution.resume_with_fuel(fuel)? { + ExecProgress::Completed(values) => Ok(ExecProgress::Completed(R::from_wasm_value_tuple(&values)?)), + ExecProgress::Suspended => Ok(ExecProgress::Suspended), + } + } + + #[cfg(feature = "std")] + /// Resume typed execution for at most `time_budget` wall-clock time. + /// + /// Time is checked periodically, so execution may overshoot the requested + /// time budget before returning [`ExecProgress::Suspended`]. + pub fn resume_with_time_budget(&mut self, time_budget: crate::std::time::Duration) -> Result> { + match self.execution.resume_with_time_budget(time_budget)? { + ExecProgress::Completed(values) => Ok(ExecProgress::Completed(R::from_wasm_value_tuple(&values)?)), + ExecProgress::Suspended => Ok(ExecProgress::Suspended), + } + } } pub trait ValTypesFromTuple { diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs index ea8b965..7ce9f40 100644 --- a/crates/tinywasm/src/imports.rs +++ b/crates/tinywasm/src/imports.rs @@ -82,6 +82,21 @@ impl FuncContext<'_> { pub fn exported_memory_mut(&mut self, name: &str) -> Result> { self.module().exported_memory_mut(self.store, name) } + + /// Charge additional fuel from the currently running resumable invocation. + /// + /// This is a no-op when the current invocation is not using fuel-based + /// resumption. + pub fn charge_fuel(&mut self, fuel: u32) { + self.store.execution_fuel = self.store.execution_fuel.saturating_sub(fuel); + } + + /// Get remaining fuel for the current invocation. + /// + /// Returns `0` when fuel-based resumption is not active. + pub fn remaining_fuel(&self) -> u32 { + self.store.execution_fuel + } } impl Debug for HostFunction { diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index 3457120..2e2bd0c 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -9,33 +9,42 @@ use core::ops::ControlFlow; use interpreter::stack::CallFrame; use tinywasm_types::*; +use super::ExecState; use super::num_helpers::*; use super::values::*; +use crate::engine::FuelPolicy; use crate::instance::ModuleInstanceInner; use crate::interpreter::Value128; use crate::*; -pub(crate) struct Executor<'store> { + +const FUEL_ACCOUNTING_INTERVAL: u32 = 1024; +#[cfg(feature = "std")] +const TIME_BUDGET_CHECK_INTERVAL: u32 = 2048; +const FUEL_COST_CALL_TOTAL: u32 = 5; + +pub(crate) struct Executor<'store, const BUDGETED: bool> { cf: CallFrame, func: Rc, module: Rc, store: &'store mut Store, } -impl<'store> Executor<'store> { +impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { pub(crate) fn new(store: &'store mut Store, cf: CallFrame) -> Result { let module = store.get_module_instance_raw(cf.module_addr).clone(); let func = store.state.get_wasm_func(cf.func_addr).clone(); Ok(Self { module, store, cf, func }) } - pub(crate) fn run_to_completion(&mut self) -> Result<()> { - loop { - if let ControlFlow::Break(res) = self.exec_next() { - return match res { - Some(e) => Err(e), - None => Ok(()), - }; - } + #[inline(always)] + fn charge_call_fuel(&mut self, total_fuel_cost: u32) { + if BUDGETED { + let extra = match self.store.engine.config().fuel_policy { + FuelPolicy::PerInstruction => 0, + FuelPolicy::Weighted => total_fuel_cost.saturating_sub(1), + }; + + self.store.execution_fuel = self.store.execution_fuel.saturating_sub(extra); } } @@ -701,6 +710,7 @@ impl<'store> Executor<'store> { ControlFlow::Continue(()) } fn exec_call_direct(&mut self, v: u32) -> ControlFlow> { + self.charge_call_fuel(FUEL_COST_CALL_TOTAL); let addr = self.module.resolve_func_addr(v); let func_inst = self.store.state.get_func(addr); match &func_inst.func { @@ -712,6 +722,7 @@ impl<'store> Executor<'store> { } fn exec_call_self(&mut self) -> ControlFlow> { + self.charge_call_fuel(FUEL_COST_CALL_TOTAL); let params = self.func.params; let locals = self.func.locals; @@ -745,6 +756,7 @@ impl<'store> Executor<'store> { type_addr: u32, table_addr: u32, ) -> ControlFlow> { + self.charge_call_fuel(FUEL_COST_CALL_TOTAL); // verify that the table is of the right type, this should be validated by the parser already let func_ref = { let table_idx: u32 = self.store.stack.values.pop::() as u32; @@ -1112,3 +1124,64 @@ impl<'store> Executor<'store> { table.fill(self.module.func_addrs(), i as usize, n as usize, val.into()) } } + +impl<'store> Executor<'store, false> { + #[inline(always)] + pub(crate) fn run_to_completion(&mut self) -> Result<()> { + loop { + match self.exec_next() { + ControlFlow::Continue(()) => continue, + ControlFlow::Break(res) => break res.map_or(Ok(()), Err), + } + } + } + + #[cfg(feature = "std")] + #[inline(always)] + pub(crate) fn run_with_time_budget(&mut self, time_budget: crate::std::time::Duration) -> Result { + use crate::std::time::Instant; + let start = Instant::now(); + if time_budget.is_zero() { + return Ok(ExecState::Suspended(self.cf)); + } + + loop { + for _ in 0..TIME_BUDGET_CHECK_INTERVAL { + match self.exec_next() { + ControlFlow::Continue(()) => {} + ControlFlow::Break(None) => return Ok(ExecState::Completed), + ControlFlow::Break(Some(e)) => return Err(e), + } + } + + if start.elapsed() >= time_budget { + return Ok(ExecState::Suspended(self.cf)); + } + } + } +} + +impl<'store> Executor<'store, true> { + #[inline(always)] + pub(crate) fn run_with_fuel(&mut self, fuel: u32) -> Result { + self.store.execution_fuel = fuel; + if self.store.execution_fuel == 0 { + return Ok(ExecState::Suspended(self.cf)); + } + + loop { + for _ in 0..FUEL_ACCOUNTING_INTERVAL { + match self.exec_next() { + ControlFlow::Continue(()) => {} + ControlFlow::Break(None) => return Ok(ExecState::Completed), + ControlFlow::Break(Some(e)) => return Err(e), + } + } + + self.store.execution_fuel = self.store.execution_fuel.saturating_sub(FUEL_ACCOUNTING_INTERVAL); + if self.store.execution_fuel == 0 { + return Ok(ExecState::Suspended(self.cf)); + } + } + } +} diff --git a/crates/tinywasm/src/interpreter/mod.rs b/crates/tinywasm/src/interpreter/mod.rs index 1a9a0cc..8c3a840 100644 --- a/crates/tinywasm/src/interpreter/mod.rs +++ b/crates/tinywasm/src/interpreter/mod.rs @@ -11,6 +11,12 @@ use crate::{Result, Store, interpreter::stack::CallFrame}; pub(crate) use value128::*; pub(crate) use values::*; +#[derive(Debug, Clone, Copy)] +pub(crate) enum ExecState { + Completed, + Suspended(CallFrame), +} + /// The main `TinyWasm` runtime. /// /// This is the default runtime used by `TinyWasm`. @@ -19,6 +25,19 @@ pub(crate) struct InterpreterRuntime; impl InterpreterRuntime { pub(crate) fn exec(store: &mut Store, cf: CallFrame) -> Result<()> { - executor::Executor::new(store, cf)?.run_to_completion() + executor::Executor::::new(store, cf)?.run_to_completion() + } + + pub(crate) fn exec_with_fuel(store: &mut Store, cf: CallFrame, fuel: u32) -> Result { + executor::Executor::::new(store, cf)?.run_with_fuel(fuel) + } + + #[cfg(feature = "std")] + pub(crate) fn exec_with_time_budget( + store: &mut Store, + cf: CallFrame, + time_budget: crate::std::time::Duration, + ) -> Result { + executor::Executor::::new(store, cf)?.run_with_time_budget(time_budget) } } diff --git a/crates/tinywasm/src/interpreter/value128.rs b/crates/tinywasm/src/interpreter/value128.rs index 7de4dca..a41d43f 100644 --- a/crates/tinywasm/src/interpreter/value128.rs +++ b/crates/tinywasm/src/interpreter/value128.rs @@ -296,8 +296,8 @@ impl Value128 { #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] #[inline(always)] + #[rustfmt::skip] fn from_wasm_v128(value: wasm::v128) -> Self { - #[rustfmt::skip] Self::from_le_bytes([ wasm::u8x16_extract_lane::<0>(value), wasm::u8x16_extract_lane::<1>(value), wasm::u8x16_extract_lane::<2>(value), wasm::u8x16_extract_lane::<3>(value), wasm::u8x16_extract_lane::<4>(value), wasm::u8x16_extract_lane::<5>(value), wasm::u8x16_extract_lane::<6>(value), wasm::u8x16_extract_lane::<7>(value), wasm::u8x16_extract_lane::<8>(value), wasm::u8x16_extract_lane::<9>(value), wasm::u8x16_extract_lane::<10>(value), wasm::u8x16_extract_lane::<11>(value), wasm::u8x16_extract_lane::<12>(value), wasm::u8x16_extract_lane::<13>(value), wasm::u8x16_extract_lane::<14>(value), wasm::u8x16_extract_lane::<15>(value)]) } diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 31d52f7..b77dc09 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -91,7 +91,7 @@ pub(crate) mod log { mod error; pub use error::*; -pub use func::{FuncHandle, FuncHandleTyped}; +pub use func::{ExecProgress, FuncExecution, FuncExecutionTyped, FuncHandle, FuncHandleTyped}; pub use imports::*; pub use instance::ModuleInstance; pub use module::Module; diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index c175b8e..b409a0c 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -35,6 +35,7 @@ pub struct Store { module_instances: Vec>, pub(crate) engine: Engine, + pub(crate) execution_fuel: u32, pub(crate) state: State, pub(crate) stack: Stack, } @@ -54,7 +55,14 @@ impl Store { /// Create a new store pub fn new(engine: Engine) -> Self { let id = STORE_ID.fetch_add(1, Ordering::Relaxed); - Self { id, module_instances: Vec::new(), state: State::default(), stack: Stack::new(engine.config()), engine } + Self { + id, + module_instances: Vec::new(), + state: State::default(), + stack: Stack::new(engine.config()), + engine, + execution_fuel: 0, + } } /// Get a module instance by the internal id diff --git a/crates/tinywasm/tests/resume_execution.rs b/crates/tinywasm/tests/resume_execution.rs new file mode 100644 index 0000000..0f45e20 --- /dev/null +++ b/crates/tinywasm/tests/resume_execution.rs @@ -0,0 +1,116 @@ +use eyre::Result; +use tinywasm::engine::{Config, FuelPolicy}; +use tinywasm::{ExecProgress, Module, types::WasmValue}; + +#[cfg(feature = "std")] +use std::time::Duration; + +const FIBONACCI_WASM: &[u8] = include_bytes!("../../../examples/rust/out/fibonacci.wasm"); +const ADD_WASM: &[u8] = include_bytes!("../../../examples/wasm/add.wasm"); + +#[test] +fn typed_resume_matches_non_budgeted_call() -> Result<()> { + let module = Module::parse_bytes(FIBONACCI_WASM)?; + + let mut store_full = tinywasm::Store::default(); + let instance_full = module.clone().instantiate(&mut store_full, None)?; + let func_full = instance_full.exported_func::(&store_full, "fibonacci_recursive")?; + let expected = func_full.call(&mut store_full, 20)?; + + let mut store_budgeted = tinywasm::Store::default(); + let instance_budgeted = module.instantiate(&mut store_budgeted, None)?; + let func_budgeted = instance_budgeted.exported_func::(&store_budgeted, "fibonacci_recursive")?; + + let mut exec = func_budgeted.call_resumable(&mut store_budgeted, 20)?; + let mut saw_suspended = false; + let actual = loop { + match exec.resume_with_fuel(64)? { + ExecProgress::Completed(value) => break value, + ExecProgress::Suspended => saw_suspended = true, + } + }; + + assert!(saw_suspended, "expected at least one suspension for recursive fibonacci"); + assert_eq!(actual, expected); + + Ok(()) +} + +#[test] +fn untyped_resume_supports_zero_fuel() -> Result<()> { + let module = Module::parse_bytes(ADD_WASM)?; + let mut store = tinywasm::Store::default(); + let instance = module.instantiate(&mut store, None)?; + let func = instance.exported_func_untyped(&store, "add")?; + + let mut exec = func.call_resumable(&mut store, &[WasmValue::I32(20), WasmValue::I32(22)])?; + assert!(matches!(exec.resume_with_fuel(0)?, ExecProgress::Suspended)); + + match exec.resume_with_fuel(16)? { + ExecProgress::Completed(values) => assert_eq!(values, vec![WasmValue::I32(42)]), + ExecProgress::Suspended => panic!("expected completion"), + } + + Ok(()) +} + +#[test] +fn weighted_call_fuel_requires_more_rounds() -> Result<()> { + let module = Module::parse_bytes(FIBONACCI_WASM)?; + + let mut per_instr_store = tinywasm::Store::default(); + let instance_per_instr = module.clone().instantiate(&mut per_instr_store, None)?; + let func_per_instr = instance_per_instr.exported_func::(&per_instr_store, "fibonacci_recursive")?; + + let mut weighted_store = + tinywasm::Store::new(tinywasm::Engine::new(Config::new().fuel_policy(FuelPolicy::Weighted))); + let instance_weighted = module.instantiate(&mut weighted_store, None)?; + let func_weighted = instance_weighted.exported_func::(&weighted_store, "fibonacci_recursive")?; + + let fuel = 64; + let n = 20; + + let mut per_exec = func_per_instr.call_resumable(&mut per_instr_store, n)?; + let mut per_rounds = 0; + let per_result = loop { + per_rounds += 1; + match per_exec.resume_with_fuel(fuel)? { + ExecProgress::Completed(value) => break value, + ExecProgress::Suspended => {} + } + }; + + let mut weighted_exec = func_weighted.call_resumable(&mut weighted_store, n)?; + let mut weighted_rounds = 0; + let weighted_result = loop { + weighted_rounds += 1; + match weighted_exec.resume_with_fuel(fuel)? { + ExecProgress::Completed(value) => break value, + ExecProgress::Suspended => {} + } + }; + + assert_eq!(weighted_result, per_result); + assert!(weighted_rounds >= per_rounds, "weighted call fuel should not use fewer rounds than per-instruction"); + + Ok(()) +} + +#[cfg(feature = "std")] +#[test] +fn time_budget_zero_suspends_then_completes() -> Result<()> { + let module = Module::parse_bytes(ADD_WASM)?; + let mut store = tinywasm::Store::default(); + let instance = module.instantiate(&mut store, None)?; + let func = instance.exported_func::<(i32, i32), i32>(&store, "add")?; + + let mut exec = func.call_resumable(&mut store, (20, 22))?; + assert!(matches!(exec.resume_with_time_budget(Duration::ZERO)?, ExecProgress::Suspended)); + + match exec.resume_with_time_budget(Duration::from_millis(1))? { + ExecProgress::Completed(value) => assert_eq!(value, 42), + ExecProgress::Suspended => panic!("expected completion"), + } + + Ok(()) +} diff --git a/examples/resumable.rs b/examples/resumable.rs new file mode 100644 index 0000000..6838e20 --- /dev/null +++ b/examples/resumable.rs @@ -0,0 +1,46 @@ +use eyre::Result; +use tinywasm::{ExecProgress, Module, Store}; + +const WASM: &str = r#" +(module + (func (export "count_down") (param $n i32) (result i32) + (local $cur i32) + local.get $n + local.set $cur + block + loop + local.get $cur + i32.eqz + br_if 1 + local.get $cur + i32.const 1 + i32.sub + local.set $cur + br 0 + end + end + local.get $cur)) +"#; + +fn main() -> Result<()> { + let wasm = wat::parse_str(WASM)?; + let module = Module::parse_bytes(&wasm)?; + let mut store = Store::default(); + let instance = module.instantiate(&mut store, None)?; + let count_down = instance.exported_func::(&store, "count_down")?; + + let mut execution = count_down.call_resumable(&mut store, 10_000)?; + let fuel_per_round = 128; + let mut fuel_rounds = 0; + + let result = loop { + fuel_rounds += 1; + match execution.resume_with_fuel(fuel_per_round)? { + ExecProgress::Completed(value) => break value, + ExecProgress::Suspended => {} + } + }; + + println!("completed in {fuel_rounds} rounds of {fuel_per_round} fuel, result={result}"); + Ok(()) +} diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index 430bcad..6bf8a8f 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -52,3 +52,4 @@ lto="fat" codegen-units=1 panic="abort" inherits="release" +strip=true diff --git a/examples/rust/build.sh b/examples/rust/build.sh index 9df5447..4574d23 100755 --- a/examples/rust/build.sh +++ b/examples/rust/build.sh @@ -2,11 +2,12 @@ cd "$(dirname "$0")" || exit bins=("host_fn" "hello" "fibonacci" "print" "tinywasm" "argon2id") +exclude_wat=("tinywasm") out_dir="./target/wasm32-unknown-unknown/wasm" dest_dir="out" -rust_features="+sign-ext,+simd128,+reference-types,+bulk-memory,+bulk-memory-opt,+multimemory,+call-indirect-overlong,+mutable-globals,+multivalue,+sign-ext,+nontrapping-fptoint,+extended-const,+tail-call" -# wasmopt_features="--enable-reference-types --enable-bulk-memory --enable-mutable-globals --enable-multivalue --enable-sign-ext --enable-nontrapping-float-to-int" +rust_features="+simd128,+reference-types,+bulk-memory,+mutable-globals,+multivalue,+sign-ext,+nontrapping-fptoint" +wasmopt_features="--enable-simd --enable-reference-types --enable-bulk-memory --enable-mutable-globals --enable-multivalue --enable-sign-ext --enable-nontrapping-float-to-int --duplicate-function-elimination" # ensure out dir exists mkdir -p "$dest_dir" @@ -16,8 +17,12 @@ cargo build --target wasm32-unknown-unknown --package rust-wasm-examples --profi cp "$out_dir/tinywasm_no_std.wasm" "$dest_dir/" for bin in "${bins[@]}"; do - RUSTFLAGS="-C target-feature=$rust_features -C panic=abort" cargo build --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin "$bin" + RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none -C target-feature=$rust_features -C panic=abort" cargo build -Z build-std=std,panic_abort -Z build-std-features="optimize_for_size" --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin "$bin" cp "$out_dir/$bin.wasm" "$dest_dir/" - # wasm-opt "$dest_dir/$bin.wasm" -o "$dest_dir/$bin.opt.wasm" -O3 $wasmopt_features -done + wasm-opt "$dest_dir/$bin.wasm" -o "$dest_dir/$bin.opt.wasm" -O3 -Oz $wasmopt_features + + if [[ ! " ${exclude_wat[@]} " =~ " $bin " ]]; then + wasm2wat "$dest_dir/$bin.wasm" -o "$dest_dir/$bin.wat" + fi +done \ No newline at end of file diff --git a/examples/wasm-rust.rs b/examples/wasm-rust.rs index 4d8cdf4..49792b0 100644 --- a/examples/wasm-rust.rs +++ b/examples/wasm-rust.rs @@ -72,7 +72,7 @@ fn main() -> Result<()> { } fn tinywasm() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/tinywasm.wasm")?; + let module = Module::parse_file("./examples/rust/out/tinywasm.opt.wasm")?; let mut store = Store::default(); let mut imports = Imports::new(); @@ -87,7 +87,7 @@ fn tinywasm() -> Result<()> { } fn tinywasm_no_std() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/tinywasm_no_std.wasm")?; + let module = Module::parse_file("./examples/rust/out/tinywasm_no_std.opt.wasm")?; let mut store = Store::default(); let mut imports = Imports::new(); @@ -102,7 +102,7 @@ fn tinywasm_no_std() -> Result<()> { } fn hello() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/hello.wasm")?; + let module = Module::parse_file("./examples/rust/out/hello.opt.wasm")?; let mut store = Store::default(); let mut imports = Imports::new(); @@ -131,7 +131,7 @@ fn hello() -> Result<()> { } fn host_fn() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/host_fn.wasm")?; + let module = Module::parse_file("./examples/rust/out/host_fn.opt.wasm")?; let mut store = Store::default(); let mut imports = Imports::new(); imports.define( @@ -151,7 +151,7 @@ fn host_fn() -> Result<()> { } fn printi32() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/print.wasm")?; + let module = Module::parse_file("./examples/rust/out/print.opt.wasm")?; let mut store = Store::default(); let mut imports = Imports::new(); @@ -172,7 +172,7 @@ fn printi32() -> Result<()> { } fn fibonacci() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/fibonacci.wasm")?; + let module = Module::parse_file("./examples/rust/out/fibonacci.opt.wasm")?; let mut store = Store::default(); let instance = module.instantiate(&mut store, None)?; @@ -185,7 +185,7 @@ fn fibonacci() -> Result<()> { } fn argon2id() -> Result<()> { - let module = Module::parse_file("./examples/rust/out/argon2id.wasm")?; + let module = Module::parse_file("./examples/rust/out/argon2id.opt.wasm")?; let mut store = Store::default(); let instance = module.instantiate(&mut store, None)?; -- cgit v1.3.1