Skip to content

Async #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: next
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
982925e
feat: implement basic async
WhaleKit Jan 1, 2025
ff0c467
test: add example/test for async
WhaleKit Jan 1, 2025
30292a6
remove unnecessary mut for making exported memory
WhaleKit Jan 5, 2025
9ff3d6d
fix: support for multiple resumes on single suspendedFucn now works
WhaleKit Jan 5, 2025
b66cd50
feat: add support for suspending execution by timer, atomic flag or u…
WhaleKit Jan 5, 2025
250402c
ci: add test for suspending and resuming wasm code
WhaleKit Jan 5, 2025
3aa69da
chore: fix codestyle in host_coro example
WhaleKit Jan 5, 2025
d59d31f
fix bug with br_if instruction corrupting stack
WhaleKit Jan 8, 2025
9454509
Merge branch 'next' into async
WhaleKit Jan 8, 2025
a0bfa66
make wasm_resume nominally look like a test
WhaleKit Jan 8, 2025
9a90550
fix: building with no-std
WhaleKit Jan 8, 2025
b49c1c4
support suspending start function when instantiating module
WhaleKit Jan 9, 2025
9b7496b
ci: add feature to run test-suite with async suspends
WhaleKit Jan 9, 2025
89d9dcc
minor: clippy, names, qol function
WhaleKit Jan 9, 2025
ff933a2
chore: reorganized some code
WhaleKit Jan 11, 2025
b73ab18
codestyle, relax bounds on YieldedValue/ResumeArgument
WhaleKit Jan 11, 2025
ae83085
feat: added call_coro to typed fucn handle, some code reorganization …
WhaleKit Jan 11, 2025
a41f68b
fix: fix no-std build
WhaleKit Jan 11, 2025
27c7d66
move resume test
WhaleKit Jan 12, 2025
450881e
feat: all async/resume functionality is now feature-gated
WhaleKit Jan 13, 2025
cf85b34
fix various issues
WhaleKit Jan 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -30,6 +30,10 @@ rust-version.workspace=true
name="wasm-rust"
test=false

[[example]]
name="host_coro"
required-features=["async"]

[dev-dependencies]
wat={workspace=true}
eyre={workspace=true}
2 changes: 2 additions & 0 deletions crates/tinywasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -37,6 +37,8 @@ logging=["log", "tinywasm-parser?/logging", "tinywasm-types/logging"]
std=["tinywasm-parser?/std", "tinywasm-types/std"]
parser=["dep:tinywasm-parser"]
archive=["tinywasm-types/archive"]
async=[]
test_async=["async"] #feels weird putting it here

[[test]]
name="test-wasm-1"
260 changes: 260 additions & 0 deletions crates/tinywasm/src/coro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
#![cfg_attr(not(feature = "async"), allow(unused))]
#![cfg_attr(not(feature = "async"), allow(unreachable_pub))]

mod module {
use crate::Result;
use core::fmt::Debug;
pub(crate) use tinywasm_types::{ResumeArgument, YieldedValue};

///"coroutine statse", "coroutine instance", "resumable". Stores info to continue a function that was paused
pub trait CoroState<Ret, ResumeContext>: Debug {
#[cfg(feature = "async")]
/// resumes the execution of the coroutine
fn resume(&mut self, ctx: ResumeContext, arg: ResumeArgument) -> Result<CoroStateResumeResult<Ret>>;
}

/// explains why did execution suspend, and carries payload if needed
#[derive(Debug)]
#[non_exhaustive] // some variants are feature-gated
#[cfg(feature = "async")]
pub enum SuspendReason {
/// host function yielded
/// some host functions might expect resume argument when calling resume
Yield(YieldedValue),

/// time to suspend has come,
/// host shouldn't provide resume argument when calling resume
#[cfg(feature = "std")]
SuspendedEpoch,

/// user's should-suspend-callback returned Break,
/// host shouldn't provide resume argument when calling resume
SuspendedCallback,

/// async should_suspend flag was set
/// host shouldn't provide resume argument when calling resume
SuspendedFlag,
// possible others: delimited continuations proposal, debugger breakpoint, out of fuel
}

#[cfg(not(feature = "async"))]
pub type SuspendReason = core::convert::Infallible;

/// result of a function that might pause in the middle and yield
/// to be resumed later
#[derive(Debug)]
pub enum PotentialCoroCallResult<R, State>
//where for<Ctx>
// State: CoroState<R, Ctx>, // can't in stable rust
{
/// function returns normally
Return(R),
/// interpreter will be suspended and execution will return to host along with SuspendReason
Suspended(SuspendReason, State),
}

/// result of resuming coroutine state. Unlike [`PotentialCoroCallResult`]
/// doesn't need to have state, since it's contained in self
#[derive(Debug)]
pub enum CoroStateResumeResult<R> {
/// CoroState has finished
/// after this CoroState::resume can't be called again on that CoroState
Return(R),

/// host function yielded
/// execution returns to host along with yielded value
Suspended(SuspendReason),
}

impl<R, State> PotentialCoroCallResult<R, State> {
/// in case you expect function only to return
/// you can make Suspend into [crate::Error::UnexpectedSuspend] error
pub fn suspend_to_err(self) -> Result<R> {
match self {
PotentialCoroCallResult::Return(r) => Ok(r),
#[cfg(feature = "async")]
PotentialCoroCallResult::Suspended(r, _) => Err(crate::Error::UnexpectedSuspend(r.into())),
}
}

/// true if coro is finished
pub fn finished(&self) -> bool {
matches!(self, Self::Return(_))
}
/// separates state from PotentialCoroCallResult, leaving CoroStateResumeResult (one without state)
pub fn split_state(self) -> (CoroStateResumeResult<R>, Option<State>) {
match self {
Self::Return(val) => (CoroStateResumeResult::Return(val), None),
Self::Suspended(suspend, state) => (CoroStateResumeResult::Suspended(suspend), Some(state)),
}
}
/// separates result from PotentialCoroCallResult, leaving unit type in it's place
pub fn split_result(self) -> (PotentialCoroCallResult<(), State>, Option<R>) {
match self {
Self::Return(result) => (PotentialCoroCallResult::Return(()), Some(result)),
Self::Suspended(suspend, state) => (PotentialCoroCallResult::Suspended(suspend, state), None),
}
}

/// transforms state
pub fn map_state<OutS>(self, mapper: impl FnOnce(State) -> OutS) -> PotentialCoroCallResult<R, OutS> {
match self {
Self::Return(val) => PotentialCoroCallResult::Return(val),
Self::Suspended(suspend, state) => PotentialCoroCallResult::Suspended(suspend, mapper(state)),
}
}
/// transform result with mapper if there is none - calls "otherwise".
/// user_val passed to whichever is called and is guaranteed to be used
pub fn map<OutR, Usr, OutS>(
self,
user_val: Usr,
res_mapper: impl FnOnce(R, Usr) -> OutR,
state_mapper: impl FnOnce(State, Usr) -> OutS,
) -> PotentialCoroCallResult<OutR, OutS> {
match self {
Self::Return(res) => PotentialCoroCallResult::Return(res_mapper(res, user_val)),
Self::Suspended(suspend, state) => {
PotentialCoroCallResult::Suspended(suspend, state_mapper(state, user_val))
}
}
}
/// transforms result
pub fn map_result<OutR>(self, mapper: impl FnOnce(R) -> OutR) -> PotentialCoroCallResult<OutR, State> {
self.map((), |val, _| mapper(val), |s, _| s)
}
}

impl<R, State, E> PotentialCoroCallResult<core::result::Result<R, E>, State> {
/// turns Self<Result<R>, S> into Resulf<Self<R>, S>
pub fn propagate_err_result(self) -> core::result::Result<PotentialCoroCallResult<R, State>, E> {
Ok(match self {
PotentialCoroCallResult::Return(res) => PotentialCoroCallResult::<R, State>::Return(res?),
PotentialCoroCallResult::Suspended(why, state) => {
PotentialCoroCallResult::<R, State>::Suspended(why, state)
}
})
}
}
impl<R, State, E> PotentialCoroCallResult<R, core::result::Result<State, E>> {
/// turns Self<R, Result<S>> into Resulf<R, Self<S>>
pub fn propagate_err_state(self) -> core::result::Result<PotentialCoroCallResult<R, State>, E> {
Ok(match self {
PotentialCoroCallResult::Return(res) => PotentialCoroCallResult::<R, State>::Return(res),
PotentialCoroCallResult::Suspended(why, state) => {
PotentialCoroCallResult::<R, State>::Suspended(why, state?)
}
})
}
}

impl<R> CoroStateResumeResult<R> {
/// in case you expect function only to return
/// you can make Suspend into [crate::Error::UnexpectedSuspend] error
pub fn suspend_to_err(self) -> Result<R> {
match self {
Self::Return(r) => Ok(r),
#[cfg(feature = "async")]
Self::Suspended(r) => Err(crate::Error::UnexpectedSuspend(r.into())),
}
}

/// true if coro is finished
pub fn finished(&self) -> bool {
matches!(self, Self::Return(_))
}
/// separates result from CoroStateResumeResult, leaving unit type in it's place
pub fn split_result(self) -> (CoroStateResumeResult<()>, Option<R>) {
let (a, r) = PotentialCoroCallResult::<R, ()>::from(self).split_result();
(a.into(), r)
}
/// transforms result
pub fn map_result<OutR>(self, mapper: impl FnOnce(R) -> OutR) -> CoroStateResumeResult<OutR> {
PotentialCoroCallResult::<R, ()>::from(self).map_result(mapper).into()
}
/// transform result with mapper. If there is none - calls "otherwise"
/// user_val passed to whichever is called and is guaranteed to be used
pub fn map<OutR, Usr>(
self,
user_val: Usr,
mapper: impl FnOnce(R, Usr) -> OutR,
otherwise: impl FnOnce(Usr),
) -> CoroStateResumeResult<OutR> {
PotentialCoroCallResult::<R, ()>::from(self).map(user_val, mapper, |(), usr| otherwise(usr)).into()
}
}

impl<R, E> CoroStateResumeResult<core::result::Result<R, E>> {
/// turns Self<Result<R>> into Resulf<Self<R>>
pub fn propagate_err(self) -> core::result::Result<CoroStateResumeResult<R>, E> {
Ok(PotentialCoroCallResult::<core::result::Result<R, E>, ()>::from(self).propagate_err_result()?.into())
}
}

// convert between PotentialCoroCallResult<SrcR, ()> and CoroStateResumeResult<SrcR>
impl<DstR, SrcR> From<PotentialCoroCallResult<SrcR, ()>> for CoroStateResumeResult<DstR>
where
DstR: From<SrcR>,
{
fn from(value: PotentialCoroCallResult<SrcR, ()>) -> Self {
match value {
PotentialCoroCallResult::Return(val) => Self::Return(val.into()),
PotentialCoroCallResult::Suspended(suspend, ()) => Self::Suspended(suspend),
}
}
}
impl<SrcR> From<CoroStateResumeResult<SrcR>> for PotentialCoroCallResult<SrcR, ()> {
fn from(value: CoroStateResumeResult<SrcR>) -> Self {
match value {
CoroStateResumeResult::Return(val) => PotentialCoroCallResult::Return(val),
CoroStateResumeResult::Suspended(suspend) => PotentialCoroCallResult::Suspended(suspend, ()),
}
}
}

#[cfg(feature = "async")]
impl SuspendReason {
/// shotrhand to package val into a Box<any> in a [SuspendReason::Yield] variant
/// you'll need to specify type explicitly, because you'll need to use exact same type when downcasting
pub fn make_yield<T>(val: impl Into<T> + core::any::Any) -> Self {
Self::Yield(Some(alloc::boxed::Box::new(val) as alloc::boxed::Box<dyn core::any::Any>))
}
}

/// for use in error [`crate::Error::UnexpectedSuspend`]
/// same as [SuspendReason], but without [tinywasm_types::YieldedValue], since we can't debug-print it
/// and including it would either require YieldedValue to be Send+Sync or disable that for Error
#[derive(Debug)]
pub enum UnexpectedSuspendError {
/// host function yielded
Yield,

/// timeout,
#[cfg(feature = "std")]
SuspendedEpoch,

/// user's should-suspend-callback returned Break,
SuspendedCallback,

/// async should_suspend flag was set
SuspendedFlag,
}

#[cfg(feature = "async")]
impl From<SuspendReason> for UnexpectedSuspendError {
fn from(value: SuspendReason) -> Self {
match value {
SuspendReason::Yield(_) => Self::Yield,
#[cfg(feature = "std")]
SuspendReason::SuspendedEpoch => Self::SuspendedEpoch,
SuspendReason::SuspendedCallback => Self::SuspendedCallback,
SuspendReason::SuspendedFlag => Self::SuspendedFlag,
}
}
}
}

