#![cfg(not(miri))]
use super::REALLOC_AND_FREE;
use crate::call_hook::{Context, State, sync_call_hook};
use std::future::Future;
use std::pin::Pin;
use std::task::{self, Poll};
use wasmtime::component::*;
use wasmtime::{CallHook, CallHookHandler, Engine, Result, Store, StoreContextMut, bail};
#[test]
fn call_wrapped_func() -> Result<()> {
let wat = r#"
(component
(import "f" (func $f))
(core func $f_lower
(canon lower (func $f))
)
(core module $m
(import "" "" (func $f))
(func $export
(call $f)
)
(export "export" (func $export))
)
(core instance $i (instantiate $m
(with "" (instance
(export "" (func $f_lower))
))
))
(func (export "export")
(canon lift
(core func $i "export")
)
)
)
"#;
let engine = Engine::default();
let component = Component::new(&engine, wat)?;
let mut linker = Linker::<State>::new(&engine);
linker
.root()
.func_wrap("f", |_, _: ()| -> Result<()> { Ok(()) })?;
let mut store = Store::new(&engine, State::default());
store.call_hook(sync_call_hook);
let inst = linker
.instantiate(&mut store, &component)
.expect("instantiate");
let export = inst
.get_typed_func::<(), ()>(&mut store, "export")
.expect("looking up `export`");
export.call(&mut store, ())?;
let s = store.into_data();
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
Ok(())
}
#[test]
fn call_func_with_realloc() -> Result<()> {
let wat = format!(
r#"(component
(core module $m
(memory (export "memory") 1)
(func (export "roundtrip") (param i32 i32) (result i32)
(local $base i32)
(local.set $base
(call $realloc
(i32.const 0)
(i32.const 0)
(i32.const 4)
(i32.const 8)))
(i32.store offset=0
(local.get $base)
(local.get 0))
(i32.store offset=4
(local.get $base)
(local.get 1))
(local.get $base)
)
{REALLOC_AND_FREE}
)
(core instance $i (instantiate $m))
(func (export "list8-to-str") (param "a" (list u8)) (result string)
(canon lift
(core func $i "roundtrip")
(memory $i "memory")
(realloc (func $i "realloc"))
)
)
)"#
);
let engine = Engine::default();
let component = Component::new(&engine, wat)?;
let linker = Linker::<State>::new(&engine);
let mut store = Store::new(&engine, State::default());
store.call_hook(sync_call_hook);
let inst = linker
.instantiate(&mut store, &component)
.expect("instantiate");
let export = inst
.get_typed_func::<(&[u8],), (WasmStr,)>(&mut store, "list8-to-str")
.expect("looking up `list8-to-str`");
let message = String::from("hello, world!");
let res = export.call(&mut store, (message.as_bytes(),))?.0;
let result = res.to_str(&store)?;
assert_eq!(&message, &result);
let s = store.into_data();
assert_eq!(s.calls_into_host, 0);
assert_eq!(s.returns_from_host, 0);
assert_eq!(s.calls_into_wasm, 2);
assert_eq!(s.returns_from_wasm, 2);
Ok(())
}
#[test]
fn call_func_with_post_return() -> Result<()> {
let wat = r#"
(component
(core module $m
(func (export "roundtrip"))
(func (export "post-return"))
)
(core instance $i (instantiate $m))
(func (export "export")
(canon lift
(core func $i "roundtrip")
(post-return (func $i "post-return"))
)
)
)"#;
let engine = Engine::default();
let component = Component::new(&engine, wat)?;
let linker = Linker::<State>::new(&engine);
let mut store = Store::new(&engine, State::default());
store.call_hook(sync_call_hook);
let inst = linker
.instantiate(&mut store, &component)
.expect("instantiate");
let export = inst
.get_typed_func::<(), ()>(&mut store, "export")
.expect("looking up `export`");
export.call(&mut store, ())?;
let s = store.into_data();
assert_eq!(s.calls_into_host, 0);
assert_eq!(s.returns_from_host, 0);
assert_eq!(s.calls_into_wasm, 2);
assert_eq!(s.returns_from_wasm, 2);
Ok(())
}
#[tokio::test]
async fn call_wrapped_async_func() -> Result<()> {
let wat = r#"
(component
(import "f" (func $f))
(core func $f_lower
(canon lower (func $f))
)
(core module $m
(import "" "" (func $f))
(func $export
(call $f)
)
(export "export" (func $export))
)
(core instance $i (instantiate $m
(with "" (instance
(export "" (func $f_lower))
))
))
(func (export "export")
(canon lift
(core func $i "export")
)
)
)
"#;
let engine = Engine::default();
let component = Component::new(&engine, wat)?;
let mut linker = Linker::<State>::new(&engine);
linker
.root()
.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;
let mut store = Store::new(&engine, State::default());
store.call_hook(sync_call_hook);
let inst = linker
.instantiate_async(&mut store, &component)
.await
.expect("instantiate");
let export = inst
.get_typed_func::<(), ()>(&mut store, "export")
.expect("looking up `export`");
export.call_async(&mut store, ()).await?;
let s = store.into_data();
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
Ok(())
}
#[test]
fn trapping() -> Result<()> {
const TRAP_IN_F: i32 = 0;
const TRAP_NEXT_CALL_HOST: i32 = 1;
const TRAP_NEXT_RETURN_HOST: i32 = 2;
const TRAP_NEXT_CALL_WASM: i32 = 3;
const TRAP_NEXT_RETURN_WASM: i32 = 4;
const DO_NOTHING: i32 = 5;
let engine = Engine::default();
let mut linker = Linker::<State>::new(&engine);
linker
.root()
.func_wrap("f", |mut store: _, (action,): (i32,)| -> Result<()> {
assert_eq!(store.data().context.last(), Some(&Context::Host));
assert_eq!(store.data().calls_into_host, store.data().calls_into_wasm);
match action {
TRAP_IN_F => bail!("trapping in f"),
TRAP_NEXT_CALL_HOST => store.data_mut().trap_next_call_host = true,
TRAP_NEXT_RETURN_HOST => store.data_mut().trap_next_return_host = true,
TRAP_NEXT_CALL_WASM => store.data_mut().trap_next_call_wasm = true,
TRAP_NEXT_RETURN_WASM => store.data_mut().trap_next_return_wasm = true,
_ => {} }
Ok(())
})?;
let wat = r#"
(component
(import "f" (func $f (param "action" s32)))
(core func $f_lower
(canon lower (func $f))
)
(core module $m
(import "" "" (func $f (param i32)))
(func $export (param i32)
(call $f (local.get 0))
)
(export "export" (func $export))
)
(core instance $i (instantiate $m
(with "" (instance
(export "" (func $f_lower))
))
))
(func (export "export") (param "action" s32)
(canon lift
(core func $i "export")
)
)
)
"#;
let component = Component::new(&engine, wat)?;
let run = |action: i32, again: bool| -> (State, Option<wasmtime::Error>) {
let mut store = Store::new(&engine, State::default());
store.call_hook(sync_call_hook);
let inst = linker
.instantiate(&mut store, &component)
.expect("instantiate");
let export = inst
.get_typed_func::<(i32,), ()>(&mut store, "export")
.expect("looking up `export`");
let mut r = export.call(&mut store, (action,));
if r.is_ok() && again {
r = export.call(&mut store, (action,));
}
(store.into_data(), r.err())
};
let (s, e) = run(DO_NOTHING, false);
assert!(e.is_none());
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
let (s, e) = run(DO_NOTHING, true);
assert!(e.is_none());
assert_eq!(s.calls_into_host, 2);
assert_eq!(s.returns_from_host, 2);
assert_eq!(s.calls_into_wasm, 2);
assert_eq!(s.returns_from_wasm, 2);
let (s, e) = run(TRAP_IN_F, false);
assert!(format!("{:?}", e.unwrap()).contains("trapping in f"));
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
let (s, e) = run(TRAP_NEXT_CALL_HOST, false);
assert!(e.is_none());
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
let (s, e) = run(TRAP_NEXT_CALL_HOST, true);
println!("{:?}", e.as_ref().unwrap());
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingHost"));
assert_eq!(s.calls_into_host, 2);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 2);
assert_eq!(s.returns_from_wasm, 2);
let (s, e) = run(TRAP_NEXT_RETURN_HOST, false);
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromHost"));
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
let (s, e) = run(TRAP_NEXT_CALL_WASM, false);
assert!(e.is_none());
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
let (s, e) = run(TRAP_NEXT_CALL_WASM, true);
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingWasm"));
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 2);
assert_eq!(s.returns_from_wasm, 1);
let (s, e) = run(TRAP_NEXT_RETURN_WASM, false);
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromWasm"));
assert_eq!(s.calls_into_host, 1);
assert_eq!(s.returns_from_host, 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 1);
Ok(())
}
#[tokio::test]
async fn timeout_async_hook() -> Result<()> {
struct HandlerR;
#[async_trait::async_trait]
impl CallHookHandler<State> for HandlerR {
async fn handle_call_event(
&self,
mut ctx: StoreContextMut<'_, State>,
ch: CallHook,
) -> Result<()> {
let obj = ctx.data_mut();
if obj.calls_into_host > 200 {
bail!("timeout");
}
match ch {
CallHook::CallingHost => obj.calls_into_host += 1,
CallHook::CallingWasm => obj.calls_into_wasm += 1,
CallHook::ReturningFromHost => obj.returns_from_host += 1,
CallHook::ReturningFromWasm => obj.returns_from_wasm += 1,
}
Ok(())
}
}
let wat = r#"
(component
(import "f" (func $f))
(core func $f_lower
(canon lower (func $f))
)
(core module $m
(import "" "" (func $f))
(func $export
(loop $start
(call $f)
(br $start))
)
(export "export" (func $export))
)
(core instance $i (instantiate $m
(with "" (instance
(export "" (func $f_lower))
))
))
(func (export "export")
(canon lift
(core func $i "export")
)
)
)
"#;
let engine = Engine::default();
let component = Component::new(&engine, wat)?;
let mut linker = Linker::<State>::new(&engine);
linker
.root()
.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;
let mut store = Store::new(&engine, State::default());
store.call_hook_async(HandlerR {});
let inst = linker
.instantiate_async(&mut store, &component)
.await
.expect("instantiate");
let export = inst
.get_typed_func::<(), ()>(&mut store, "export")
.expect("looking up `export`");
let r = export.call_async(&mut store, ()).await;
assert!(format!("{:?}", r.unwrap_err()).contains("timeout"));
let s = store.into_data();
assert!(s.calls_into_host > 1);
assert!(s.returns_from_host > 1);
assert_eq!(s.calls_into_wasm, 1);
assert_eq!(s.returns_from_wasm, 0);
Ok(())
}
#[tokio::test]
async fn drop_suspended_async_hook() -> Result<()> {
struct Handler;
#[async_trait::async_trait]
impl CallHookHandler<u32> for Handler {
async fn handle_call_event(
&self,
mut ctx: StoreContextMut<'_, u32>,
_ch: CallHook,
) -> Result<()> {
let state = ctx.data_mut();
assert_eq!(*state, 0);
*state += 1;
let _dec = Decrement(state);
for _ in 0..500 {
tokio::task::yield_now().await;
}
Ok(())
}
}
let wat = r#"
(component
(import "f" (func $f))
(core func $f_lower
(canon lower (func $f))
)
(core module $m
(import "" "" (func $f))
(func $export
(call $f)
)
(export "export" (func $export))
)
(core instance $i (instantiate $m
(with "" (instance
(export "" (func $f_lower))
))
))
(func (export "export")
(canon lift
(core func $i "export")
)
)
)
"#;
let engine = Engine::default();
let component = Component::new(&engine, wat)?;
let mut linker = Linker::<u32>::new(&engine);
linker.root().func_wrap_async("f", |mut store, _: ()| {
Box::new(async move {
let state = store.data_mut();
assert_eq!(*state, 0);
*state += 1;
let _dec = Decrement(state);
for _ in 0.. {
tokio::task::yield_now().await;
}
Ok(())
})
})?;
let mut store = Store::new(&engine, 0);
store.call_hook_async(Handler);
let inst = linker
.instantiate_async(&mut store, &component)
.await
.expect("instantiate");
let export = inst
.get_typed_func::<(), ()>(&mut store, "export")
.expect("looking up `export`");
PollNTimes {
future: Box::pin(export.call_async(&mut store, ())),
times: 200,
}
.await;
assert_eq!(*store.data(), 0);
return Ok(());
struct PollNTimes<F> {
future: F,
times: u32,
}
impl<F: Future + Unpin> Future for PollNTimes<F>
where
F::Output: std::fmt::Debug,
{
type Output = ();
fn poll(mut self: Pin<&mut Self>, task: &mut task::Context<'_>) -> Poll<()> {
for i in 0..self.times {
match Pin::new(&mut self.future).poll(task) {
Poll::Ready(v) => panic!("future should not be ready at {i}; result is {v:?}"),
Poll::Pending => {}
}
}
Poll::Ready(())
}
}
struct Decrement<'a>(&'a mut u32);
impl Drop for Decrement<'_> {
fn drop(&mut self) {
*self.0 -= 1;
}
}
}