Path: blob/main/tests/all/component_model/call_hook.rs
1692 views
#![cfg(not(miri))]12use super::REALLOC_AND_FREE;3use crate::call_hook::{Context, State, sync_call_hook};4use anyhow::{Result, bail};5use std::future::Future;6use std::pin::Pin;7use std::task::{self, Poll};8use wasmtime::component::*;9use wasmtime::{CallHook, CallHookHandler, Config, Engine, Store, StoreContextMut};1011// Crate a synchronous Func, call it directly:12#[test]13fn call_wrapped_func() -> Result<()> {14let wat = r#"15(component16(import "f" (func $f))1718(core func $f_lower19(canon lower (func $f))20)21(core module $m22(import "" "" (func $f))2324(func $export25(call $f)26)2728(export "export" (func $export))29)30(core instance $i (instantiate $m31(with "" (instance32(export "" (func $f_lower))33))34))35(func (export "export")36(canon lift37(core func $i "export")38)39)40)41"#;4243let engine = Engine::default();44let component = Component::new(&engine, wat)?;4546let mut linker = Linker::<State>::new(&engine);47linker48.root()49.func_wrap("f", |_, _: ()| -> Result<()> { Ok(()) })?;5051let mut store = Store::new(&engine, State::default());52store.call_hook(sync_call_hook);53let inst = linker54.instantiate(&mut store, &component)55.expect("instantiate");5657let export = inst58.get_typed_func::<(), ()>(&mut store, "export")59.expect("looking up `export`");6061export.call(&mut store, ())?;62export.post_return(&mut store)?;6364let s = store.into_data();65assert_eq!(s.calls_into_host, 1);66assert_eq!(s.returns_from_host, 1);67assert_eq!(s.calls_into_wasm, 1);68assert_eq!(s.returns_from_wasm, 1);6970Ok(())71}7273// Call a func that turns a `list<u8>` into a `string`, to ensure that `realloc` calls are counted.74#[test]75fn call_func_with_realloc() -> Result<()> {76let wat = format!(77r#"(component78(core module $m79(memory (export "memory") 1)80(func (export "roundtrip") (param i32 i32) (result i32)81(local $base i32)82(local.set $base83(call $realloc84(i32.const 0)85(i32.const 0)86(i32.const 4)87(i32.const 8)))88(i32.store offset=089(local.get $base)90(local.get 0))91(i32.store offset=492(local.get $base)93(local.get 1))94(local.get $base)95)9697{REALLOC_AND_FREE}98)99(core instance $i (instantiate $m))100101(func (export "list8-to-str") (param "a" (list u8)) (result string)102(canon lift103(core func $i "roundtrip")104(memory $i "memory")105(realloc (func $i "realloc"))106)107)108)"#109);110111let engine = Engine::default();112let component = Component::new(&engine, wat)?;113let linker = Linker::<State>::new(&engine);114let mut store = Store::new(&engine, State::default());115store.call_hook(sync_call_hook);116let inst = linker117.instantiate(&mut store, &component)118.expect("instantiate");119120let export = inst121.get_typed_func::<(&[u8],), (WasmStr,)>(&mut store, "list8-to-str")122.expect("looking up `list8-to-str`");123124let message = String::from("hello, world!");125let res = export.call(&mut store, (message.as_bytes(),))?.0;126let result = res.to_str(&store)?;127assert_eq!(&message, &result);128129export.post_return(&mut store)?;130131// There are two wasm calls for the `list8-to-str` call and the guest realloc call for the list132// argument.133let s = store.into_data();134assert_eq!(s.calls_into_host, 0);135assert_eq!(s.returns_from_host, 0);136assert_eq!(s.calls_into_wasm, 2);137assert_eq!(s.returns_from_wasm, 2);138139Ok(())140}141142// Call a guest function that also defines a post-return.143#[test]144fn call_func_with_post_return() -> Result<()> {145let wat = r#"146(component147(core module $m148(func (export "roundtrip"))149(func (export "post-return"))150)151(core instance $i (instantiate $m))152153(func (export "export")154(canon lift155(core func $i "roundtrip")156(post-return (func $i "post-return"))157)158)159)"#;160161let engine = Engine::default();162let component = Component::new(&engine, wat)?;163let linker = Linker::<State>::new(&engine);164let mut store = Store::new(&engine, State::default());165store.call_hook(sync_call_hook);166let inst = linker167.instantiate(&mut store, &component)168.expect("instantiate");169170let export = inst171.get_typed_func::<(), ()>(&mut store, "export")172.expect("looking up `export`");173174export.call(&mut store, ())?;175176// Before post-return, there will only have been one call into wasm.177assert_eq!(store.data().calls_into_wasm, 1);178assert_eq!(store.data().returns_from_wasm, 1);179180export.post_return(&mut store)?;181182// There are no host calls in this example, but the post-return does increment the count of183// wasm calls by 1, putting the total number of wasm calls at 2.184let s = store.into_data();185assert_eq!(s.calls_into_host, 0);186assert_eq!(s.returns_from_host, 0);187assert_eq!(s.calls_into_wasm, 2);188assert_eq!(s.returns_from_wasm, 2);189190Ok(())191}192193// Create an async Func, call it directly:194#[tokio::test]195async fn call_wrapped_async_func() -> Result<()> {196let wat = r#"197(component198(import "f" (func $f))199200(core func $f_lower201(canon lower (func $f))202)203(core module $m204(import "" "" (func $f))205206(func $export207(call $f)208)209210(export "export" (func $export))211)212(core instance $i (instantiate $m213(with "" (instance214(export "" (func $f_lower))215))216))217(func (export "export")218(canon lift219(core func $i "export")220)221)222)223"#;224225let mut config = Config::new();226config.async_support(true);227let engine = Engine::new(&config)?;228229let component = Component::new(&engine, wat)?;230231let mut linker = Linker::<State>::new(&engine);232linker233.root()234.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;235236let mut store = Store::new(&engine, State::default());237store.call_hook(sync_call_hook);238239let inst = linker240.instantiate_async(&mut store, &component)241.await242.expect("instantiate");243244let export = inst245.get_typed_func::<(), ()>(&mut store, "export")246.expect("looking up `export`");247248export.call_async(&mut store, ()).await?;249export.post_return_async(&mut store).await?;250251let s = store.into_data();252assert_eq!(s.calls_into_host, 1);253assert_eq!(s.returns_from_host, 1);254assert_eq!(s.calls_into_wasm, 1);255assert_eq!(s.returns_from_wasm, 1);256257Ok(())258}259260#[test]261fn trapping() -> Result<()> {262const TRAP_IN_F: i32 = 0;263const TRAP_NEXT_CALL_HOST: i32 = 1;264const TRAP_NEXT_RETURN_HOST: i32 = 2;265const TRAP_NEXT_CALL_WASM: i32 = 3;266const TRAP_NEXT_RETURN_WASM: i32 = 4;267const DO_NOTHING: i32 = 5;268269let engine = Engine::default();270271let mut linker = Linker::<State>::new(&engine);272273linker274.root()275.func_wrap("f", |mut store: _, (action,): (i32,)| -> Result<()> {276assert_eq!(store.data().context.last(), Some(&Context::Host));277assert_eq!(store.data().calls_into_host, store.data().calls_into_wasm);278279match action {280TRAP_IN_F => bail!("trapping in f"),281TRAP_NEXT_CALL_HOST => store.data_mut().trap_next_call_host = true,282TRAP_NEXT_RETURN_HOST => store.data_mut().trap_next_return_host = true,283TRAP_NEXT_CALL_WASM => store.data_mut().trap_next_call_wasm = true,284TRAP_NEXT_RETURN_WASM => store.data_mut().trap_next_return_wasm = true,285_ => {} // Do nothing286}287288Ok(())289})?;290291let wat = r#"292(component293(import "f" (func $f (param "action" s32)))294295(core func $f_lower296(canon lower (func $f))297)298(core module $m299(import "" "" (func $f (param i32)))300301(func $export (param i32)302(call $f (local.get 0))303)304305(export "export" (func $export))306)307(core instance $i (instantiate $m308(with "" (instance309(export "" (func $f_lower))310))311))312(func (export "export") (param "action" s32)313(canon lift314(core func $i "export")315)316)317)318"#;319320let component = Component::new(&engine, wat)?;321322let run = |action: i32, again: bool| -> (State, Option<anyhow::Error>) {323let mut store = Store::new(&engine, State::default());324store.call_hook(sync_call_hook);325let inst = linker326.instantiate(&mut store, &component)327.expect("instantiate");328329let export = inst330.get_typed_func::<(i32,), ()>(&mut store, "export")331.expect("looking up `export`");332333let mut r = export.call(&mut store, (action,));334if r.is_ok() && again {335export.post_return(&mut store).unwrap();336r = export.call(&mut store, (action,));337}338(store.into_data(), r.err())339};340341let (s, e) = run(DO_NOTHING, false);342assert!(e.is_none());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);347348let (s, e) = run(DO_NOTHING, true);349assert!(e.is_none());350assert_eq!(s.calls_into_host, 2);351assert_eq!(s.returns_from_host, 2);352assert_eq!(s.calls_into_wasm, 2);353assert_eq!(s.returns_from_wasm, 2);354355let (s, e) = run(TRAP_IN_F, false);356assert!(format!("{:?}", e.unwrap()).contains("trapping in f"));357assert_eq!(s.calls_into_host, 1);358assert_eq!(s.returns_from_host, 1);359assert_eq!(s.calls_into_wasm, 1);360assert_eq!(s.returns_from_wasm, 1);361362// // trap in next call to host. No calls after the bit is set, so this trap shouldn't happen363let (s, e) = run(TRAP_NEXT_CALL_HOST, false);364assert!(e.is_none());365assert_eq!(s.calls_into_host, 1);366assert_eq!(s.returns_from_host, 1);367assert_eq!(s.calls_into_wasm, 1);368assert_eq!(s.returns_from_wasm, 1);369370// trap in next call to host. call again, so the second call into host traps:371let (s, e) = run(TRAP_NEXT_CALL_HOST, true);372println!("{:?}", e.as_ref().unwrap());373assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingHost"));374assert_eq!(s.calls_into_host, 2);375assert_eq!(s.returns_from_host, 1);376assert_eq!(s.calls_into_wasm, 2);377assert_eq!(s.returns_from_wasm, 2);378379// trap in the return from host. should trap right away, without a second call380let (s, e) = run(TRAP_NEXT_RETURN_HOST, false);381assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromHost"));382assert_eq!(s.calls_into_host, 1);383assert_eq!(s.returns_from_host, 1);384assert_eq!(s.calls_into_wasm, 1);385assert_eq!(s.returns_from_wasm, 1);386387// trap in next call to wasm. No calls after the bit is set, so this trap shouldn't happen:388let (s, e) = run(TRAP_NEXT_CALL_WASM, false);389assert!(e.is_none());390assert_eq!(s.calls_into_host, 1);391assert_eq!(s.returns_from_host, 1);392assert_eq!(s.calls_into_wasm, 1);393assert_eq!(s.returns_from_wasm, 1);394395// trap in next call to wasm. call again, so the second call into wasm traps:396let (s, e) = run(TRAP_NEXT_CALL_WASM, true);397assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingWasm"));398assert_eq!(s.calls_into_host, 1);399assert_eq!(s.returns_from_host, 1);400assert_eq!(s.calls_into_wasm, 2);401assert_eq!(s.returns_from_wasm, 1);402403// trap in the return from wasm. should trap right away, without a second call404let (s, e) = run(TRAP_NEXT_RETURN_WASM, false);405assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromWasm"));406assert_eq!(s.calls_into_host, 1);407assert_eq!(s.returns_from_host, 1);408assert_eq!(s.calls_into_wasm, 1);409assert_eq!(s.returns_from_wasm, 1);410411Ok(())412}413414#[tokio::test]415async fn timeout_async_hook() -> Result<()> {416struct HandlerR;417418#[async_trait::async_trait]419impl CallHookHandler<State> for HandlerR {420async fn handle_call_event(421&self,422mut ctx: StoreContextMut<'_, State>,423ch: CallHook,424) -> Result<()> {425let obj = ctx.data_mut();426if obj.calls_into_host > 200 {427bail!("timeout");428}429430match ch {431CallHook::CallingHost => obj.calls_into_host += 1,432CallHook::CallingWasm => obj.calls_into_wasm += 1,433CallHook::ReturningFromHost => obj.returns_from_host += 1,434CallHook::ReturningFromWasm => obj.returns_from_wasm += 1,435}436437Ok(())438}439}440441let wat = r#"442(component443(import "f" (func $f))444445(core func $f_lower446(canon lower (func $f))447)448(core module $m449(import "" "" (func $f))450451(func $export452(loop $start453(call $f)454(br $start))455)456457(export "export" (func $export))458)459(core instance $i (instantiate $m460(with "" (instance461(export "" (func $f_lower))462))463))464(func (export "export")465(canon lift466(core func $i "export")467)468)469)470"#;471472let mut config = Config::new();473config.async_support(true);474let engine = Engine::new(&config)?;475476let component = Component::new(&engine, wat)?;477478let mut linker = Linker::<State>::new(&engine);479linker480.root()481.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;482483let mut store = Store::new(&engine, State::default());484store.call_hook_async(HandlerR {});485486let inst = linker487.instantiate_async(&mut store, &component)488.await489.expect("instantiate");490491let export = inst492.get_typed_func::<(), ()>(&mut store, "export")493.expect("looking up `export`");494495let r = export.call_async(&mut store, ()).await;496assert!(format!("{:?}", r.unwrap_err()).contains("timeout"));497498let s = store.into_data();499assert!(s.calls_into_host > 1);500assert!(s.returns_from_host > 1);501assert_eq!(s.calls_into_wasm, 1);502assert_eq!(s.returns_from_wasm, 0);503504Ok(())505}506507#[tokio::test]508async fn drop_suspended_async_hook() -> Result<()> {509struct Handler;510511#[async_trait::async_trait]512impl CallHookHandler<u32> for Handler {513async fn handle_call_event(514&self,515mut ctx: StoreContextMut<'_, u32>,516_ch: CallHook,517) -> Result<()> {518let state = ctx.data_mut();519assert_eq!(*state, 0);520*state += 1;521let _dec = Decrement(state);522523// Simulate some sort of event which takes a number of yields524for _ in 0..500 {525tokio::task::yield_now().await;526}527Ok(())528}529}530531let wat = r#"532(component533(import "f" (func $f))534535(core func $f_lower536(canon lower (func $f))537)538(core module $m539(import "" "" (func $f))540541(func $export542(call $f)543)544545(export "export" (func $export))546)547(core instance $i (instantiate $m548(with "" (instance549(export "" (func $f_lower))550))551))552(func (export "export")553(canon lift554(core func $i "export")555)556)557)558"#;559560let mut config = Config::new();561config.async_support(true);562let engine = Engine::new(&config)?;563564let component = Component::new(&engine, wat)?;565566let mut linker = Linker::<u32>::new(&engine);567linker.root().func_wrap_async("f", |mut store, _: ()| {568Box::new(async move {569let state = store.data_mut();570assert_eq!(*state, 0);571*state += 1;572let _dec = Decrement(state);573for _ in 0.. {574tokio::task::yield_now().await;575}576Ok(())577})578})?;579580let mut store = Store::new(&engine, 0);581store.call_hook_async(Handler);582583let inst = linker584.instantiate_async(&mut store, &component)585.await586.expect("instantiate");587588let export = inst589.get_typed_func::<(), ()>(&mut store, "export")590.expect("looking up `export`");591592// Test that if we drop in the middle of an async hook that everything593// is alright.594PollNTimes {595future: Box::pin(export.call_async(&mut store, ())),596times: 200,597}598.await;599assert_eq!(*store.data(), 0); // double-check user dtors ran600601return Ok(());602603// A helper struct to poll an inner `future` N `times` and then resolve.604// This is used above to test that when futures are dropped while they're605// pending everything works and is cleaned up on the Wasmtime side of606// things.607struct PollNTimes<F> {608future: F,609times: u32,610}611612impl<F: Future + Unpin> Future for PollNTimes<F>613where614F::Output: std::fmt::Debug,615{616type Output = ();617fn poll(mut self: Pin<&mut Self>, task: &mut task::Context<'_>) -> Poll<()> {618for i in 0..self.times {619match Pin::new(&mut self.future).poll(task) {620Poll::Ready(v) => panic!("future should not be ready at {i}; result is {v:?}"),621Poll::Pending => {}622}623}624625Poll::Ready(())626}627}628629// helper struct to decrement a counter on drop630struct Decrement<'a>(&'a mut u32);631632impl Drop for Decrement<'_> {633fn drop(&mut self) {634*self.0 -= 1;635}636}637}638639640