#[cfg(feature = "async")]
pub use module::*;

#[cfg(not(feature = "async"))]
pub(crate) use module::*;
27 changes: 24 additions & 3 deletions crates/tinywasm/src/error.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,11 @@ use tinywasm_types::FuncType;
#[cfg(feature = "parser")]
pub use tinywasm_parser::ParseError;

#[cfg(feature = "async")]
use crate::coro::UnexpectedSuspendError;

use crate::interpreter;

/// Errors that can occur for `TinyWasm` operations
#[derive(Debug)]
pub enum Error {
@@ -35,6 +40,17 @@ pub enum Error {
/// The store is not the one that the module instance was instantiated in
InvalidStore,

/// ResumeArgument of wrong type was provided
InvalidResumeArgument,

/// Tried to resume on runtime when it's not suspended
InvalidResume,

/// Function unexpectedly yielded instead of returning
/// (for backwards compatibility with old api)
#[cfg(feature = "async")]
UnexpectedSuspend(UnexpectedSuspendError),

#[cfg(feature = "std")]
/// An I/O error occurred
Io(crate::std::io::Error),
@@ -184,6 +200,9 @@ impl Display for Error {
#[cfg(feature = "std")]
Self::Io(err) => write!(f, "I/O error: {err}"),

#[cfg(feature = "async")]
Self::UnexpectedSuspend(_) => write!(f, "funtion yielded instead of returning"),

Self::Trap(trap) => write!(f, "trap: {trap}"),
Self::Linker(err) => write!(f, "linking error: {err}"),
Self::InvalidLabelType => write!(f, "invalid label type"),
@@ -193,6 +212,8 @@ impl Display for Error {
write!(f, "invalid host function return: expected={expected:?}, actual={actual:?}")
}
Self::InvalidStore => write!(f, "invalid store"),
Self::InvalidResumeArgument => write!(f, "invalid resume argument supplied to suspended function"),
Self::InvalidResume => write!(f, "attempt to resume coroutine that has already finished"),
}
}
}
@@ -246,14 +267,14 @@ impl From<tinywasm_parser::ParseError> for Error {
pub type Result<T, E = Error> = crate::std::result::Result<T, E>;

pub(crate) trait Controlify<T> {
fn to_cf(self) -> ControlFlow<Option<Error>, T>;
fn to_cf(self) -> ControlFlow<interpreter::executor::ReasonToBreak, T>;
}

impl<T> Controlify<T> for Result<T, Error> {
fn to_cf(self) -> ControlFlow<Option<Error>, T> {
fn to_cf(self) -> ControlFlow<interpreter::executor::ReasonToBreak, T> {
match self {
Ok(value) => ControlFlow::Continue(value),
Err(err) => ControlFlow::Break(Some(err)),
Err(err) => ControlFlow::Break(interpreter::executor::ReasonToBreak::Errored(err)),
}
}
}
155 changes: 140 additions & 15 deletions crates/tinywasm/src/func.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[cfg(feature = "async")]
use {crate::coro::CoroState, tinywasm_types::ResumeArgument};

use crate::interpreter;
use crate::interpreter::executor::SuspendedHostCoroState;
use crate::interpreter::stack::{CallFrame, Stack};
use crate::{log, unlikely, Function};
use crate::{Error, FuncContext, Result, Store};
@@ -19,8 +24,15 @@ 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>> {
self.call_coro(store, params)?.suspend_to_err()
}

/// Call a function (Invocation) and anticipate possible yield instead as well as return
#[inline]
pub fn call_coro(&self, store: &mut Store, params: &[WasmValue]) -> Result<FuncHandleCallOutcome> {
// Comments are ordered by the steps in the spec
// In this implementation, some steps are combined and ordered differently for performance reasons

@@ -53,7 +65,14 @@ impl FuncHandle {
Function::Host(host_func) => {
let host_func = host_func.clone();
let ctx = FuncContext { store, module_addr: self.module_addr };
return host_func.call(ctx, params);
return Ok(host_func.call(ctx, params)?.map_state(|state| SuspendedFunc {
func: SuspendedFuncInner::Host(SuspendedHostCoroState {
coro_state: state,
coro_orig_function: self.addr,
}),
module_addr: self.module_addr,
store_id: store.id(),
}));
}
Function::Wasm(wasm_func) => wasm_func,
};
@@ -63,23 +82,33 @@ impl FuncHandle {

// 7. Push the frame f to the call stack
// & 8. Push the values to the stack (Not needed since the call frame owns the values)
let mut stack = Stack::new(call_frame);
let stack = Stack::new(call_frame);

// 9. Invoke the function instance
let runtime = store.runtime();
runtime.exec(store, &mut stack)?;

// Once the function returns:
// let result_m = func_ty.results.len();

// 1. Assert: m values are on the top of the stack (Ensured by validation)
// assert!(stack.values.len() >= result_m);

// 2. Pop m values from the stack
let res = stack.values.pop_results(&func_ty.results);

// The values are returned as the results of the invocation.
Ok(res)
let exec_outcome = runtime.exec(store, stack)?;
Ok(exec_outcome
.map_result(|mut stack| -> Vec<WasmValue> {
// Once the function returns:
// let result_m = func_ty.results.len();

// 1. Assert: m values are on the top of the stack (Ensured by validation)
// assert!(stack.values.len() >= result_m);

// 2. Pop m values from the stack
stack.values.pop_results(&func_ty.results)
// The values are returned as the results of the invocation.
})
.map_state(|coro_state| -> SuspendedFunc {
SuspendedFunc {
func: SuspendedFuncInner::Wasm(SuspendedWasmFunc {
runtime: coro_state,
result_types: func_ty.results.clone(),
}),
module_addr: self.module_addr,
store_id: store.id(),
}
}))
}
}

@@ -113,6 +142,102 @@ 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, anticipating possible suspension of execution
pub fn call_coro(&self, store: &mut Store, params: P) -> Result<TypedFuncHandleCallOutcome<R>> {
// Convert params into Vec<WasmValue>
let wasm_values = params.into_wasm_value_tuple();

// Call the underlying WASM function
let result = self.func.call_coro(store, &wasm_values)?;

// Convert the Vec<WasmValue> back to R
result
.map_result(|vals| R::from_wasm_value_tuple(&vals))
.map_state(|state| SuspendedFuncTyped::<R> { func: state, _marker: Default::default() })
.propagate_err_result()
}
}

pub(crate) type FuncHandleCallOutcome = crate::coro::PotentialCoroCallResult<Vec<WasmValue>, SuspendedFunc>;
pub(crate) type TypedFuncHandleCallOutcome<R> = crate::coro::PotentialCoroCallResult<R, SuspendedFuncTyped<R>>;

#[derive(Debug)]
#[cfg_attr(not(feature = "async"), allow(unused))]
struct SuspendedWasmFunc {
runtime: interpreter::SuspendedRuntime,
result_types: Box<[ValType]>,
}
impl SuspendedWasmFunc {
#[cfg(feature = "async")]
fn resume(
&mut self,
ctx: FuncContext<'_>,
arg: ResumeArgument,
) -> Result<crate::CoroStateResumeResult<Vec<WasmValue>>> {
Ok(self.runtime.resume(ctx, arg)?.map_result(|mut stack| stack.values.pop_results(&self.result_types)))
}
}

#[derive(Debug)]
#[cfg_attr(not(feature = "async"), allow(unused))]
#[allow(clippy::large_enum_variant)] // Wasm is bigger, but also much more common variant
enum SuspendedFuncInner {
Wasm(SuspendedWasmFunc),
Host(SuspendedHostCoroState),
}

/// handle to function that was suspended and can be resumed
#[derive(Debug)]
#[cfg_attr(not(feature = "async"), allow(unused))]
pub struct SuspendedFunc {
func: SuspendedFuncInner,
module_addr: ModuleInstanceAddr,
store_id: usize,
}

impl crate::coro::CoroState<Vec<WasmValue>, &mut Store> for SuspendedFunc {
#[cfg(feature = "async")]
fn resume(
&mut self,
store: &mut Store,
arg: ResumeArgument,
) -> Result<crate::CoroStateResumeResult<Vec<WasmValue>>> {
if store.id() != self.store_id {
return Err(Error::InvalidStore);
}

let ctx = FuncContext { store, module_addr: self.module_addr };
match &mut self.func {
SuspendedFuncInner::Wasm(wasm) => wasm.resume(ctx, arg),
SuspendedFuncInner::Host(host) => Ok(host.coro_state.resume(ctx, arg)?),
}
}
}

/// A typed suspended function.
/// Only returned value(s) are typed, yielded value and resume argument types are impossible to know
#[cfg_attr(not(feature = "async"), allow(unused))]
pub struct SuspendedFuncTyped<R> {
/// The underlying untyped suspended function
pub func: SuspendedFunc,
pub(crate) _marker: core::marker::PhantomData<R>,
}

impl<R> core::fmt::Debug for SuspendedFuncTyped<R> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SuspendedFuncTyped").field("func", &self.func).finish()
}
}

