The Guest Heap
Giving a guest a heap, so it can allocate.
cargo run --example guest_heap
use rvtime::{Config, Engine, Linker, Module, Store};
const GUEST: &[u8] = include_bytes!("../../../fixtures/hosted.elf");
fn main() -> anyhow::Result<()> {
let mut config = Config::default();
config.memory_size(16 << 20).stack_size(64 << 10);
let engine = Engine::new(&config)?;
let module = Module::new(&engine, GUEST)?;
let mut store = Store::new(&engine, ());
let instance = Linker::new(&engine).instantiate(&mut store, &module)?;
// rvtime commits a heap between the guest's image and its stack, but does
// not tell the guest where it is: the bounds travel through whatever
// interface you defined. Here the guest exports a function to receive them.
let heap = store.heap()?;
println!(
"heap: {:#x}..{:#x} ({} KiB)",
heap.start,
heap.end,
(heap.end - heap.start) / 1024
);
let init = instance.get_typed_func::<(u64, u64), u64>("init_heap")?;
init.call(&mut store, (heap.start, heap.end - heap.start))?;
// With the allocator fed, the guest can use `alloc` -- `Vec`, `String`,
// and any crate that does not need `std`.
let sum = instance.get_typed_func::<(u64,), u64>("alloc_sum")?;
println!(
"guest summed a heap-allocated vector: {}",
sum.call(&mut store, (1000,))?
);
// The allocator hands memory back, so a long-running guest does not leak.
let used = instance.get_typed_func::<(), u64>("heap_used")?;
println!("bytes still allocated: {}", used.call(&mut store, ())?);
Ok(())
}
Why the host has to hand it over
rvtime commits the heap but does not tell the guest where it is. That looks like an omission and is deliberate.
Every way of conveying the bounds automatically means rvtime inventing an ABI:
a header at a fixed guest address, a reserved call number, a value stashed in
tp. Each of those claims part of a space the embedder owns, and each becomes a
compatibility surface the moment anyone depends on it. So Store::heap() reports
the bounds and you pass them in however your interface already works — an init
export, as here, or a host function you registered.
What you get
The region sits between the guest’s image and its stack, committed read-write
and zeroed. Committing it up front costs address space rather than memory:
mprotect only changes protection, and pages fault in when first touched.
A guard page separates it from the stack, so running off the top of the heap faults instead of quietly landing in stack frames.
Size follows Config::memory_size — the heap is whatever is left after the
image and the stack. For many small guests, size them down; the default 64 MiB
is address space per store.
Before the handover
An allocation before heap::init returns null, and the guest writes through it.
Guest address 0 is never committed, so that faults rather than corrupting the
bottom of the address space. A null dereference in a guest is a trap you can
catch, not silent damage.