Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.