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

Calling a Guest

The smallest complete program: compile an ELF, instantiate it, call a function.

cargo run --example calling_a_guest
use rvtime::{Config, Engine, Linker, Module, Store};

/// A statically linked RV64IMAC ELF, built with `--emit-relocs`.
const GUEST: &[u8] = include_bytes!("../../../fixtures/basic.elf");

fn main() -> anyhow::Result<()> {
    // An engine holds the target configuration. Compiled code is tied to it,
    // so a module and the store that runs it must come from the same one.
    let engine = Engine::new(&Config::default())?;

    // Compiling reads the ELF, decodes every function, and generates native
    // code for all of them up front.
    let module = Module::new(&engine, GUEST)?;

    // A store owns one guest's memory and registers, plus whatever data you
    // want host functions to see. Here there is none, so `()`.
    let mut store = Store::new(&engine, ());

    // Instantiating maps the guest's memory and wires it to the compiled code.
    let instance = Linker::new(&engine).instantiate(&mut store, &module)?;

    // Exports are looked up by symbol name. The type parameters say how many
    // argument registers to use and how many results to read back -- an ELF
    // carries no signature to check them against.
    let add = instance.get_typed_func::<(u64, u64), u64>("op_add")?;
    assert_eq!(add.call(&mut store, (10, 3))?, 13);

    // A handle is reusable, and calls are ordinary function calls.
    let fib = instance.get_typed_func::<(u64,), u64>("fib")?;
    for n in 0..10 {
        print!("{} ", fib.call(&mut store, (n,))?);
    }
    println!();

    Ok(())
}

What is happening

Engine holds the target configuration — optimisation level, address space size, whether interruption checks are emitted. Compiled code is tied to it, because some of those settings are baked into the generated instructions. A module and the store that runs it must come from the same engine.

Module::new does all the work: it parses the ELF, recovers function boundaries from the symbol table, decodes every instruction, analyses control flow, and generates native code for the whole program. There is no lazy path yet, so this is where the time goes — see Performance.

Store owns one guest: its memory, its registers, and whatever data you want host functions to see. One store holds one instance, which is narrower than wasmtime and matches what a program image needs — one address space, one register file.

get_typed_func resolves a symbol and gives it a Rust signature. Nothing checks that signature against the guest, because an ELF carries no type information to check against. The type parameters choose how many argument registers to write and how many results to read; they are not a contract the guest declared.

At most eight arguments and two resultsa0..a7 going in, a0 and a1 coming back. Asking for more results is an error rather than a silent misread, because the registers beyond those two hold whatever was there before the call. See Registers.