use core::slice;
use wasmi::{
AsContext,
AsContextMut,
Caller,
Config,
Engine,
Error,
Extern,
Func,
Linker,
Module,
ResumableCall,
ResumableCallHostTrap,
Store,
TrapCode,
TypedFunc,
TypedResumableCall,
TypedResumableCallHostTrap,
Val,
ValType,
errors::ErrorKind,
};
fn test_setup(remaining: u32) -> (Store<TestData>, Linker<TestData>) {
let mut config = Config::default();
config.wasm_tail_call(true);
let engine = Engine::new(&config);
let store = Store::new(
&engine,
TestData {
_remaining: remaining,
},
);
let linker = <Linker<TestData>>::new(&engine);
(store, linker)
}
#[derive(Debug)]
pub struct TestData {
_remaining: u32,
}
fn resumable_call_smoldot_common(wasm: &str) -> (Store<TestData>, TypedFunc<(), i32>) {
let (mut store, mut linker) = test_setup(0);
linker
.func_wrap(
"env",
"host_fn",
|mut _caller: Caller<'_, TestData>| -> Result<i32, Error> { Err(Error::i32_exit(100)) },
)
.unwrap();
let module = Module::new(store.engine(), wasm).unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let wasm_fn = instance.get_typed_func::<(), i32>(&store, "test").unwrap();
(store, wasm_fn)
}
pub trait ResumableExt {
type Results;
fn assert_finished_with(self, check_results: impl FnOnce(Self::Results) -> bool);
fn unwrap_resumable_host_trap(self) -> TypedResumableCallHostTrap<Self::Results>;
}
impl<Results> ResumableExt for Result<TypedResumableCall<Results>, Error> {
type Results = Results;
fn assert_finished_with(self, check_results: impl FnOnce(Results) -> bool) {
match self.unwrap() {
TypedResumableCall::Finished(results) => {
assert!(check_results(results));
}
TypedResumableCall::HostTrap(_) => {
panic!(
"expected `TypedResumableCall::Finished` but found: `TypedResumableCall::HostTrap`"
)
}
TypedResumableCall::OutOfFuel(_) => {
panic!(
"expected `TypedResumableCall::Finished` but found: `TypedResumableCall::OutOfFuel`"
)
}
}
}
fn unwrap_resumable_host_trap(self) -> TypedResumableCallHostTrap<Self::Results> {
match self.unwrap() {
TypedResumableCall::HostTrap(invocation) => invocation,
TypedResumableCall::OutOfFuel(_) => {
panic!("expected resumable host trap but found: `TypedResumableCall::OutOfFuel`")
}
TypedResumableCall::Finished(_) => {
panic!("expected resumable host trap but found: `TypedResumableCall::Finished`")
}
}
}
}
#[test]
fn resumable_call_smoldot_01() {
let (mut store, wasm_fn) = resumable_call_smoldot_common(
r#"
(module
(import "env" "host_fn" (func $host_fn (result i32)))
(func (export "test") (result i32)
(call $host_fn)
)
)
"#,
);
let invocation = wasm_fn
.call_resumable(&mut store, ())
.unwrap_resumable_host_trap();
invocation
.resume(&mut store, &[Val::I32(42)])
.assert_finished_with(|result| result == 42);
}
#[test]
fn resumable_call_smoldot_tail_01() {
let (mut store, wasm_fn) = resumable_call_smoldot_common(
r#"
(module
(import "env" "host_fn" (func $host_fn (result i32)))
(func (export "test") (result i32)
(return_call $host_fn)
)
)
"#,
);
assert_eq!(
wasm_fn
.call_resumable(&mut store, ())
.unwrap_err()
.i32_exit_status(),
Some(100),
);
}
#[test]
fn resumable_call_smoldot_tail_02() {
let (mut store, wasm_fn) = resumable_call_smoldot_common(
r#"
(module
(import "env" "host_fn" (func $host (result i32)))
(func $wasm (result i32)
(return_call $host)
)
(func (export "test") (result i32)
(call $wasm)
)
)
"#,
);
let invocation = wasm_fn
.call_resumable(&mut store, ())
.unwrap_resumable_host_trap();
invocation
.resume(&mut store, &[Val::I32(42)])
.assert_finished_with(|result| result == 42);
}
#[test]
fn resumable_call_smoldot_02() {
let (mut store, wasm_fn) = resumable_call_smoldot_common(
r#"
(module
(import "env" "host_fn" (func $host_fn (result i32)))
(func (export "test") (result i32)
(if (result i32) (i32.ne (call $host_fn) (i32.const 0))
(then
(i32.const 11) ;; EXPECTED
)
(else
(i32.const 22) ;; FAILURE
)
)
)
)
"#,
);
let invocation = wasm_fn
.call_resumable(&mut store, ())
.unwrap_resumable_host_trap();
invocation
.resume(&mut store, &[Val::I32(42)])
.assert_finished_with(|result| result == 11);
}
#[test]
fn resumable_call_host() {
let (mut store, _linker) = test_setup(0);
let host_fn = Func::wrap(&mut store, || -> Result<(), Error> {
Err(Error::i32_exit(100))
});
let error = host_fn
.call_resumable(&mut store, &[], &mut [])
.unwrap_err();
match error.i32_exit_status() {
Some(100) => {}
_ => panic!("expected Wasm trap"),
}
let trap = host_fn
.typed::<(), ()>(&store)
.unwrap()
.call_resumable(&mut store, ())
.unwrap_err();
assert_eq!(trap.i32_exit_status(), Some(100));
}
#[test]
fn resumable_call() {
let (mut store, mut linker) = test_setup(0);
let host_fn = Func::wrap(&mut store, |input: i32| -> Result<i32, Error> {
match input {
1 => Err(Error::i32_exit(10)),
2 => Err(Error::i32_exit(20)),
n => Ok(n + 1),
}
});
linker.define("env", "host_fn", host_fn).unwrap();
let wasm = r#"
(module
(import "env" "host_fn" (func $host_fn (param i32) (result i32)))
(func (export "wasm_fn") (param $wasm_trap i32) (result i32)
(local $i i32)
(local.set $i (i32.const 0))
(local.set $i (call $host_fn (local.get $i))) ;; Ok
(local.set $i (call $host_fn (local.get $i))) ;; Trap::i32_exit(1)
(local.set $i (call $host_fn (local.get $i))) ;; Trap::i32_exit(2)
(local.set $i (call $host_fn (local.get $i))) ;; Ok
(if (i32.eq (local.get $wasm_trap) (i32.const 1))
(then unreachable) ;; trap in Wasm if $wasm_trap == 1
)
(local.get $i) ;; return i == 4
)
)
"#;
let module = Module::new(store.engine(), wasm).unwrap();
let instance = linker.instantiate_and_start(&mut store, &module).unwrap();
let wasm_fn = instance
.get_export(&store, "wasm_fn")
.and_then(Extern::into_func)
.unwrap();
run_test(wasm_fn, &mut store, false);
run_test(wasm_fn, &mut store, true);
run_test_typed(wasm_fn, &mut store, false);
run_test_typed(wasm_fn, &mut store, true);
}
trait AssertResumable {
type Results;
type Invocation;
fn assert_resumable(
self,
store: &Store<TestData>,
exit_status: i32,
host_results: &[ValType],
) -> Self::Invocation;
fn unwrap_finished(self) -> Self::Results;
}
impl AssertResumable for Result<ResumableCall, Error> {
type Results = ();
type Invocation = ResumableCallHostTrap;
fn assert_resumable(
self,
store: &Store<TestData>,
exit_status: i32,
host_results: &[ValType],
) -> Self::Invocation {
match self.unwrap() {
ResumableCall::HostTrap(invocation) => {
assert_eq!(invocation.host_error().i32_exit_status(), Some(exit_status));
assert_eq!(invocation.host_func().ty(store).results(), host_results,);
invocation
}
ResumableCall::OutOfFuel(_) => panic!("expected `HostTrap` but found: `OutOfFuel`"),
ResumableCall::Finished => panic!("expected `HostTrap` but found: `Finished`"),
}
}
fn unwrap_finished(self) -> Self::Results {
match self.unwrap() {
ResumableCall::Finished => (),
ResumableCall::HostTrap(_) => panic!("expected `Finished` but found: `HostTrap`"),
ResumableCall::OutOfFuel(_) => panic!("expected `Finished` but found: `OutOfFuel`"),
}
}
}
fn run_test(wasm_fn: Func, store: &mut Store<TestData>, wasm_trap: bool) {
let mut results = Val::I32(0);
let invocation = wasm_fn
.call_resumable(
store.as_context_mut(),
&[Val::I32(wasm_trap as i32)],
slice::from_mut(&mut results),
)
.assert_resumable(store, 10, &[ValType::I32]);
let invocation = invocation
.resume(
store.as_context_mut(),
&[Val::I32(2)],
slice::from_mut(&mut results),
)
.assert_resumable(store, 20, &[ValType::I32]);
let call = invocation.resume(store, &[Val::I32(3)], slice::from_mut(&mut results));
if wasm_trap {
match call.unwrap_err().kind() {
ErrorKind::TrapCode(trap) => {
assert!(matches!(trap, TrapCode::UnreachableCodeReached,));
}
_ => panic!("expected Wasm trap"),
}
} else {
call.unwrap_finished();
assert_eq!(results.i32(), Some(4));
}
}
impl<Results> AssertResumable for Result<TypedResumableCall<Results>, Error> {
type Results = Results;
type Invocation = TypedResumableCallHostTrap<Results>;
fn assert_resumable(
self,
store: &Store<TestData>,
exit_status: i32,
host_results: &[ValType],
) -> Self::Invocation {
match self.unwrap() {
TypedResumableCall::HostTrap(invocation) => {
assert_eq!(invocation.host_error().i32_exit_status(), Some(exit_status));
assert_eq!(invocation.host_func().ty(store).results(), host_results,);
invocation
}
TypedResumableCall::OutOfFuel(_) => {
panic!("expected `HostTrap` but found: `OutOfFuel`")
}
TypedResumableCall::Finished(_) => panic!("expected `HostTrap` but found: `Finished`"),
}
}
fn unwrap_finished(self) -> Self::Results {
match self.unwrap() {
TypedResumableCall::Finished(results) => results,
TypedResumableCall::HostTrap(_) => panic!("expected `Finished` but found: `HostTrap`"),
TypedResumableCall::OutOfFuel(_) => {
panic!("expected `Finished` but found: `OutOfFuel`")
}
}
}
}
fn run_test_typed(wasm_fn: Func, store: &mut Store<TestData>, wasm_trap: bool) {
let invocation = wasm_fn
.typed::<i32, i32>(store.as_context())
.unwrap()
.call_resumable(store.as_context_mut(), wasm_trap as i32)
.assert_resumable(store, 10, &[ValType::I32]);
let invocation = invocation
.resume(store.as_context_mut(), &[Val::I32(2)])
.assert_resumable(store, 20, &[ValType::I32]);
let call = invocation.resume(store, &[Val::I32(3)]);
if wasm_trap {
match call.unwrap_err().kind() {
ErrorKind::TrapCode(trap) => {
assert!(matches!(trap, TrapCode::UnreachableCodeReached,));
}
_ => panic!("expected Wasm trap"),
}
} else {
assert_eq!(call.unwrap_finished(), 4);
}
}