RISC-V runner
The RISC-V runner is a #![no_std] Rust RV64GC interpreter compiled to WebAssembly. It loads unmodified Linux ELF binaries and runs them instruction by instruction against a Linux syscall layer.
Design
- One interpreter function. A dense
matchinexec()compiles to a WebAssemblybr_table; the source is split across files with#[inline(always)]and fat LTO fuses it back into one function. - Hot state in locals. Registers and
pclive in local variables insideexec(). - No allocation in the hot path; one dependency,
vte(no_std), for the terminal parser. - Block cache. Decoded basic blocks (16,384 entries) are entered on every control transfer and chained; stores to a page with cached code invalidate it (self-modifying code safety).
- Cooperative yielding. An instruction budget returns control to the JS host so the event loop keeps running.
- Small host boundary. Six imports; file I/O through a shared-memory protocol. See Host API.
ISA
RV64IMAFDC: integer, multiply/divide, atomics, single and double float, and the 16-bit compressed encoding (the decoder checks the low two bits). Guest programs are static riscv64gc-linux-musl binaries; the catalog builds apps for that target.
Execution model
- The host calls
vm_step(vm, budget). - The interpreter runs until the budget is spent, the process exits, or a syscall needs the host.
- Internal syscalls (memory, threads, sockets, time) complete inside the module; file syscalls return
FS_PENDINGfor the host's in-memory filesystem;epollwaits returnEPOLL_BLOCKEDso time and injected connections can advance.
Memory layout
WebAssembly linear memory (shared; 192 MiB initial, 2 GiB max) ├── VM struct (12,680 bytes) ├── guest RAM (the rest of ramMB: program text, heap via brk/mmap, stack) ├── scratch buffers (virtual server I/O) └── data section: bundled ELFs, only in builds that embed them
Guest virtual addresses map directly to offsets in the RAM region. brk, mmap and mremap are bump allocators; munmap doesn't free (see Syscalls).
The VM struct
All machine state is one #[repr(C)] struct of 12,680 bytes (a compile-time assertion in types.rs); the JS host reads it at fixed offsets.
x[32], pc, f[32], fcsr, status, exit code, budget, fault infoThreads
clone creates thread slots (up to 16), futex provides wait/wake, and context switches happen at syscall boundaries. The memory is shared with atomics enabled, so this is real threading inside one instance. Node.js runs with UV_THREADPOOL_SIZE=0. fork is serialized: the child runs first, then the parent resumes.
Source map
runners/riscv/src/cpu.rsrunners/riscv/src/decode.rsrunners/riscv/src/syscall.rsrunners/riscv/src/mem.rsrunners/riscv/src/elf.rsrunners/riscv/src/types.rsrunners/riscv/src/exports.rsrunners/riscv/src/term.rs, tty.rsrunners/riscv/src/alloc.rs, host.rs, lib.rsrunners/riscv/host/nanovm.mjsWhat the guest sees
uname -a head -1 /proc/meminfo busybox | head -1
$ uname -a Linux nanovm 6.1.0 #1 NanoVM riscv64 GNU/Linux $ head -1 /proc/meminfo MemTotal: 524288 kB $ busybox | head -1 BusyBox v1.36.1 (2026-02-15 09:06:52 UTC) multi-call binary.
Measured speed is on Performance; feature tour on RISC-V emulator.