summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHenry <mail@henrygressmann.de>2026-04-03 00:36:57 +0200
committerHenry <mail@henrygressmann.de>2026-04-03 00:36:57 +0200
commitca6c66af87106b097a599e344d5c0cb538250abb (patch)
tree1caf44f96a8dd82883e68fe1df8e89ccd418b37b
parentbfeefda25407d29a843d2aec713943843f8f1239 (diff)
feat: add resumable calls / fuel tracking
Signed-off-by: Henry <mail@henrygressmann.de>
-rw-r--r--crates/tinywasm/Cargo.toml4
-rw-r--r--crates/tinywasm/benches/tinywasm.rs10
-rw-r--r--crates/tinywasm/benches/tinywasm_modes.rs102
-rw-r--r--crates/tinywasm/src/engine.rs25
-rw-r--r--crates/tinywasm/src/func.rs236
-rw-r--r--crates/tinywasm/src/imports.rs15
-rw-r--r--crates/tinywasm/src/interpreter/executor.rs93
-rw-r--r--crates/tinywasm/src/interpreter/mod.rs21
-rw-r--r--crates/tinywasm/src/interpreter/value128.rs2
-rw-r--r--crates/tinywasm/src/lib.rs2
-rw-r--r--crates/tinywasm/src/store/mod.rs10
-rw-r--r--crates/tinywasm/tests/resume_execution.rs116
-rw-r--r--examples/resumable.rs46
-rw-r--r--examples/rust/Cargo.toml1
-rwxr-xr-xexamples/rust/build.sh15
-rw-r--r--examples/wasm-rust.rs14
16 files changed, 651 insertions, 61 deletions
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<TinyWasmModule> {
+ let parser = tinywasm_parser::Parser::new();
+ Ok(parser.parse_module_bytes(WASM)?)
+}
+
+fn setup_typed_func(module: TinyWasmModule, engine: Option<Engine>) -> 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<dyn Allocator + Send + Sync>,
}
+/// 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<T> {
+ /// 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<Vec<WasmValue>> },
+}
+
+#[derive(Debug)]
+/// Resumable execution for a typed function call.
+pub struct FuncExecutionTyped<'store, R> {
+ execution: FuncExecution<'store>,
+ marker: core::marker::PhantomData<R>,
+}
+
impl FuncHandle {
/// Call a function (Invocation)
///
/// See <https://webassembly.github.io/spec/core/exec/modules.html#invocation>
#[inline]
pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result<Vec<WasmValue>> {
- // 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 == &param.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,31 +62,155 @@ 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)
+ }
- // 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
+ /// 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<FuncExecution<'store>> {
+ 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();
- // The values are returned as the results of the invocation.
- Ok(res)
+ 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<ExecProgress<Vec<WasmValue>>> {
+ 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<ExecProgress<Vec<WasmValue>>> {
+ 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)
+ }
+ }
+ }
+}
+
+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()
+ )));
+ }
+
+ if !(func_ty.params.iter().zip(params).all(|(ty, param)| ty == &param.val_type())) {
+ return Err(Error::Other("Type mismatch".into()));
+ }
+
+ Ok(())
+}
+
+fn collect_call_results(store: &mut Store, func_ty: &FuncType) -> Result<Vec<WasmValue>> {
+ // 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)]
/// A typed function handle
pub struct FuncHandleTyped<P, R> {
@@ -101,6 +241,40 @@ impl<P: IntoWasmValueTuple, R: FromWasmValueTuple> FuncHandleTyped<P, R> {
// Convert the Vec<WasmValue> 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<FuncExecutionTyped<'store, R>> {
+ 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<ExecProgress<R>> {
+ 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<ExecProgress<R>> {
+ 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<MemoryRefMut<'_>> {
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<WasmFunction>,
module: Rc<ModuleInstanceInner>,
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<Self> {
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<const IS_RETURN_CALL: bool>(&mut self, v: u32) -> ControlFlow<Option<Error>> {
+ 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<const IS_RETURN_CALL: bool>(&mut self) -> ControlFlow<Option<Error>> {
+ 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<Option<Error>> {
+ 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::<i32>() 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<ExecState> {
+ 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<ExecState> {
+ 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::<false>::new(store, cf)?.run_to_completion()
+ }
+
+ pub(crate) fn exec_with_fuel(store: &mut Store, cf: CallFrame, fuel: u32) -> Result<ExecState> {
+ executor::Executor::<true>::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<ExecState> {
+ executor::Executor::<false>::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<Rc<ModuleInstanceInner>>,
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::<i32, i32>(&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::<i32, i32>(&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::<i32, i32>(&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::<i32, i32>(&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::<i32, i32>(&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)?;