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 results — a0..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.