Path: blob/main/tests/all/component_model/call_hook.rs
3069 views
#![cfg(not(miri))]12use super::REALLOC_AND_FREE;3use crate::call_hook::{Context, State, sync_call_hook};4use std::future::Future;5use std::pin::Pin;6use std::task::{self, Poll};7use wasmtime::component::*;8use wasmtime::{CallHook, CallHookHandler, Engine, Result, Store, StoreContextMut, bail};910// Crate a synchronous Func, call it directly:11#[test]12fn call_wrapped_func() -> Result<()> {13let wat = r#"14(component15(import "f" (func $f))1617(core func $f_lower18(canon lower (func $f))19)20(core module $m21(import "" "" (func $f))2223(func $export24(call $f)25)2627(export "export" (func $export))28)29(core instance $i (instantiate $m30(with "" (instance31(export "" (func $f_lower))32))33))34(func (export "export")35(canon lift36(core func $i "export")37)38)39)40"#;4142let engine = Engine::default();43let component = Component::new(&engine, wat)?;4445let mut linker = Linker::<State>::new(&engine);46linker47.root()48.func_wrap("f", |_, _: ()| -> Result<()> { Ok(()) })?;4950let mut store = Store::new(&engine, State::default());51store.call_hook(sync_call_hook);52let inst = linker53.instantiate(&mut store, &component)54.expect("instantiate");5556let export = inst57.get_typed_func::<(), ()>(&mut store, "export")58.expect("looking up `export`");5960export.call(&mut store, ())?;6162let s = store.into_data();63assert_eq!(s.calls_into_host, 1);64assert_eq!(s.returns_from_host, 1);65assert_eq!(s.calls_into_wasm, 1);66assert_eq!(s.returns_from_wasm, 1);6768Ok(())69}7071// Call a func that turns a `list<u8>` into a `string`, to ensure that `realloc` calls are counted.72#[test]73fn call_func_with_realloc() -> Result<()> {74let wat = format!(75r#"(component76(core module $m77(memory (export "memory") 1)78(func (export "roundtrip") (param i32 i32) (result i32)79(local $base i32)80(local.set $base81(call $realloc82(i32.const 0)83(i32.const 0)84(i32.const 4)85(i32.const 8)))86(i32.store offset=087(local.get $base)88(local.get 0))89(i32.store offset=490(local.get $base)91(local.get 1))92(local.get $base)93)9495{REALLOC_AND_FREE}96)97(core instance $i (instantiate $m))9899(func (export "list8-to-str") (param "a" (list u8)) (result string)100(canon lift101(core func $i "roundtrip")102(memory $i "memory")103(realloc (func $i "realloc"))104)105)106)"#107);108109let engine = Engine::default();110let component = Component::new(&engine, wat)?;111let linker = Linker::<State>::new(&engine);112let mut store = Store::new(&engine, State::default());113store.call_hook(sync_call_hook);114let inst = linker115.instantiate(&mut store, &component)116.expect("instantiate");117118let export = inst119.get_typed_func::<(&[u8],), (WasmStr,)>(&mut store, "list8-to-str")120.expect("looking up `list8-to-str`");121122let message = String::from("hello, world!");123let res = export.call(&mut store, (message.as_bytes(),))?.0;124let result = res.to_str(&store)?;125assert_eq!(&message, &result);126127// There are two wasm calls for the `list8-to-str` call and the guest realloc call for the list128// argument.129let s = store.into_data();130assert_eq!(s.calls_into_host, 0);131assert_eq!(s.returns_from_host, 0);132assert_eq!(s.calls_into_wasm, 2);133assert_eq!(s.returns_from_wasm, 2);134135Ok(())136}137138// Call a guest function that also defines a post-return.139#[test]140fn call_func_with_post_return() -> Result<()> {141let wat = r#"142(component143(core module $m144(func (export "roundtrip"))145(func (export "post-return"))146)147(core instance $i (instantiate $m))148149(func (export "export")150(canon lift151(core func $i "roundtrip")152(post-return (func $i "post-return"))153)154)155)"#;156157let engine = Engine::default();158let component = Component::new(&engine, wat)?;159let linker = Linker::<State>::new(&engine);160let mut store = Store::new(&engine, State::default());161store.call_hook(sync_call_hook);162let inst = linker163.instantiate(&mut store, &component)164.expect("instantiate");165166let export = inst167.get_typed_func::<(), ()>(&mut store, "export")168.expect("looking up `export`");169170export.call(&mut store, ())?;171172// There are no host calls in this example, but the post-return does increment the count of173// wasm calls by 1, putting the total number of wasm calls at 2.174let s = store.into_data();175assert_eq!(s.calls_into_host, 0);176assert_eq!(s.returns_from_host, 0);177assert_eq!(s.calls_into_wasm, 2);178assert_eq!(s.returns_from_wasm, 2);179180Ok(())181}182183// Create an async Func, call it directly:184#[tokio::test]185async fn call_wrapped_async_func() -> Result<()> {186let wat = r#"187(component188(import "f" (func $f))189190(core func $f_lower191(canon lower (func $f))192)193(core module $m194(import "" "" (func $f))195196(func $export197(call $f)198)199200(export "export" (func $export))201)202(core instance $i (instantiate $m203(with "" (instance204(export "" (func $f_lower))205))206))207(func (export "export")208(canon lift209(core func $i "export")210)211)212)213"#;214215let engine = Engine::default();216217let component = Component::new(&engine, wat)?;218219let mut linker = Linker::<State>::new(&engine);220linker221.root()222.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;223224let mut store = Store::new(&engine, State::default());225store.call_hook(sync_call_hook);226227let inst = linker228.instantiate_async(&mut store, &component)229.await230.expect("instantiate");231232let export = inst233.get_typed_func::<(), ()>(&mut store, "export")234.expect("looking up `export`");235236export.call_async(&mut store, ()).await?;237238let s = store.into_data();239assert_eq!(s.calls_into_host, 1);240assert_eq!(s.returns_from_host, 1);241assert_eq!(s.calls_into_wasm, 1);242assert_eq!(s.returns_from_wasm, 1);243244Ok(())245}246247#[test]248fn trapping() -> Result<()> {249const TRAP_IN_F: i32 = 0;250const TRAP_NEXT_CALL_HOST: i32 = 1;251const TRAP_NEXT_RETURN_HOST: i32 = 2;252const TRAP_NEXT_CALL_WASM: i32 = 3;253const TRAP_NEXT_RETURN_WASM: i32 = 4;254const DO_NOTHING: i32 = 5;255256let engine = Engine::default();257258let mut linker = Linker::<State>::new(&engine);259260linker261.root()262.func_wrap("f", |mut store: _, (action,): (i32,)| -> Result<()> {263assert_eq!(store.data().context.last(), Some(&Context::Host));264assert_eq!(store.data().calls_into_host, store.data().calls_into_wasm);265266match action {267TRAP_IN_F => bail!("trapping in f"),268TRAP_NEXT_CALL_HOST => store.data_mut().trap_next_call_host = true,269TRAP_NEXT_RETURN_HOST => store.data_mut().trap_next_return_host = true,270TRAP_NEXT_CALL_WASM => store.data_mut().trap_next_call_wasm = true,271TRAP_NEXT_RETURN_WASM => store.data_mut().trap_next_return_wasm = true,272_ => {} // Do nothing273}274275Ok(())276})?;277278let wat = r#"279(component280(import "f" (func $f (param "action" s32)))281282(core func $f_lower283(canon lower (func $f))284)285(core module $m286(import "" "" (func $f (param i32)))287288(func $export (param i32)289(call $f (local.get 0))290)291292(export "export" (func $export))293)294(core instance $i (instantiate $m295(with "" (instance296(export "" (func $f_lower))297))298))299(func (export "export") (param "action" s32)300(canon lift301(core func $i "export")302)303)304)305"#;306307let component = Component::new(&engine, wat)?;308309let run = |action: i32, again: bool| -> (State, Option<wasmtime::Error>) {310let mut store = Store::new(&engine, State::default());311store.call_hook(sync_call_hook);312let inst = linker313.instantiate(&mut store, &component)314.expect("instantiate");315316let export = inst317.get_typed_func::<(i32,), ()>(&mut store, "export")318.expect("looking up `export`");319320let mut r = export.call(&mut store, (action,));321if r.is_ok() && again {322r = export.call(&mut store, (action,));323}324(store.into_data(), r.err())325};326327let (s, e) = run(DO_NOTHING, false);328assert!(e.is_none());329assert_eq!(s.calls_into_host, 1);330assert_eq!(s.returns_from_host, 1);331assert_eq!(s.calls_into_wasm, 1);332assert_eq!(s.returns_from_wasm, 1);333334let (s, e) = run(DO_NOTHING, true);335assert!(e.is_none());336assert_eq!(s.calls_into_host, 2);337assert_eq!(s.returns_from_host, 2);338assert_eq!(s.calls_into_wasm, 2);339assert_eq!(s.returns_from_wasm, 2);340341let (s, e) = run(TRAP_IN_F, false);342assert!(format!("{:?}", e.unwrap()).contains("trapping in f"));343assert_eq!(s.calls_into_host, 1);344assert_eq!(s.returns_from_host, 1);345assert_eq!(s.calls_into_wasm, 1);346assert_eq!(s.returns_from_wasm, 1);347348// // trap in next call to host. No calls after the bit is set, so this trap shouldn't happen349let (s, e) = run(TRAP_NEXT_CALL_HOST, false);350assert!(e.is_none());351assert_eq!(s.calls_into_host, 1);352assert_eq!(s.returns_from_host, 1);353assert_eq!(s.calls_into_wasm, 1);354assert_eq!(s.returns_from_wasm, 1);355356// trap in next call to host. call again, so the second call into host traps:357let (s, e) = run(TRAP_NEXT_CALL_HOST, true);358println!("{:?}", e.as_ref().unwrap());359assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingHost"));360assert_eq!(s.calls_into_host, 2);361assert_eq!(s.returns_from_host, 1);362assert_eq!(s.calls_into_wasm, 2);363assert_eq!(s.returns_from_wasm, 2);364365// trap in the return from host. should trap right away, without a second call366let (s, e) = run(TRAP_NEXT_RETURN_HOST, false);367assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromHost"));368assert_eq!(s.calls_into_host, 1);369assert_eq!(s.returns_from_host, 1);370assert_eq!(s.calls_into_wasm, 1);371assert_eq!(s.returns_from_wasm, 1);372373// trap in next call to wasm. No calls after the bit is set, so this trap shouldn't happen:374let (s, e) = run(TRAP_NEXT_CALL_WASM, false);375assert!(e.is_none());376assert_eq!(s.calls_into_host, 1);377assert_eq!(s.returns_from_host, 1);378assert_eq!(s.calls_into_wasm, 1);379assert_eq!(s.returns_from_wasm, 1);380381// trap in next call to wasm. call again, so the second call into wasm traps:382let (s, e) = run(TRAP_NEXT_CALL_WASM, true);383assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingWasm"));384assert_eq!(s.calls_into_host, 1);385assert_eq!(s.returns_from_host, 1);386assert_eq!(s.calls_into_wasm, 2);387assert_eq!(s.returns_from_wasm, 1);388389// trap in the return from wasm. should trap right away, without a second call390let (s, e) = run(TRAP_NEXT_RETURN_WASM, false);391assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromWasm"));392assert_eq!(s.calls_into_host, 1);393assert_eq!(s.returns_from_host, 1);394assert_eq!(s.calls_into_wasm, 1);395assert_eq!(s.returns_from_wasm, 1);396397Ok(())398}399400#[tokio::test]401async fn timeout_async_hook() -> Result<()> {402struct HandlerR;403404#[async_trait::async_trait]405impl CallHookHandler<State> for HandlerR {406async fn handle_call_event(407&self,408mut ctx: StoreContextMut<'_, State>,409ch: CallHook,410) -> Result<()> {411let obj = ctx.data_mut();412if obj.calls_into_host > 200 {413bail!("timeout");414}415416match ch {417CallHook::CallingHost => obj.calls_into_host += 1,418CallHook::CallingWasm => obj.calls_into_wasm += 1,419CallHook::ReturningFromHost => obj.returns_from_host += 1,420CallHook::ReturningFromWasm => obj.returns_from_wasm += 1,421}422423Ok(())424}425}426427let wat = r#"428(component429(import "f" (func $f))430431(core func $f_lower432(canon lower (func $f))433)434(core module $m435(import "" "" (func $f))436437(func $export438(loop $start439(call $f)440(br $start))441)442443(export "export" (func $export))444)445(core instance $i (instantiate $m446(with "" (instance447(export "" (func $f_lower))448))449))450(func (export "export")451(canon lift452(core func $i "export")453)454)455)456"#;457458let engine = Engine::default();459460let component = Component::new(&engine, wat)?;461462let mut linker = Linker::<State>::new(&engine);463linker464.root()465.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;466467let mut store = Store::new(&engine, State::default());468store.call_hook_async(HandlerR {});469470let inst = linker471.instantiate_async(&mut store, &component)472.await473.expect("instantiate");474475let export = inst476.get_typed_func::<(), ()>(&mut store, "export")477.expect("looking up `export`");478479let r = export.call_async(&mut store, ()).await;480assert!(format!("{:?}", r.unwrap_err()).contains("timeout"));481482let s = store.into_data();483assert!(s.calls_into_host > 1);484assert!(s.returns_from_host > 1);485assert_eq!(s.calls_into_wasm, 1);486assert_eq!(s.returns_from_wasm, 0);487488Ok(())489}490491#[tokio::test]492async fn drop_suspended_async_hook() -> Result<()> {493struct Handler;494495#[async_trait::async_trait]496impl CallHookHandler<u32> for Handler {497async fn handle_call_event(498&self,499mut ctx: StoreContextMut<'_, u32>,500_ch: CallHook,501) -> Result<()> {502let state = ctx.data_mut();503assert_eq!(*state, 0);504*state += 1;505let _dec = Decrement(state);506507// Simulate some sort of event which takes a number of yields508for _ in 0..500 {509tokio::task::yield_now().await;510}511Ok(())512}513}514515let wat = r#"516(component517(import "f" (func $f))518519(core func $f_lower520(canon lower (func $f))521)522(core module $m523(import "" "" (func $f))524525(func $export526(call $f)527)528529(export "export" (func $export))530)531(core instance $i (instantiate $m532(with "" (instance533(export "" (func $f_lower))534))535))536(func (export "export")537(canon lift538(core func $i "export")539)540)541)542"#;543544let engine = Engine::default();545546let component = Component::new(&engine, wat)?;547548let mut linker = Linker::<u32>::new(&engine);549linker.root().func_wrap_async("f", |mut store, _: ()| {550Box::new(async move {551let state = store.data_mut();552assert_eq!(*state, 0);553*state += 1;554let _dec = Decrement(state);555for _ in 0.. {556tokio::task::yield_now().await;557}558Ok(())559})560})?;561562let mut store = Store::new(&engine, 0);563store.call_hook_async(Handler);564565let inst = linker566.instantiate_async(&mut store, &component)567.await568.expect("instantiate");569570let export = inst571.get_typed_func::<(), ()>(&mut store, "export")572.expect("looking up `export`");573574// Test that if we drop in the middle of an async hook that everything575// is alright.576PollNTimes {577future: Box::pin(export.call_async(&mut store, ())),578times: 200,579}580.await;581assert_eq!(*store.data(), 0); // double-check user dtors ran582583return Ok(());584585// A helper struct to poll an inner `future` N `times` and then resolve.586// This is used above to test that when futures are dropped while they're587// pending everything works and is cleaned up on the Wasmtime side of588// things.589struct PollNTimes<F> {590future: F,591times: u32,592}593594impl<F: Future + Unpin> Future for PollNTimes<F>595where596F::Output: std::fmt::Debug,597{598type Output = ();599fn poll(mut self: Pin<&mut Self>, task: &mut task::Context<'_>) -> Poll<()> {600for i in 0..self.times {601match Pin::new(&mut self.future).poll(task) {602Poll::Ready(v) => panic!("future should not be ready at {i}; result is {v:?}"),603Poll::Pending => {}604}605}606607Poll::Ready(())608}609}610611// helper struct to decrement a counter on drop612struct Decrement<'a>(&'a mut u32);613614impl Drop for Decrement<'_> {615fn drop(&mut self) {616*self.0 -= 1;617}618}619}620621622