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

Interrupting a Guest

Stopping a guest that will not stop itself.

cargo run --example interrupting
use rvtime::{Config, Engine, Linker, Module, Store};
use std::{thread, time::Duration};

const GUEST: &[u8] = include_bytes!("../../../fixtures/hosted.elf");

fn main() -> anyhow::Result<()> {
    // On by default, but spelled out here because it is the point of the
    // example: it makes the translator emit a check on every backward edge.
    let mut config = Config::default();
    config.interruptible(true);

    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)?;

    // The handle is `Send + Sync`, so a watchdog can hold one while the guest
    // runs somewhere else.
    let handle = store.interrupt_handle()?;

    // `spin` never returns on its own.
    let spin = instance.get_typed_func::<(u64,), u64>("spin")?;
    let guest = thread::spawn(move || spin.call(&mut store, (0,)));

    thread::sleep(Duration::from_millis(100));
    println!("asking the guest to stop");
    handle.interrupt();

    // The guest stops at its next loop iteration and the pending call fails.
    match guest.join().expect("the guest thread panicked") {
        Ok(value) => println!("returned {value} -- unexpected, `spin` should not return"),
        Err(error) => println!("stopped: {error}"),
    }

    Ok(())
}

Why this exists

Without it, a guest that loops forever holds the thread that called into it forever. There is no way to take that thread back: you cannot safely kill a thread mid-execution, and the guest is running native code with no scheduler above it.

For a guest you wrote, that is a bug you fix. For anything installed at runtime, it is a denial of service against the host — which is why the checks are on by default, and why interrupt_handle() errors rather than returning a handle that would silently do nothing on a module compiled without them.

What it costs

A load, a test and a branch on every backward edge. Measured at 0.2% on a 50-million-iteration tight loop — the flag stays in L1 and the branch predicts perfectly, so it fills slack the loop already had.

Straight-line code pays nothing: the flag pointer is loaded once per function, and only in functions that actually loop.

What it does not cover

Only loops. That is enough for non-termination — a guest can run forever by looping, and unbounded recursion exhausts the stack and traps — but it means a long-running straight-line computation cannot be interrupted mid-way.

In practice, optimisation decides. A counted loop that LLVM turns into a closed form has no backward edge and therefore no check; it also always terminates, so nothing is lost.

Using it

Interrupt is Send + Sync and cheap to clone, so a watchdog thread can hold one while the guest runs elsewhere. interrupt() returns immediately; the guest stops at its next loop iteration and its pending call fails with Trap::Interrupted. clear() withdraws the request so the store can be used again.