Host Functions
Letting a guest call back into the host.
cargo run --example host_functions
use rvtime::{Caller, Config, Engine, Linker, Module, Store};
const GUEST: &[u8] = include_bytes!("../../../fixtures/hosted.elf");
/// Whatever the host wants to keep across calls. Host functions reach it
/// through `Caller::data`.
#[derive(Default)]
struct State {
ticks: u64,
}
fn main() -> anyhow::Result<()> {
let engine = Engine::new(&Config::default())?;
let module = Module::new(&engine, GUEST)?;
let mut store = Store::new(&engine, State::default());
let mut linker = Linker::new(&engine);
// A guest calls these with `ecall`, taking the number from `a7`. The key
// is a number rather than a name because an ELF has no import table to
// resolve names against -- you and the guest agree on the numbering.
linker.func_wrap(1, |_: Caller<'_, State>, a: u64, b: u64| {
Ok(a.wrapping_add(b))
})?;
linker.func_wrap(4, |mut caller: Caller<'_, State>| {
caller.data_mut().ticks += 1;
Ok(caller.data().ticks)
})?;
// Host functions can read and write guest memory. The guest passes a
// buffer the usual way, as a pointer and a length.
linker.func_wrap(2, |caller: Caller<'_, State>, ptr: u64, len: u64| {
let bytes = caller.read(ptr, len)?;
Ok(bytes.iter().map(|b| *b as u64).sum::<u64>())
})?;
let instance = linker.instantiate(&mut store, &module)?;
let add = instance.get_typed_func::<(u64, u64), u64>("call_add")?;
println!(
"guest asked the host to add: {}",
add.call(&mut store, (20, 22))?
);
let tick = instance.get_typed_func::<(), u64>("call_tick")?;
tick.call(&mut store, ())?;
tick.call(&mut store, ())?;
println!("host state after two ticks: {}", store.data().ticks);
// The guest fills a buffer and asks the host to sum it.
let round_trip = instance.get_typed_func::<(u64,), u64>("round_trip")?;
println!(
"sum of 1..=10 computed by the host: {}",
round_trip.call(&mut store, (10,))?
);
// Returning `Err` from a host function stops the guest rather than
// handing it a value. Use `Ok(code)` for failures the guest should handle.
Ok(())
}
Numbers, not names
A guest reaches a host function with ecall, taking the call number from a7
and arguments from a0 onwards — the standard RISC-V syscall convention. So
Linker is keyed by number.
That is not a simplification of wasmtime’s named imports; it is what the input
format allows. A WebAssembly module carries an import table naming what it needs,
which the host resolves. An ELF carries nothing of the sort. There is no name to
match, so the guest and the host agree on a numbering, and nothing checks that
they agree — a mismatch surfaces as Trap::UnknownHostCall at the moment the
guest calls it.
Reading and writing guest memory
Caller gives host functions access to the guest’s address space. Buffers are
passed the usual way, as a pointer and a length in two registers, and the host
validates the range before touching it.
There is no marshalling layer above that. A host function receives u64s and
decides what they mean, because rvtime has no way to know.
Failing
Two different failures, and the difference matters:
Ok(code)returns a value the guest handles. Use this for anything the guest should be able to recover from — a missing key, a closed connection.Err(..)stops the guest. The pending call fails with the error attached. Use this when continuing makes no sense.
This is the same split as a return value versus a trap in wasmtime. Reaching for
Err where the guest could have coped turns a recoverable condition into a dead
plugin.
Arity
func_wrap covers zero to six arguments. Beyond that, Linker::func hands you
the raw Caller and you read registers yourself — useful when the number of
arguments is not fixed.