Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/tests/all/component_model/call_hook.rs
1692 views
1
#![cfg(not(miri))]
2
3
use super::REALLOC_AND_FREE;
4
use crate::call_hook::{Context, State, sync_call_hook};
5
use anyhow::{Result, bail};
6
use std::future::Future;
7
use std::pin::Pin;
8
use std::task::{self, Poll};
9
use wasmtime::component::*;
10
use wasmtime::{CallHook, CallHookHandler, Config, Engine, Store, StoreContextMut};
11
12
// Crate a synchronous Func, call it directly:
13
#[test]
14
fn call_wrapped_func() -> Result<()> {
15
let wat = r#"
16
(component
17
(import "f" (func $f))
18
19
(core func $f_lower
20
(canon lower (func $f))
21
)
22
(core module $m
23
(import "" "" (func $f))
24
25
(func $export
26
(call $f)
27
)
28
29
(export "export" (func $export))
30
)
31
(core instance $i (instantiate $m
32
(with "" (instance
33
(export "" (func $f_lower))
34
))
35
))
36
(func (export "export")
37
(canon lift
38
(core func $i "export")
39
)
40
)
41
)
42
"#;
43
44
let engine = Engine::default();
45
let component = Component::new(&engine, wat)?;
46
47
let mut linker = Linker::<State>::new(&engine);
48
linker
49
.root()
50
.func_wrap("f", |_, _: ()| -> Result<()> { Ok(()) })?;
51
52
let mut store = Store::new(&engine, State::default());
53
store.call_hook(sync_call_hook);
54
let inst = linker
55
.instantiate(&mut store, &component)
56
.expect("instantiate");
57
58
let export = inst
59
.get_typed_func::<(), ()>(&mut store, "export")
60
.expect("looking up `export`");
61
62
export.call(&mut store, ())?;
63
export.post_return(&mut store)?;
64
65
let s = store.into_data();
66
assert_eq!(s.calls_into_host, 1);
67
assert_eq!(s.returns_from_host, 1);
68
assert_eq!(s.calls_into_wasm, 1);
69
assert_eq!(s.returns_from_wasm, 1);
70
71
Ok(())
72
}
73
74
// Call a func that turns a `list<u8>` into a `string`, to ensure that `realloc` calls are counted.
75
#[test]
76
fn call_func_with_realloc() -> Result<()> {
77
let wat = format!(
78
r#"(component
79
(core module $m
80
(memory (export "memory") 1)
81
(func (export "roundtrip") (param i32 i32) (result i32)
82
(local $base i32)
83
(local.set $base
84
(call $realloc
85
(i32.const 0)
86
(i32.const 0)
87
(i32.const 4)
88
(i32.const 8)))
89
(i32.store offset=0
90
(local.get $base)
91
(local.get 0))
92
(i32.store offset=4
93
(local.get $base)
94
(local.get 1))
95
(local.get $base)
96
)
97
98
{REALLOC_AND_FREE}
99
)
100
(core instance $i (instantiate $m))
101
102
(func (export "list8-to-str") (param "a" (list u8)) (result string)
103
(canon lift
104
(core func $i "roundtrip")
105
(memory $i "memory")
106
(realloc (func $i "realloc"))
107
)
108
)
109
)"#
110
);
111
112
let engine = Engine::default();
113
let component = Component::new(&engine, wat)?;
114
let linker = Linker::<State>::new(&engine);
115
let mut store = Store::new(&engine, State::default());
116
store.call_hook(sync_call_hook);
117
let inst = linker
118
.instantiate(&mut store, &component)
119
.expect("instantiate");
120
121
let export = inst
122
.get_typed_func::<(&[u8],), (WasmStr,)>(&mut store, "list8-to-str")
123
.expect("looking up `list8-to-str`");
124
125
let message = String::from("hello, world!");
126
let res = export.call(&mut store, (message.as_bytes(),))?.0;
127
let result = res.to_str(&store)?;
128
assert_eq!(&message, &result);
129
130
export.post_return(&mut store)?;
131
132
// There are two wasm calls for the `list8-to-str` call and the guest realloc call for the list
133
// argument.
134
let s = store.into_data();
135
assert_eq!(s.calls_into_host, 0);
136
assert_eq!(s.returns_from_host, 0);
137
assert_eq!(s.calls_into_wasm, 2);
138
assert_eq!(s.returns_from_wasm, 2);
139
140
Ok(())
141
}
142
143
// Call a guest function that also defines a post-return.
144
#[test]
145
fn call_func_with_post_return() -> Result<()> {
146
let wat = r#"
147
(component
148
(core module $m
149
(func (export "roundtrip"))
150
(func (export "post-return"))
151
)
152
(core instance $i (instantiate $m))
153
154
(func (export "export")
155
(canon lift
156
(core func $i "roundtrip")
157
(post-return (func $i "post-return"))
158
)
159
)
160
)"#;
161
162
let engine = Engine::default();
163
let component = Component::new(&engine, wat)?;
164
let linker = Linker::<State>::new(&engine);
165
let mut store = Store::new(&engine, State::default());
166
store.call_hook(sync_call_hook);
167
let inst = linker
168
.instantiate(&mut store, &component)
169
.expect("instantiate");
170
171
let export = inst
172
.get_typed_func::<(), ()>(&mut store, "export")
173
.expect("looking up `export`");
174
175
export.call(&mut store, ())?;
176
177
// Before post-return, there will only have been one call into wasm.
178
assert_eq!(store.data().calls_into_wasm, 1);
179
assert_eq!(store.data().returns_from_wasm, 1);
180
181
export.post_return(&mut store)?;
182
183
// There are no host calls in this example, but the post-return does increment the count of
184
// wasm calls by 1, putting the total number of wasm calls at 2.
185
let s = store.into_data();
186
assert_eq!(s.calls_into_host, 0);
187
assert_eq!(s.returns_from_host, 0);
188
assert_eq!(s.calls_into_wasm, 2);
189
assert_eq!(s.returns_from_wasm, 2);
190
191
Ok(())
192
}
193
194
// Create an async Func, call it directly:
195
#[tokio::test]
196
async fn call_wrapped_async_func() -> Result<()> {
197
let wat = r#"
198
(component
199
(import "f" (func $f))
200
201
(core func $f_lower
202
(canon lower (func $f))
203
)
204
(core module $m
205
(import "" "" (func $f))
206
207
(func $export
208
(call $f)
209
)
210
211
(export "export" (func $export))
212
)
213
(core instance $i (instantiate $m
214
(with "" (instance
215
(export "" (func $f_lower))
216
))
217
))
218
(func (export "export")
219
(canon lift
220
(core func $i "export")
221
)
222
)
223
)
224
"#;
225
226
let mut config = Config::new();
227
config.async_support(true);
228
let engine = Engine::new(&config)?;
229
230
let component = Component::new(&engine, wat)?;
231
232
let mut linker = Linker::<State>::new(&engine);
233
linker
234
.root()
235
.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;
236
237
let mut store = Store::new(&engine, State::default());
238
store.call_hook(sync_call_hook);
239
240
let inst = linker
241
.instantiate_async(&mut store, &component)
242
.await
243
.expect("instantiate");
244
245
let export = inst
246
.get_typed_func::<(), ()>(&mut store, "export")
247
.expect("looking up `export`");
248
249
export.call_async(&mut store, ()).await?;
250
export.post_return_async(&mut store).await?;
251
252
let s = store.into_data();
253
assert_eq!(s.calls_into_host, 1);
254
assert_eq!(s.returns_from_host, 1);
255
assert_eq!(s.calls_into_wasm, 1);
256
assert_eq!(s.returns_from_wasm, 1);
257
258
Ok(())
259
}
260
261
#[test]
262
fn trapping() -> Result<()> {
263
const TRAP_IN_F: i32 = 0;
264
const TRAP_NEXT_CALL_HOST: i32 = 1;
265
const TRAP_NEXT_RETURN_HOST: i32 = 2;
266
const TRAP_NEXT_CALL_WASM: i32 = 3;
267
const TRAP_NEXT_RETURN_WASM: i32 = 4;
268
const DO_NOTHING: i32 = 5;
269
270
let engine = Engine::default();
271
272
let mut linker = Linker::<State>::new(&engine);
273
274
linker
275
.root()
276
.func_wrap("f", |mut store: _, (action,): (i32,)| -> Result<()> {
277
assert_eq!(store.data().context.last(), Some(&Context::Host));
278
assert_eq!(store.data().calls_into_host, store.data().calls_into_wasm);
279
280
match action {
281
TRAP_IN_F => bail!("trapping in f"),
282
TRAP_NEXT_CALL_HOST => store.data_mut().trap_next_call_host = true,
283
TRAP_NEXT_RETURN_HOST => store.data_mut().trap_next_return_host = true,
284
TRAP_NEXT_CALL_WASM => store.data_mut().trap_next_call_wasm = true,
285
TRAP_NEXT_RETURN_WASM => store.data_mut().trap_next_return_wasm = true,
286
_ => {} // Do nothing
287
}
288
289
Ok(())
290
})?;
291
292
let wat = r#"
293
(component
294
(import "f" (func $f (param "action" s32)))
295
296
(core func $f_lower
297
(canon lower (func $f))
298
)
299
(core module $m
300
(import "" "" (func $f (param i32)))
301
302
(func $export (param i32)
303
(call $f (local.get 0))
304
)
305
306
(export "export" (func $export))
307
)
308
(core instance $i (instantiate $m
309
(with "" (instance
310
(export "" (func $f_lower))
311
))
312
))
313
(func (export "export") (param "action" s32)
314
(canon lift
315
(core func $i "export")
316
)
317
)
318
)
319
"#;
320
321
let component = Component::new(&engine, wat)?;
322
323
let run = |action: i32, again: bool| -> (State, Option<anyhow::Error>) {
324
let mut store = Store::new(&engine, State::default());
325
store.call_hook(sync_call_hook);
326
let inst = linker
327
.instantiate(&mut store, &component)
328
.expect("instantiate");
329
330
let export = inst
331
.get_typed_func::<(i32,), ()>(&mut store, "export")
332
.expect("looking up `export`");
333
334
let mut r = export.call(&mut store, (action,));
335
if r.is_ok() && again {
336
export.post_return(&mut store).unwrap();
337
r = export.call(&mut store, (action,));
338
}
339
(store.into_data(), r.err())
340
};
341
342
let (s, e) = run(DO_NOTHING, false);
343
assert!(e.is_none());
344
assert_eq!(s.calls_into_host, 1);
345
assert_eq!(s.returns_from_host, 1);
346
assert_eq!(s.calls_into_wasm, 1);
347
assert_eq!(s.returns_from_wasm, 1);
348
349
let (s, e) = run(DO_NOTHING, true);
350
assert!(e.is_none());
351
assert_eq!(s.calls_into_host, 2);
352
assert_eq!(s.returns_from_host, 2);
353
assert_eq!(s.calls_into_wasm, 2);
354
assert_eq!(s.returns_from_wasm, 2);
355
356
let (s, e) = run(TRAP_IN_F, false);
357
assert!(format!("{:?}", e.unwrap()).contains("trapping in f"));
358
assert_eq!(s.calls_into_host, 1);
359
assert_eq!(s.returns_from_host, 1);
360
assert_eq!(s.calls_into_wasm, 1);
361
assert_eq!(s.returns_from_wasm, 1);
362
363
// // trap in next call to host. No calls after the bit is set, so this trap shouldn't happen
364
let (s, e) = run(TRAP_NEXT_CALL_HOST, false);
365
assert!(e.is_none());
366
assert_eq!(s.calls_into_host, 1);
367
assert_eq!(s.returns_from_host, 1);
368
assert_eq!(s.calls_into_wasm, 1);
369
assert_eq!(s.returns_from_wasm, 1);
370
371
// trap in next call to host. call again, so the second call into host traps:
372
let (s, e) = run(TRAP_NEXT_CALL_HOST, true);
373
println!("{:?}", e.as_ref().unwrap());
374
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingHost"));
375
assert_eq!(s.calls_into_host, 2);
376
assert_eq!(s.returns_from_host, 1);
377
assert_eq!(s.calls_into_wasm, 2);
378
assert_eq!(s.returns_from_wasm, 2);
379
380
// trap in the return from host. should trap right away, without a second call
381
let (s, e) = run(TRAP_NEXT_RETURN_HOST, false);
382
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromHost"));
383
assert_eq!(s.calls_into_host, 1);
384
assert_eq!(s.returns_from_host, 1);
385
assert_eq!(s.calls_into_wasm, 1);
386
assert_eq!(s.returns_from_wasm, 1);
387
388
// trap in next call to wasm. No calls after the bit is set, so this trap shouldn't happen:
389
let (s, e) = run(TRAP_NEXT_CALL_WASM, false);
390
assert!(e.is_none());
391
assert_eq!(s.calls_into_host, 1);
392
assert_eq!(s.returns_from_host, 1);
393
assert_eq!(s.calls_into_wasm, 1);
394
assert_eq!(s.returns_from_wasm, 1);
395
396
// trap in next call to wasm. call again, so the second call into wasm traps:
397
let (s, e) = run(TRAP_NEXT_CALL_WASM, true);
398
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on CallingWasm"));
399
assert_eq!(s.calls_into_host, 1);
400
assert_eq!(s.returns_from_host, 1);
401
assert_eq!(s.calls_into_wasm, 2);
402
assert_eq!(s.returns_from_wasm, 1);
403
404
// trap in the return from wasm. should trap right away, without a second call
405
let (s, e) = run(TRAP_NEXT_RETURN_WASM, false);
406
assert!(format!("{:?}", e.unwrap()).contains("call_hook: trapping on ReturningFromWasm"));
407
assert_eq!(s.calls_into_host, 1);
408
assert_eq!(s.returns_from_host, 1);
409
assert_eq!(s.calls_into_wasm, 1);
410
assert_eq!(s.returns_from_wasm, 1);
411
412
Ok(())
413
}
414
415
#[tokio::test]
416
async fn timeout_async_hook() -> Result<()> {
417
struct HandlerR;
418
419
#[async_trait::async_trait]
420
impl CallHookHandler<State> for HandlerR {
421
async fn handle_call_event(
422
&self,
423
mut ctx: StoreContextMut<'_, State>,
424
ch: CallHook,
425
) -> Result<()> {
426
let obj = ctx.data_mut();
427
if obj.calls_into_host > 200 {
428
bail!("timeout");
429
}
430
431
match ch {
432
CallHook::CallingHost => obj.calls_into_host += 1,
433
CallHook::CallingWasm => obj.calls_into_wasm += 1,
434
CallHook::ReturningFromHost => obj.returns_from_host += 1,
435
CallHook::ReturningFromWasm => obj.returns_from_wasm += 1,
436
}
437
438
Ok(())
439
}
440
}
441
442
let wat = r#"
443
(component
444
(import "f" (func $f))
445
446
(core func $f_lower
447
(canon lower (func $f))
448
)
449
(core module $m
450
(import "" "" (func $f))
451
452
(func $export
453
(loop $start
454
(call $f)
455
(br $start))
456
)
457
458
(export "export" (func $export))
459
)
460
(core instance $i (instantiate $m
461
(with "" (instance
462
(export "" (func $f_lower))
463
))
464
))
465
(func (export "export")
466
(canon lift
467
(core func $i "export")
468
)
469
)
470
)
471
"#;
472
473
let mut config = Config::new();
474
config.async_support(true);
475
let engine = Engine::new(&config)?;
476
477
let component = Component::new(&engine, wat)?;
478
479
let mut linker = Linker::<State>::new(&engine);
480
linker
481
.root()
482
.func_wrap_async("f", |_, _: ()| Box::new(async { Ok(()) }))?;
483
484
let mut store = Store::new(&engine, State::default());
485
store.call_hook_async(HandlerR {});
486
487
let inst = linker
488
.instantiate_async(&mut store, &component)
489
.await
490
.expect("instantiate");
491
492
let export = inst
493
.get_typed_func::<(), ()>(&mut store, "export")
494
.expect("looking up `export`");
495
496
let r = export.call_async(&mut store, ()).await;
497
assert!(format!("{:?}", r.unwrap_err()).contains("timeout"));
498
499
let s = store.into_data();
500
assert!(s.calls_into_host > 1);
501
assert!(s.returns_from_host > 1);
502
assert_eq!(s.calls_into_wasm, 1);
503
assert_eq!(s.returns_from_wasm, 0);
504
505
Ok(())
506
}
507
508
#[tokio::test]
509
async fn drop_suspended_async_hook() -> Result<()> {
510
struct Handler;
511
512
#[async_trait::async_trait]
513
impl CallHookHandler<u32> for Handler {
514
async fn handle_call_event(
515
&self,
516
mut ctx: StoreContextMut<'_, u32>,
517
_ch: CallHook,
518
) -> Result<()> {
519
let state = ctx.data_mut();
520
assert_eq!(*state, 0);
521
*state += 1;
522
let _dec = Decrement(state);
523
524
// Simulate some sort of event which takes a number of yields
525
for _ in 0..500 {
526
tokio::task::yield_now().await;
527
}
528
Ok(())
529
}
530
}
531
532
let wat = r#"
533
(component
534
(import "f" (func $f))
535
536
(core func $f_lower
537
(canon lower (func $f))
538
)
539
(core module $m
540
(import "" "" (func $f))
541
542
(func $export
543
(call $f)
544
)
545
546
(export "export" (func $export))
547
)
548
(core instance $i (instantiate $m
549
(with "" (instance
550
(export "" (func $f_lower))
551
))
552
))
553
(func (export "export")
554
(canon lift
555
(core func $i "export")
556
)
557
)
558
)
559
"#;
560
561
let mut config = Config::new();
562
config.async_support(true);
563
let engine = Engine::new(&config)?;
564
565
let component = Component::new(&engine, wat)?;
566
567
let mut linker = Linker::<u32>::new(&engine);
568
linker.root().func_wrap_async("f", |mut store, _: ()| {
569
Box::new(async move {
570
let state = store.data_mut();
571
assert_eq!(*state, 0);
572
*state += 1;
573
let _dec = Decrement(state);
574
for _ in 0.. {
575
tokio::task::yield_now().await;
576
}
577
Ok(())
578
})
579
})?;
580
581
let mut store = Store::new(&engine, 0);
582
store.call_hook_async(Handler);
583
584
let inst = linker
585
.instantiate_async(&mut store, &component)
586
.await
587
.expect("instantiate");
588
589
let export = inst
590
.get_typed_func::<(), ()>(&mut store, "export")
591
.expect("looking up `export`");
592
593
// Test that if we drop in the middle of an async hook that everything
594
// is alright.
595
PollNTimes {
596
future: Box::pin(export.call_async(&mut store, ())),
597
times: 200,
598
}
599
.await;
600
assert_eq!(*store.data(), 0); // double-check user dtors ran
601
602
return Ok(());
603
604
// A helper struct to poll an inner `future` N `times` and then resolve.
605
// This is used above to test that when futures are dropped while they're
606
// pending everything works and is cleaned up on the Wasmtime side of
607
// things.
608
struct PollNTimes<F> {
609
future: F,
610
times: u32,
611
}
612
613
impl<F: Future + Unpin> Future for PollNTimes<F>
614
where
615
F::Output: std::fmt::Debug,
616
{
617
type Output = ();
618
fn poll(mut self: Pin<&mut Self>, task: &mut task::Context<'_>) -> Poll<()> {
619
for i in 0..self.times {
620
match Pin::new(&mut self.future).poll(task) {
621
Poll::Ready(v) => panic!("future should not be ready at {i}; result is {v:?}"),
622
Poll::Pending => {}
623
}
624
}
625
626
Poll::Ready(())
627
}
628
}
629
630
// helper struct to decrement a counter on drop
631
struct Decrement<'a>(&'a mut u32);
632
633
impl Drop for Decrement<'_> {
634
fn drop(&mut self) {
635
*self.0 -= 1;
636
}
637
}
638
}
639
640