impl<R> crate::coro::CoroState<R, &mut Store> for SuspendedFuncTyped<R>
where
R: FromWasmValueTuple,
{
#[cfg(feature = "async")]
fn resume(&mut self, ctx: &mut Store, arg: ResumeArgument) -> Result<crate::CoroStateResumeResult<R>> {
self.func.resume(ctx, arg)?.map_result(|vals| R::from_wasm_value_tuple(&vals)).propagate_err()
}
}

macro_rules! impl_into_wasm_value_tuple {
51 changes: 44 additions & 7 deletions crates/tinywasm/src/imports.rs
Original file line number Diff line number Diff line change
@@ -5,8 +5,9 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt::Debug;

use crate::coro::CoroState;
use crate::func::{FromWasmValueTuple, IntoWasmValueTuple, ValTypesFromTuple};
use crate::{log, LinkingError, MemoryRef, MemoryRefMut, Result};
use crate::{coro, log, LinkingError, MemoryRef, MemoryRefMut, PotentialCoroCallResult, Result};
use tinywasm_types::*;

/// The internal representation of a function
@@ -28,6 +29,11 @@ impl Function {
}
}

/// A "resumable" function. If a host function need to suspend wasm execution
/// it can return [`coro::PotentialCoroCallResult::Suspended`] with an object that implements this trait
pub trait HostCoroState: for<'a> CoroState<Vec<WasmValue>, FuncContext<'a>> + core::fmt::Debug + Send {}
impl<T: core::fmt::Debug + Send + for<'a> CoroState<Vec<WasmValue>, FuncContext<'a>>> HostCoroState for T {}

