Lines
95.12 %
Functions
71.43 %
Branches
100 %
//! Tier 2 architectural spike: confirm wasmtime supports re-entrant
//! funcref calls from a host fn with per-call `Err` recovery, and that
//! engine-managed traps (OutOfFuel) propagate without being catchable.
//!
//! This test underwrites the `__nomi_catch_each` host native design in
//! the master plan: walking items in Rust, calling a script-supplied
//! closure per item, recovering script-raised errors per call.
//! What's verified:
//! - A host fn can receive a `Func` via wasm and call it back via
//! `Func::call`, with multiple invocations per host-fn call.
//! - When the called fn returns `Err`, control returns cleanly to the
//! host caller; it can keep iterating and call the same fn again.
//! - `OutOfFuel` traps cannot be silently swallowed: once the fuel
//! budget is exhausted inside the called fn, the trap propagates
//! through the host fn and reaches the outer caller.
use scripting::runtime::{EngineError, EngineOpts, build_engine, classify_runtime_error};
use wasmtime::{Caller, Func, Linker, Module, Store, Val, ValType};
/// Smallest wasm module exercising the re-entrancy pattern. The
/// exported `walk` fn calls into the host's `host_walk`, passing along
/// a callback funcref + arg-count. `host_walk` calls the callback that
/// many times with arg = 0..count and packs the per-call outcomes into
/// a single i32: low byte = ok-count, second byte = err-count.
const SPIKE_WAT: &str = r#"
(module
(import "spike" "host_walk"
(func $host_walk (param funcref) (param i32) (result i32)))
(func (export "walk") (param $cb funcref) (param $n i32) (result i32)
local.get $cb
local.get $n
call $host_walk)
)
"#;
#[test]
fn host_can_re_enter_funcref_and_recover_errors() {
let engine = build_engine(EngineOpts::baseline()).expect("build engine");
let module = Module::new(&engine, SPIKE_WAT).expect("compile spike module");
let mut store: Store<()> = Store::new(&engine, ());
store.set_epoch_deadline(1_000_000);
let mut linker: Linker<()> = Linker::new(&engine);
linker
.func_wrap(
"spike",
"host_walk",
move |mut caller: Caller<'_, ()>, cb: Option<Func>, n: i32| -> wasmtime::Result<i32> {
let cb = cb.ok_or_else(|| wasmtime::Error::msg("null callback funcref"))?;
let mut ok_count: i32 = 0;
let mut err_count: i32 = 0;
for i in 0..n {
let mut results = [Val::I32(0)];
match cb.call(&mut caller, &[Val::I32(i)], &mut results) {
Ok(()) => ok_count += 1,
Err(e) => {
let classified = classify_runtime_error(&e);
if matches!(
classified,
EngineError::OutOfFuel | EngineError::EpochInterrupt
) {
return Err(e);
}
err_count += 1;
Ok((err_count << 8) | ok_count)
},
.expect("register host_walk");
let instance = linker
.instantiate(&mut store, &module)
.expect("instantiate");
// Add a callback that fails on i == 2 — simulates `(error 'oops)`.
let callback = Func::new(
&mut store,
wasmtime::FuncType::new(
&engine,
[ValType::I32].iter().cloned(),
),
|_caller, args, results| {
let i = args[0].i32().expect("i32 arg");
if i == 2 {
return Err(wasmtime::Error::msg("__nomi_raise:oops:i was two"));
results[0] = Val::I32(i * 10);
Ok(())
);
let walk = instance.get_func(&mut store, "walk").expect("walk export");
walk.call(
&[Val::FuncRef(Some(callback)), Val::I32(5)],
&mut results,
.expect("walk call");
let packed = results[0].i32().expect("i32 result");
let ok = packed & 0xff;
let err = (packed >> 8) & 0xff;
assert_eq!(ok, 4, "expected 4 ok calls, got {ok}");
assert_eq!(err, 1, "expected 1 err call, got {err}");
fn out_of_fuel_during_callback_propagates_through_host_walk() {
let engine = build_engine(EngineOpts::baseline().with_fuel()).expect("build engine");
store.set_fuel(20).expect("set fuel");
Ok(ok_count)
// Callback contains a busy loop that drains fuel; OutOfFuel must
// bubble through host_walk to the outer call site.
let busy_wat = r#"
(func (export "busy") (param i32) (result i32)
(local $i i32)
i32.const 0
local.set $i
(loop
local.get $i
i32.const 1
i32.add
i32.const 1000000
i32.lt_s
br_if 0)
local.get $i))
let busy_module = Module::new(&engine, busy_wat).expect("compile busy module");
let busy_instance = Linker::<()>::new(&engine)
.instantiate(&mut store, &busy_module)
.expect("instantiate busy");
let callback = busy_instance
.get_func(&mut store, "busy")
.expect("busy export");
let outcome = walk.call(
&[Val::FuncRef(Some(callback)), Val::I32(10)],
let err = outcome.expect_err("walk must fail with OutOfFuel");
assert!(
matches!(classify_runtime_error(&err), EngineError::OutOfFuel),
"expected OutOfFuel, got {:?}",
classify_runtime_error(&err)