/// A host function
pub struct HostFunction {
pub(crate) ty: tinywasm_types::FuncType,
@@ -41,12 +47,14 @@ impl HostFunction {
}

/// Call the function
pub fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result<Vec<WasmValue>> {
pub fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result<InnerHostFunCallOutcome> {
(self.func)(ctx, args)
}
}

pub(crate) type HostFuncInner = Box<dyn Fn(FuncContext<'_>, &[WasmValue]) -> Result<Vec<WasmValue>>>;
pub(crate) type InnerHostFunCallOutcome = coro::PotentialCoroCallResult<Vec<WasmValue>, Box<dyn HostCoroState>>;

pub(crate) type HostFuncInner = Box<dyn Fn(FuncContext<'_>, &[WasmValue]) -> Result<InnerHostFunCallOutcome>>;

/// The context of a host-function call
#[derive(Debug)]
@@ -134,13 +142,22 @@ impl Extern {
Self::Memory { ty }
}

/// Create a new function import
pub fn func_coro(
ty: &tinywasm_types::FuncType,
func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result<InnerHostFunCallOutcome> + 'static,
) -> Self {
Self::Function(Function::Host(Rc::new(HostFunction { func: Box::new(func), ty: ty.clone() })))
}

/// Create a new function import
pub fn func(
ty: &tinywasm_types::FuncType,
func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result<Vec<WasmValue>> + 'static,
) -> Self {
let _ty = ty.clone();
let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result<Vec<WasmValue>> {
let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]|
-> Result<InnerHostFunCallOutcome> {
let _ty = _ty.clone();
let result = func(ctx, args)?;

@@ -155,10 +172,30 @@ impl Extern {
Ok(())
})?;

Ok(result)
Ok(PotentialCoroCallResult::Return(result))
};

Self::Function(Function::Host(Rc::new(HostFunction { func: Box::new(inner_func), ty: ty.clone() })))

}

/// Create a new typed function import
pub fn typed_func_coro<P, R>(
func: impl Fn(FuncContext<'_>, P) -> Result<coro::PotentialCoroCallResult<R, Box<dyn HostCoroState>>>
+ 'static,
) -> Self
where
P: FromWasmValueTuple + ValTypesFromTuple,
R: IntoWasmValueTuple + ValTypesFromTuple + Debug,
{
let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result<InnerHostFunCallOutcome> {
let args = P::from_wasm_value_tuple(args)?;
let result = func(ctx, args)?;
Ok(result.map_result(|vals|{vals.into_wasm_value_tuple().to_vec()}))
};

let ty = tinywasm_types::FuncType { params: P::val_types(), results: R::val_types() };
Self::Function(Function::Host(Rc::new(HostFunction { func: Box::new(inner_func), ty })))
}

/// Create a new typed function import
@@ -167,10 +204,10 @@ impl Extern {
P: FromWasmValueTuple + ValTypesFromTuple,
R: IntoWasmValueTuple + ValTypesFromTuple + Debug,
{
let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result<Vec<WasmValue>> {
let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result<InnerHostFunCallOutcome> {
let args = P::from_wasm_value_tuple(args)?;
let result = func(ctx, args)?;
Ok(result.into_wasm_value_tuple().to_vec())
Ok(InnerHostFunCallOutcome::Return(result.into_wasm_value_tuple().to_vec()))
};

let ty = tinywasm_types::FuncType { params: P::val_types(), results: R::val_types() };
19 changes: 18 additions & 1 deletion crates/tinywasm/src/instance.rs
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ use tinywasm_types::*;

use crate::func::{FromWasmValueTuple, IntoWasmValueTuple};
use crate::{Error, FuncHandle, FuncHandleTyped, Imports, MemoryRef, MemoryRefMut, Module, Result, Store};
#[cfg(feature = "async")]
use crate::{PotentialCoroCallResult, SuspendedFunc};

/// An instanciated WebAssembly module
///
@@ -189,7 +191,7 @@ impl ModuleInstance {
}

/// Get an exported memory by name
pub fn exported_memory<'a>(&self, store: &'a mut Store, name: &str) -> Result<MemoryRef<'a>> {
pub fn exported_memory<'a>(&self, store: &'a Store, name: &str) -> Result<MemoryRef<'a>> {
let export = self.export_addr(name).ok_or_else(|| Error::Other(format!("Export not found: {name}")))?;
let ExternVal::Memory(mem_addr) = export else {
return Err(Error::Other(format!("Export is not a memory: {}", name)));
@@ -263,4 +265,19 @@ impl ModuleInstance {
let _ = func.call(store, &[])?;
Ok(Some(()))
}

/// Invoke the start function of the module
///
/// Returns None if the module has no start function
/// If start function suspends, returns SuspendedFunc.
/// Only when it finishes can this module instance be considered instantiated
#[cfg(feature = "async")]
pub fn start_coro(&self, store: &mut Store) -> Result<Option<PotentialCoroCallResult<(), SuspendedFunc>>> {
let Some(func) = self.start_func(store)? else {
return Ok(None);
};

let res = func.call_coro(store, &[])?;
Ok(Some(res.map_result(|_| {})))
}
}
252 changes: 197 additions & 55 deletions crates/tinywasm/src/interpreter/executor.rs

Large diffs are not rendered by default.

81 changes: 78 additions & 3 deletions crates/tinywasm/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
@@ -6,8 +6,13 @@ mod values;

#[cfg(not(feature = "std"))]
mod no_std_floats;
#[cfg(feature = "async")]
use {executor::Executor, tinywasm_types::ResumeArgument};

use crate::{Result, Store};
use crate::coro;
use crate::{FuncContext, ModuleInstance, Result, Store};
use executor::SuspendedHostCoroState;
use stack::{CallFrame, Stack};
pub use values::*;

/// The main `TinyWasm` runtime.
@@ -16,8 +21,78 @@ pub use values::*;
#[derive(Debug, Default)]
pub struct InterpreterRuntime {}

#[derive(Debug)]
#[cfg_attr(not(feature = "async"), allow(unused))]
pub(crate) struct SuspendedRuntimeBody {
pub(crate) suspended_host_coro: Option<SuspendedHostCoroState>,
pub(crate) module: ModuleInstance,
pub(crate) frame: CallFrame,
}

#[derive(Debug)]
pub(crate) struct SuspendedRuntime {
#[cfg_attr(not(feature = "async"), allow(unused))]
pub(crate) body: Option<(SuspendedRuntimeBody, Stack)>,
}
#[cfg(feature = "async")]
impl SuspendedRuntime {
fn make_exec<'store, 'stack>(
body: SuspendedRuntimeBody,
stack: &'stack mut Stack,
store: &'store mut Store,
) -> Executor<'store, 'stack> {
Executor { cf: body.frame, suspended_host_coro: body.suspended_host_coro, module: body.module, store, stack }
}
fn unmake_exec(exec: Executor<'_, '_>) -> SuspendedRuntimeBody {
SuspendedRuntimeBody { suspended_host_coro: exec.suspended_host_coro, module: exec.module, frame: exec.cf }
}
}

impl coro::CoroState<stack::Stack, FuncContext<'_>> for SuspendedRuntime {
#[cfg(feature = "async")]
fn resume(
&mut self,
ctx: FuncContext<'_>,
arg: ResumeArgument,
) -> Result<coro::CoroStateResumeResult<stack::Stack>> {
// should be put back into self.body unless we're finished
let (body, mut stack) = if let Some(body_) = self.body.take() {
body_
} else {
return Err(crate::error::Error::InvalidResume);
};

let mut exec = Self::make_exec(body, &mut stack, ctx.store);
let resumed = match exec.resume(arg) {
Ok(resumed) => resumed,
Err(err) => {
self.body = Some((Self::unmake_exec(exec), stack));
return Err(err);
}
};
match resumed {
executor::ExecOutcome::Return(()) => Ok(coro::CoroStateResumeResult::Return(stack)),
executor::ExecOutcome::Suspended(suspend) => {
self.body = Some((Self::unmake_exec(exec), stack));
Ok(coro::CoroStateResumeResult::Suspended(suspend))
}
}
}
}

pub(crate) type RuntimeExecOutcome = coro::PotentialCoroCallResult<stack::Stack, SuspendedRuntime>;

impl InterpreterRuntime {
pub(crate) fn exec(&self, store: &mut Store, stack: &mut stack::Stack) -> Result<()> {
executor::Executor::new(store, stack)?.run_to_completion()
pub(crate) fn exec(&self, store: &mut Store, stack: stack::Stack) -> Result<RuntimeExecOutcome> {
let mut stack = stack;
let mut executor = executor::Executor::new(store, &mut stack)?;
match executor.run_to_suspension()? {
coro::CoroStateResumeResult::Return(()) => Ok(RuntimeExecOutcome::Return(stack)),
#[cfg(feature = "async")]
coro::CoroStateResumeResult::Suspended(suspend) => Ok(RuntimeExecOutcome::Suspended(
suspend,
SuspendedRuntime { body: Some((SuspendedRuntime::unmake_exec(executor), stack)) },
)),
}
}
}
2 changes: 1 addition & 1 deletion crates/tinywasm/src/interpreter/stack/block_stack.rs
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ pub(crate) struct BlockFrame {
pub(crate) ty: BlockType,
}

#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub(crate) enum BlockType {
Loop,
If,
22 changes: 15 additions & 7 deletions crates/tinywasm/src/interpreter/stack/call_stack.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use core::ops::ControlFlow;

use super::BlockType;
use crate::interpreter::executor::ReasonToBreak;
use crate::interpreter::values::*;
use crate::unlikely;
use crate::Trap;
use crate::{unlikely, Error};

use alloc::boxed::Box;
use alloc::{rc::Rc, vec, vec::Vec};
@@ -28,9 +29,9 @@ impl CallStack {
}

#[inline]
pub(crate) fn push(&mut self, call_frame: CallFrame) -> ControlFlow<Option<Error>> {
pub(crate) fn push(&mut self, call_frame: CallFrame) -> ControlFlow<ReasonToBreak> {
if unlikely((self.stack.len() + 1) >= MAX_CALL_STACK_SIZE) {
return ControlFlow::Break(Some(Trap::CallStackOverflow.into()));
return ControlFlow::Break(ReasonToBreak::Errored(Trap::CallStackOverflow.into()));
}
self.stack.push(call_frame);
ControlFlow::Continue(())
@@ -100,18 +101,25 @@ impl CallFrame {

/// Break to a block at the given index (relative to the current frame)
/// Returns `None` if there is no block at the given index (e.g. if we need to return, this is handled by the caller)
/// otherwise returns type if block it broke to
/// <div class="warning">
/// if it returned Some (broke to block),
/// it expects caller to increment instruction pointer after calling it:
/// otherwise caller might exit block that's already exited or inter block caller's already in
/// </div>
#[inline]
pub(crate) fn break_to(
&mut self,
break_to_relative: u32,
values: &mut super::ValueStack,
blocks: &mut super::BlockStack,
) -> Option<()> {
) -> Option<BlockType> {
let break_to = blocks.get_relative_to(break_to_relative, self.block_ptr)?;

let block_ty = break_to.ty;
// instr_ptr points to the label instruction, but the next step
// will increment it by 1 since we're changing the "current" instr_ptr
match break_to.ty {
match block_ty {
BlockType::Loop => {
// this is a loop, so we want to jump back to the start of the loop
self.instr_ptr = break_to.instr_ptr;
@@ -123,7 +131,7 @@ impl CallFrame {
if break_to_relative != 0 {
// we also want to trim the label stack to the loop (but not including the loop)
blocks.truncate(blocks.len() as u32 - break_to_relative);
return Some(());
return Some(BlockType::Loop);
}
}

@@ -140,7 +148,7 @@ impl CallFrame {
}
}

Some(())
Some(block_ty)
}

#[inline]
14 changes: 14 additions & 0 deletions crates/tinywasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -91,6 +91,19 @@ pub(crate) mod log {
}

mod error;
#[cfg(not(feature = "async"))]
#[allow(unused)]
use {
coro::{CoroState, CoroStateResumeResult, PotentialCoroCallResult, SuspendReason},
func::{SuspendedFunc, SuspendedFuncTyped},
};
#[cfg(feature = "async")]
pub use {
coro::{CoroState, CoroStateResumeResult, PotentialCoroCallResult, SuspendReason},
func::{SuspendedFunc, SuspendedFuncTyped},
module::IncompleteModule,
};

pub use error::*;
pub use func::{FuncHandle, FuncHandleTyped};
pub use imports::*;
@@ -99,6 +112,7 @@ pub use module::Module;
pub use reference::*;
pub use store::*;

mod coro;
mod func;
mod imports;
mod instance;
62 changes: 62 additions & 0 deletions crates/tinywasm/src/module.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[cfg(feature = "async")]
use crate::{CoroState, PotentialCoroCallResult, SuspendedFunc};
#[cfg(feature = "async")]
use tinywasm_types::ResumeArgument;

use crate::{Imports, ModuleInstance, Result, Store};
use tinywasm_types::TinyWasmModule;

@@ -56,4 +61,61 @@ impl Module {
let _ = instance.start(store)?;
Ok(instance)
}

/// same as [Self::instantiate] but accounts for possibility of start function suspending, in which case it returns
/// [PotentialCoroCallResult::Suspended]. You can call [CoroState::resume] on it at any time to resume instantiation
#[cfg(feature = "async")]
pub fn instantiate_coro(
self,
store: &mut Store,
imports: Option<Imports>,
) -> Result<PotentialCoroCallResult<ModuleInstance, IncompleteModule>> {
let instance = ModuleInstance::instantiate(store, self, imports)?;
let core_res = match instance.start_coro(store)? {
Some(res) => res,
None => return Ok(PotentialCoroCallResult::Return(instance)),
};
Ok(match core_res {
crate::PotentialCoroCallResult::Return(_) => PotentialCoroCallResult::Return(instance),
crate::PotentialCoroCallResult::Suspended(suspend_reason, state) => {
PotentialCoroCallResult::Suspended(suspend_reason, IncompleteModule(Some(HitTheFloor(instance, state))))
}
})
}
}

/// a corostate that results in [ModuleInstance] when finished
#[derive(Debug)]
#[cfg(feature = "async")]
pub struct IncompleteModule(Option<HitTheFloor>);

#[derive(Debug)]
#[cfg(feature = "async")]
struct HitTheFloor(ModuleInstance, SuspendedFunc);

#[cfg(feature = "async")]
impl CoroState<ModuleInstance, &mut Store> for IncompleteModule {
fn resume(&mut self, ctx: &mut Store, arg: ResumeArgument) -> Result<crate::CoroStateResumeResult<ModuleInstance>> {
let mut body: HitTheFloor = match self.0.take() {
Some(body) => body,
None => return Err(crate::Error::InvalidResume),
};
let coro_res = match body.1.resume(ctx, arg) {
Ok(res) => res,
Err(e) => {
self.0 = Some(body);
return Err(e);
}
};
match coro_res {
crate::CoroStateResumeResult::Return(_) => {
let res = body.0;
Ok(crate::CoroStateResumeResult::Return(res))
}
crate::CoroStateResumeResult::Suspended(suspend_reason) => {
self.0 = Some(body); // ...once told me
Ok(crate::CoroStateResumeResult::Suspended(suspend_reason))
}
}
}
}
38 changes: 37 additions & 1 deletion crates/tinywasm/src/store/mod.rs
Original file line number Diff line number Diff line change
@@ -11,8 +11,12 @@ mod element;
mod function;
mod global;
mod memory;
mod suspend_conditions;
mod table;

#[cfg(feature = "async")]
pub use suspend_conditions::*;

pub(crate) use {data::*, element::*, function::*, global::*, memory::*, table::*};

// global store id counter
@@ -33,6 +37,13 @@ pub struct Store {

pub(crate) data: StoreData,
pub(crate) runtime: Runtime,

// idk where really to put it, but it should be accessible to host environment (obviously)
// and (less obviously) to host functions called from store - for calling wasm callbacks and propagating this config to them
// (or just complying with suspend conditions themselves)
// alternatively it could be passed to function handles and passend into function context
#[cfg(feature = "async")]
pub(crate) suspend_cond: SuspendConditions,
}

impl Debug for Store {
@@ -83,7 +94,14 @@ impl PartialEq for Store {
impl Default for Store {
fn default() -> Self {
let id = STORE_ID.fetch_add(1, Ordering::Relaxed);
Self { id, module_instances: Vec::new(), data: StoreData::default(), runtime: Runtime::Default }
Self {
id,
module_instances: Vec::new(),
data: StoreData::default(),
runtime: Runtime::Default,
#[cfg(feature = "async")]
suspend_cond: SuspendConditions::default(),
}
}
}

@@ -476,3 +494,21 @@ fn get_pair_mut<T>(slice: &mut [T], i: usize, j: usize) -> Option<(&mut T, &mut
let pair = if i < j { (&mut x[0], &mut y[0]) } else { (&mut y[0], &mut x[0]) };
Some(pair)
}

// suspend_conditions-related functions
#[cfg(feature = "async")]
impl Store {
/// sets suspend conditions for store
pub fn set_suspend_conditions(&mut self, val: SuspendConditions) {
self.suspend_cond = val;
}
/// gets suspend conditions of store
pub fn get_suspend_conditions(&self) -> &SuspendConditions {
&self.suspend_cond
}
/// transforms suspend conditions for store using user-provided function
pub fn update_suspend_conditions(&mut self, mapper: impl FnOnce(SuspendConditions) -> SuspendConditions) {
let temp = core::mem::take(&mut self.suspend_cond);
self.suspend_cond = mapper(temp);
}
}
96 changes: 96 additions & 0 deletions crates/tinywasm/src/store/suspend_conditions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#![cfg(feature = "async")]

use crate::store::Store;
use alloc::boxed::Box;
use core::fmt::Debug;
use core::ops::ControlFlow;

/// user callback for use in [SuspendConditions::suspend_cb]
pub type ShouldSuspendCb = Box<dyn FnMut(&Store) -> ControlFlow<(), ()>>;

/// used to limit execution time wasm code takes
#[derive(Default)]
#[non_exhaustive] // some fields are feature-gated, use with*-methods to construct
pub struct SuspendConditions {
/// atomic flag. when set to true it means execution should suspend
/// can be used to tell executor to stop from another thread
pub suspend_flag: Option<alloc::sync::Arc<core::sync::atomic::AtomicBool>>,

/// instant at which execution should suspend
/// can be used to control how much time will be spent in wasm without requiring other threads
/// such as for time-slice multitasking
/// uses rust standard library for checking time - so not available in no-std
#[cfg(feature = "std")]
pub timeout_instant: Option<crate::std::time::Instant>,

/// callback that returns [`ControlFlow::Break`]` when execution should suspend
/// can be used when above ways are insufficient or
/// instead of [`timeout_instant`] in no-std builds, with your own clock function
pub suspend_cb: Option<ShouldSuspendCb>,
}

impl Debug for SuspendConditions {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let stop_cb_text = if self.suspend_cb.is_some() { "<present>" } else { "<not present>" };
let mut f = f.debug_struct("SuspendConditions");
f.field("stop_flag", &self.suspend_flag);
#[cfg(feature = "std")]
{
f.field("timeout_instant", &self.timeout_instant);
}
f.field("stop_cb", &stop_cb_text).finish()
}
}

impl SuspendConditions {
/// creates suspend_conditions with every condition unset
pub fn new() -> Self {
Default::default()
}

/// sets timeout_instant to `how_long` from now
#[cfg(feature = "std")]
pub fn set_timeout_in(&mut self, how_long: crate::std::time::Duration) -> &mut Self {
self.timeout_instant = Some(crate::std::time::Instant::now() + how_long);
self
}
/// adds timeout at specified instant
#[cfg(feature = "std")]
pub fn with_timeout_at(self, when: crate::std::time::Instant) -> Self {
Self { timeout_instant: Some(when), ..self }
}
/// adds timeout in specified duration
#[cfg(feature = "std")]
pub fn with_timeout_in(self, how_long: crate::std::time::Duration) -> Self {
Self { timeout_instant: Some(crate::std::time::Instant::now() + how_long), ..self }
}
/// removes timeout
pub fn without_timeout(self) -> Self {
#[cfg(feature = "std")]
{
Self { timeout_instant: None, ..self }
}
#[cfg(not(feature = "std"))]
{
self
}
}

/// adds susped flag
pub fn with_suspend_flag(self, should_suspend: alloc::sync::Arc<core::sync::atomic::AtomicBool>) -> Self {
Self { suspend_flag: Some(should_suspend), ..self }
}
/// removes susped flag
pub fn without_suspend_flag(self) -> Self {
Self { suspend_flag: None, ..self }
}

/// adds suspend callback
pub fn with_suspend_callback(self, cb: ShouldSuspendCb) -> Self {
Self { suspend_cb: Some(cb), ..self }
}
/// removes suspend callback
pub fn without_suspend_callback(self) -> Self {
Self { suspend_cb: None, ..self }
}
}
115 changes: 114 additions & 1 deletion crates/tinywasm/tests/testsuite/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::hash::Hasher;
use std::panic::{self, AssertUnwindSafe};

use eyre::{bail, eyre, Result};
#[cfg(feature = "test_async")]
use tinywasm::{CoroState, SuspendConditions, SuspendReason};
use tinywasm_types::{ExternRef, FuncRef, ModuleInstanceAddr, TinyWasmModule, ValType, WasmValue};
use wasm_testsuite::wast;
use wasm_testsuite::wast::{core::AbstractHeapType, QuoteWat};
@@ -12,6 +15,25 @@ pub fn try_downcast_panic(panic: Box<dyn std::any::Any + Send>) -> String {
info.unwrap_or(info_str.unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())).to_string())
}

// due to imprecision it's not exact
#[cfg(feature = "test_async")]
fn make_sometimes_breaking_cb(probability: f64) -> impl FnMut(&tinywasm::Store) -> std::ops::ControlFlow<(), ()> {
let mut counter = 0 as u64;
let mut hasher = std::hash::DefaultHasher::new();
let threshhold = (probability * (u64::MAX as f64)) as u64; // 2 lossy conversions

move |_| {
hasher.write_u64(counter);
counter += 1;
if hasher.finish() < threshhold {
std::ops::ControlFlow::Break(())
} else {
std::ops::ControlFlow::Continue(())
}
}
}

#[cfg(not(feature = "test_async"))]
pub fn exec_fn_instance(
instance: Option<&ModuleInstanceAddr>,
store: &mut tinywasm::Store,
@@ -30,6 +52,50 @@ pub fn exec_fn_instance(
func.call(store, args)
}

#[cfg(feature = "test_async")]
pub fn exec_fn_instance(
instance: Option<&ModuleInstanceAddr>,
store: &mut tinywasm::Store,
name: &str,
args: &[tinywasm_types::WasmValue],
) -> Result<Vec<tinywasm_types::WasmValue>, tinywasm::Error> {
let Some(instance) = instance else {
return Err(tinywasm::Error::Other("no instance found".to_string()));
};

let mut prev_reason = None;
store.update_suspend_conditions(|old_cond| {
prev_reason = Some(old_cond);
SuspendConditions::new().with_suspend_callback(Box::new(make_sometimes_breaking_cb(2.0 / 3.0)))
});
let res = || -> Result<Vec<tinywasm_types::WasmValue>, tinywasm::Error> {
let Some(instance) = store.get_module_instance(*instance) else {
return Err(tinywasm::Error::Other("no instance found".to_string()));
};

let func = instance.exported_func_untyped(store, name)?;
let mut state = match func.call_coro(store, args)? {
tinywasm::PotentialCoroCallResult::Return(val) => return Ok(val),
tinywasm::PotentialCoroCallResult::Suspended(suspend_reason, state) => {
assert!(matches!(suspend_reason, SuspendReason::SuspendedCallback));
state
}
};
loop {
match state.resume(store, None)? {
tinywasm::CoroStateResumeResult::Return(val) => return Ok(val),
tinywasm::CoroStateResumeResult::Suspended(suspend_reason) => {
assert!(matches!(suspend_reason, SuspendReason::SuspendedCallback))
}
}
}
}();
// restore store suspend conditions before returning error or success
store.set_suspend_conditions(prev_reason.unwrap());
res
}

#[cfg(not(feature = "test_async"))]
pub fn exec_fn(
module: Option<&TinyWasmModule>,
name: &str,
@@ -39,13 +105,60 @@ pub fn exec_fn(
let Some(module) = module else {
return Err(tinywasm::Error::Other("no module found".to_string()));
};

let mut store = tinywasm::Store::new();
let module = tinywasm::Module::from(module);
let instance = module.instantiate(&mut store, imports)?;
instance.exported_func_untyped(&store, name)?.call(&mut store, args)
}

#[cfg(feature = "test_async")]
pub fn exec_fn(
module: Option<&TinyWasmModule>,
name: &str,
args: &[tinywasm_types::WasmValue],
imports: Option<tinywasm::Imports>,
) -> Result<Vec<tinywasm_types::WasmValue>, tinywasm::Error> {
let Some(module) = module else {
return Err(tinywasm::Error::Other("no module found".to_string()));
};

let mut store = tinywasm::Store::new();

store.set_suspend_conditions(
SuspendConditions::new().with_suspend_callback(Box::new(make_sometimes_breaking_cb(2.0 / 3.0))),
);

let module = tinywasm::Module::from(module);
let instance = match module.instantiate_coro(&mut store, imports)? {
tinywasm::PotentialCoroCallResult::Return(res) => res,
tinywasm::PotentialCoroCallResult::Suspended(suspend_reason, mut state) => loop {
assert!(matches!(suspend_reason, SuspendReason::SuspendedCallback));
match state.resume(&mut store, None)? {
tinywasm::CoroStateResumeResult::Return(res) => break res,
tinywasm::CoroStateResumeResult::Suspended(suspend_reason) => {
assert!(matches!(suspend_reason, SuspendReason::SuspendedCallback));
}
}
},
};

let mut state = match instance.exported_func_untyped(&store, name)?.call_coro(&mut store, args)? {
tinywasm::PotentialCoroCallResult::Return(r) => return Ok(r),
tinywasm::PotentialCoroCallResult::Suspended(suspend_reason, state) => {
assert!(matches!(suspend_reason, SuspendReason::SuspendedCallback));
state
}
};
loop {
match state.resume(&mut store, None)? {
tinywasm::CoroStateResumeResult::Return(res) => return Ok(res),
tinywasm::CoroStateResumeResult::Suspended(suspend_reason) => {
assert!(matches!(suspend_reason, SuspendReason::SuspendedCallback))
}
}
}
}

pub fn catch_unwind_silent<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> {
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
374 changes: 374 additions & 0 deletions crates/tinywasm/tests/wasm_resume.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
#![cfg(feature = "async")]

use core::panic;
use eyre;
use std::sync;
use std::{ops::ControlFlow, time::Duration};
use tinywasm::{
CoroState, CoroStateResumeResult, Extern, Imports, Module, ModuleInstance, PotentialCoroCallResult, Store,
SuspendConditions, SuspendReason,
};
use wat;

#[test]
fn main() -> std::result::Result<(), eyre::Report> {
println!("\n# testing with callback");
let mut cb_cond = |store: &mut Store| {
let callback = make_suspend_in_time_cb(30);
store.set_suspend_conditions(SuspendConditions::new().with_suspend_callback(Box::new(callback)));
};
suspend_with_pure_loop(&mut cb_cond, SuspendReason::SuspendedCallback)?;
suspend_with_wasm_fn(&mut cb_cond, SuspendReason::SuspendedCallback)?;
suspend_with_host_fn(&mut cb_cond, SuspendReason::SuspendedCallback)?;

println!("\n# testing with epoch");
let mut time_cond = |store: &mut Store| {
store.set_suspend_conditions(SuspendConditions::new().with_timeout_in(Duration::from_millis(10)))
};
suspend_with_pure_loop(&mut time_cond, SuspendReason::SuspendedEpoch)?;
suspend_with_wasm_fn(&mut time_cond, SuspendReason::SuspendedEpoch)?;
suspend_with_host_fn(&mut time_cond, SuspendReason::SuspendedEpoch)?;

println!("\n# testing atomic bool");
let mut cb_thead = |store: &mut Store| {
let arc = sync::Arc::<sync::atomic::AtomicBool>::new(sync::atomic::AtomicBool::new(false));
store.set_suspend_conditions(SuspendConditions::new().with_suspend_flag(arc.clone()));
let handle = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(10));
arc.store(true, sync::atomic::Ordering::Release);
});
drop(handle);
};
suspend_with_pure_loop(&mut cb_thead, SuspendReason::SuspendedFlag)?;
suspend_with_wasm_fn(&mut cb_thead, SuspendReason::SuspendedFlag)?;
suspend_with_host_fn(&mut cb_thead, SuspendReason::SuspendedFlag)?;

Ok(())
}

fn make_suspend_in_time_cb(milis: u64) -> impl FnMut(&Store) -> ControlFlow<(), ()> {
let mut counter = 0 as u64;
move |_| -> ControlFlow<(), ()> {
counter += 1;
if counter > milis {
counter = 0;
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}

fn try_compare(lhs: &SuspendReason, rhs: &SuspendReason) -> eyre::Result<bool> {
Ok(match lhs {
SuspendReason::Yield(_) => eyre::bail!("Can't compare yields"),
SuspendReason::SuspendedEpoch => matches!(rhs, SuspendReason::SuspendedEpoch),
SuspendReason::SuspendedCallback => matches!(rhs, SuspendReason::SuspendedCallback),
SuspendReason::SuspendedFlag => matches!(rhs, SuspendReason::SuspendedFlag),
_ => eyre::bail!("unimplemented new variant"),
})
}

// check if you can suspend while looping
fn suspend_with_pure_loop(
set_cond: &mut impl FnMut(&mut Store) -> (),
expected_reason: SuspendReason,
) -> eyre::Result<()> {
println!("## test suspend in loop");

let wasm: String = {
let detect_overflow = overflow_detect_snippet("$res");
format!(
r#"(module
(memory $mem 1)
(export "memory" (memory $mem)) ;; first 8 bytes - counter, next 4 - overflow flag
(func (export "start_counter")
(local $res i64)
(loop $lp
(i32.const 0) ;;where to store
(i64.load $mem (i32.const 0))
(i64.const 1)
(i64.add)
(local.set $res)
(local.get $res)
(i64.store $mem)
{detect_overflow}
(br $lp)
)
)
)"#
)
.into()
};

let mut tested = {
let wasm = wat::parse_str(wasm)?;
let module = Module::parse_bytes(&wasm)?;
let mut store = Store::default();
let instance = module.instantiate(&mut store, None)?;
TestedModule { store, instance: instance, resumable: None }
};

let increases = run_loops_look_at_numbers(&mut tested, set_cond, expected_reason, 16)?;
assert!(increases > 2, "code doesn't enough: either suspend condition is too tight or something is broken");
Ok(())
}

// check if you can suspend when calling wasm function
fn suspend_with_wasm_fn(
set_cond: &mut impl FnMut(&mut Store) -> (),
expected_reason: SuspendReason,
) -> eyre::Result<()> {
println!("## test suspend wasm fn");

let wasm: String = {
let detect_overflow = overflow_detect_snippet("$res");
format!(
r#"(module
(memory $mem 1)
(export "memory" (memory $mem)) ;; first 8 bytes - counter, next 8 - overflow counter
(func $wasm_nop
nop
)
(func $wasm_adder (param i64 i64) (result i64)
(local.get 0)
(local.get 1)
(i64.add)
)
(func $overflow_detect (param $res i64)
{detect_overflow}
)
(func (export "start_counter")
(local $res i64)
(loop $lp
(call $wasm_nop)
(i32.const 0) ;;where to store
(i64.load $mem (i32.const 0))
(i64.const 1)
(call $wasm_adder)
(local.set $res)
(call $wasm_nop)
(local.get $res)
(i64.store $mem)
(local.get $res)
(call $overflow_detect)
(call $wasm_nop)
(br $lp)
)
)
)"#
)
.into()
};

let mut tested = {
let wasm = wat::parse_str(wasm)?;
let module = Module::parse_bytes(&wasm)?;
let mut store = Store::default();
let instance = module.instantiate(&mut store, None)?;
TestedModule { store, instance: instance, resumable: None }
};

let increases = run_loops_look_at_numbers(&mut tested, set_cond, expected_reason, 16)?;
assert!(increases > 2, "code doesn't enough: either suspend condition is too tight or something is broken");

Ok(())
}

// check if you can suspend when calling host function
fn suspend_with_host_fn(
set_cond: &mut impl FnMut(&mut Store) -> (),
expected_reason: SuspendReason,
) -> eyre::Result<()> {
println!("## test suspend host fn");

let wasm: String = {
format!(
r#"(module
(import "host" "adder" (func $host_adder (param i64 i64)(result i64)))
(import "host" "nop" (func $host_nop))
(import "host" "detect" (func $overflow_detect (param $res i64)))
(memory $mem 1)
(export "memory" (memory $mem)) ;; first 8 bytes - counter, next 8 - overflow counter
(func (export "start_counter")
(local $res i64)
(loop $lp
(call $host_nop)
(i32.const 0) ;;where to store
(i64.load $mem (i32.const 0))
(i64.const 1)
(call $host_adder)
(local.set $res)
(call $host_nop)
(local.get $res)
(i64.store $mem)
(local.get $res)
(call $overflow_detect)
(call $host_nop)
(br $lp)
)
)
)"#
)
.into()
};

let mut tested = {
let wasm = wat::parse_str(wasm)?;
let module = Module::parse_bytes(&wasm)?;
let mut store = Store::default();
let mut imports = Imports::new();
imports.define(
"host",
"adder",
Extern::typed_func(|_, args: (i64, i64)| -> tinywasm::Result<i64> { Ok(args.0 + args.1) }),
)?;
imports.define(
"host",
"nop",
Extern::typed_func(|_, ()| -> tinywasm::Result<()> {
std::thread::sleep(Duration::from_micros(1));
Ok(())
}),
)?;
imports.define(
"host",
"detect",
Extern::typed_func(|mut ctx, arg: i64| -> tinywasm::Result<()> {
if arg != 0 {
return Ok(());
}
let mut mem = ctx.module().exported_memory_mut(ctx.store_mut(), "memory").expect("where's memory");
let mut buf = [0 as u8; 8];
buf.copy_from_slice(mem.load(8, 8)?);
let counter = i64::from_be_bytes(buf);
mem.store(8, 8, &i64::to_be_bytes(counter + 1))?;
Ok(())
}),
)?;

let instance = module.instantiate(&mut store, Some(imports))?;
TestedModule { store, instance: instance, resumable: None }
};

let increases = run_loops_look_at_numbers(&mut tested, set_cond, expected_reason, 16)?;
assert!(increases > 2, "code doesn't enough: either suspend condition is too tight or something is broken");
Ok(())
}

fn run_loops_look_at_numbers(
tested: &mut TestedModule,
set_cond: &mut impl FnMut(&mut Store) -> (),
expected_reason: SuspendReason,
times: u32,
) -> eyre::Result<u32> {
set_cond(&mut tested.store);
let suspend = tested.start_counter_incrementing_loop("start_counter")?;
assert!(try_compare(&suspend, &expected_reason).expect("unexpected yield"));

let mut prev_counter = tested.get_counter();
let mut times_increased = 0 as u32;

{
let (big, small) = prev_counter;
println!("after start {big} {small}");
}

assert!(prev_counter >= (0, 0));

for _ in 0..times - 1 {
set_cond(&mut tested.store);
assert!(try_compare(&tested.continue_counter_incrementing_loop()?, &expected_reason)?);
let new_counter = tested.get_counter();
// save for scheduling weirdness, loop should run for a bunch of times in 3ms
assert!(new_counter >= prev_counter);
{
let (big, small) = new_counter;
println!("after continue {big} {small}");
}
if new_counter > prev_counter {
times_increased += 1;
}
prev_counter = new_counter;
}
Ok(times_increased)
}

fn overflow_detect_snippet(var: &str) -> String {
format!(
r#"(i64.eq (i64.const 0) (local.get {var}))
(if
(then
;; we wrapped around back to 0 - set flag
(i32.const 8) ;;where to store
(i32.const 8) ;;where to load
(i64.load)
(i64.const 1)
(i64.add)
(i64.store $mem)
)
(else
nop
)
)
"#
)
.into()
}

// should have exported memory "memory" and
struct TestedModule {
store: Store,
instance: ModuleInstance,
resumable: Option<tinywasm::SuspendedFunc>,
}

impl TestedModule {
fn start_counter_incrementing_loop(&mut self, fn_name: &str) -> tinywasm::Result<SuspendReason> {
let starter = self.instance.exported_func_untyped(&self.store, fn_name)?;
if let PotentialCoroCallResult::Suspended(res, coro) = starter.call_coro(&mut self.store, &[])? {
self.resumable = Some(coro);
return Ok(res);
} else {
panic!("that should never return");
}
}

fn continue_counter_incrementing_loop(&mut self) -> tinywasm::Result<SuspendReason> {
let paused = if let Some(val) = self.resumable.as_mut() {
val
} else {
panic!("nothing to continue");
};
let resume_res = (*paused).resume(&mut self.store, None)?;
match resume_res {
CoroStateResumeResult::Suspended(res) => Ok(res),
CoroStateResumeResult::Return(_) => panic!("should never return"),
}
}

// (counter, overflow flag)
fn get_counter(&self) -> (u64, u64) {
let counter_now = {
let mem = self.instance.exported_memory(&self.store, "memory").expect("where's memory");
let mut buff: [u8; 8] = [0; 8];
let in_mem = mem.load(0, 8).expect("where's memory");
buff.clone_from_slice(in_mem);
u64::from_le_bytes(buff)
};
let overflow_times = {
let mem = self.instance.exported_memory(&self.store, "memory").expect("where's memory");
let mut buff: [u8; 8] = [0; 8];
let in_mem = mem.load(8, 8).expect("where's memory");
buff.clone_from_slice(in_mem);
u64::from_le_bytes(buff)
};
(overflow_times, counter_now)
}
}
5 changes: 4 additions & 1 deletion crates/types/src/lib.rs
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
extern crate alloc;
use alloc::boxed::Box;
use core::{fmt::Debug, ops::Range};
use core::{any::Any, fmt::Debug, ops::Range};

// Memory defaults
const MEM_PAGE_SIZE: u64 = 65536;
@@ -408,3 +408,6 @@ pub enum ElementItem {
Func(FuncAddr),
Expr(ConstInstruction),
}

pub type YieldedValue = Option<Box<dyn Any>>;
pub type ResumeArgument = Option<Box<dyn Any>>;
156 changes: 156 additions & 0 deletions examples/host_coro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
use eyre::{self, bail};
use tinywasm::{
types::{FuncType, ValType, WasmValue},
CoroState, CoroStateResumeResult, Extern, FuncContext, HostCoroState, Imports, Module, PotentialCoroCallResult,
Store, SuspendReason,
};
use wat;

fn main() -> eyre::Result<()> {
untyped()?;
typed()?;
Ok(())
}

const WASM: &str = r#"(module
(import "host" "hello" (func $host_hello (param i32)))
(import "host" "wait" (func $host_suspend (param i32)(result i32)))
(func (export "call_hello") (result f32)
(call $host_hello (i32.const -3))
(call $host_suspend (i32.const 10))
(call $host_hello)
(f32.const 6.28)
)
)
"#;

#[derive(Debug)]
struct MyUserData {
magic: u16,
}

#[derive(Debug)]
struct MySuspendedState {
base: i32,
}
impl<'_> CoroState<Vec<WasmValue>, FuncContext<'_>> for MySuspendedState {
fn resume(
&mut self,
_: FuncContext<'_>,
arg: tinywasm::types::ResumeArgument,
) -> tinywasm::Result<tinywasm::CoroStateResumeResult<Vec<WasmValue>>> {
let val = arg.expect("you din't send").downcast::<i32>().expect("you sent wrong");
return Ok(CoroStateResumeResult::Return(vec![WasmValue::I32(*val + self.base)]));
}
}

fn untyped() -> eyre::Result<()> {
let wasm = wat::parse_str(WASM).expect("failed to parse wat");
let module = Module::parse_bytes(&wasm)?;
let mut store = Store::default();

let mut imports = Imports::new();
imports.define(
"host",
"hello",
Extern::func(&FuncType { params: Box::new([ValType::I32]), results: Box::new([]) }, |_: FuncContext<'_>, x| {
x.first().map(|x| println!("{:?}", x));
Ok(vec![])
}),
)?;
let my_coro_starter = |_ctx: FuncContext<'_>,
vals: &[WasmValue]|
-> tinywasm::Result<PotentialCoroCallResult<Vec<WasmValue>, Box<dyn HostCoroState>>> {
let base = if let WasmValue::I32(v) = vals.first().expect("wrong args") { v } else { panic!("wrong arg") };
let coro = Box::new(MySuspendedState { base: *base });
return Ok(PotentialCoroCallResult::Suspended(
SuspendReason::make_yield::<MyUserData>(MyUserData { magic: 42 }),
coro,
));
};
imports.define(
"host",
"wait",
Extern::func_coro(
&FuncType { params: Box::new([ValType::I32]), results: Box::new([ValType::I32]) },
my_coro_starter,
),
)?;

let instance = module.instantiate(&mut store, Some(imports))?;

let greeter = instance.exported_func_untyped(&store, "call_hello")?;
let call_res = greeter.call_coro(&mut store, &[])?;
let mut resumable = match call_res {
tinywasm::PotentialCoroCallResult::Return(..) => bail!("it's not supposed to return yet"),
tinywasm::PotentialCoroCallResult::Suspended(SuspendReason::Yield(Some(val)), resumable) => {
match val.downcast::<MyUserData>() {
Ok(val) => assert_eq!(val.magic, 42),
Err(_) => bail!("invalid yielded val"),
}
resumable
}
tinywasm::PotentialCoroCallResult::Suspended(..) => bail!("wrong suspend"),
};

let final_res = resumable.resume(&mut store, Some(Box::<i32>::new(7)))?;
if let CoroStateResumeResult::Return(vals) = final_res {
println!("{:?}", vals.first().unwrap());
} else {
panic!("should have finished");
}

Ok(())
}

fn typed() -> eyre::Result<()> {
let wasm = wat::parse_str(WASM).expect("failed to parse wat");
let module = Module::parse_bytes(&wasm)?;
let mut store = Store::default();

let mut imports = Imports::new();
imports.define(
"host",
"hello",
Extern::typed_func(|_: FuncContext<'_>, x: i32| {
println!("{x}");
Ok(())
}),
)?;
let my_coro_starter =
|_ctx: FuncContext<'_>, base: i32| -> tinywasm::Result<PotentialCoroCallResult<i32, Box<dyn HostCoroState>>> {
let coro = Box::new(MySuspendedState { base: base });
return Ok(PotentialCoroCallResult::Suspended(
SuspendReason::make_yield::<MyUserData>(MyUserData { magic: 42 }),
coro,
));
};
imports.define("host", "wait", Extern::typed_func_coro(my_coro_starter))?;

let instance = module.instantiate(&mut store, Some(imports))?;

let greeter = instance.exported_func::<(), f32>(&store, "call_hello")?;
let call_res = greeter.call_coro(&mut store, ())?;
let mut resumable = match call_res {
tinywasm::PotentialCoroCallResult::Return(..) => bail!("it's not supposed to return yet"),
tinywasm::PotentialCoroCallResult::Suspended(SuspendReason::Yield(Some(val)), resumable) => {
match val.downcast::<MyUserData>() {
Ok(val) => assert_eq!(val.magic, 42),
Err(_) => bail!("invalid yielded val"),
}
resumable
}
tinywasm::PotentialCoroCallResult::Suspended(..) => bail!("wrong suspend"),
};

let final_res = resumable.resume(&mut store, Some(Box::<i32>::new(7)))?;

if let CoroStateResumeResult::Return(res) = final_res {
println!("{res}");
} else {
panic!("should have returned");
}

Ok(())
}

Unchanged files with check annotations Beta

pub(super) trait NoStdFloatExt {
fn round(self) -> Self;
fn abs(self) -> Self;

Check warning on line 3 in crates/tinywasm/src/interpreter/no_std_floats.rs

GitHub Actions / Build wasm

methods `abs`, `signum`, and `copysign` are never used
fn signum(self) -> Self;
fn ceil(self) -> Self;
fn floor(self) -> Self;