This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
NanoVM is a #![no_std] Rust WebAssembly emulator implementing an RV64 RISC-V Linux userland emulator targeting BusyBox and Node.js workloads in the browser. The architecture is defined in .claude/skills/bellard/SKILL.md.
make build # Default: fully-bundled wasm/nano.wasm (busybox + node + devenv)
make build-minimal # Bare emulator (~585KB) — no bundled binaries
make devenv # Build devenv Docker image + extract tarball (slow first time)
make clean # cargo clean + remove wasm/nano.wasm
make serve # Build + serve wasm/ on localhost:8080
make demo # Build + copy WASM to demo + start vite dev server
make test # Build minimal + run all tests (ELF + MemFS + BusyBox)
make test-devenv # Build bundled + run all tests including devenv tools
# Fast iteration (type-check only, no linking):
cargo check --target wasm32-unknown-unknownTests are in test/ and run via Node.js:
bash test/run_tests.sh # Run tests (requires wasm/nano.wasm)
bash test/run_tests.sh --build # Build test ELFs first (requires cross-compiler)
bash test/run_tests.sh --devenv # Include devenv tool tests (requires bundled WASM + images/node)
# Run single ELF test:
node test/run.mjs test/hello.elf
# Run busybox command:
node test/run.mjs images/busybox --cmd echo Hello
# Run with syscall tracing:
node test/run.mjs images/busybox --trace --cmd ls /tmpTest phases: MemFS unit tests → ELF execution (hello, test_suite, test_rvc, test_memory, test_syscalls, test_float) → BusyBox smoke tests (17 applets) → Devenv tool tests (node, tsc, npm, eslint, prettier).
- Target:
wasm32-unknown-unknown(Rust stable toolchain) - Crate type:
cdylib(produces.wasm) - Dependencies: none —
[dependencies]is empty (sqrt/sqrtflower directly to WASMf64.sqrt/f32.sqrtopcodes; nolibm) - Release profile:
opt-level = 3,lto = "fat",codegen-units = 1,panic = "abort",strip = true(speed-first, per the Bellard interpreter design) - Dev profile:
opt-level = 1,codegen-units = 256(fast incremental builds) - WASM memory: 1MB stack, 192MB initial, 2GB max, shared + imported (set in
.cargo/config.toml) - Features:
demo(default) — embeds busybox + node + devenv into the WASM data section
The core design principle is a single monolithic exec() function with dense dispatch that compiles to WASM br_table (jump table). Source code is split across files marked #[inline(always)]; fat LTO with codegen-units = 1 fuses them into one function at compile time.
- All VM structs must be
#[repr(C)]for stable layout - Hot CPU state lives in locals inside
exec(), not repeated struct reads - No heap allocation, trait objects, HashMap, or recursion in the CPU hot path
- Cooperative yielding via instruction budget counter
- Syscalls batched through shared memory request/response blocks
- Threading via SharedArrayBuffer + Web Workers; worker entrypoints take
vm_ptr: u32explicitly
src/cpu.rs— Main RV64 interpreter loop (exec()) with instruction decode/dispatchsrc/decode.rs— Instruction decode helperssrc/syscall.rs— Linux syscall dispatch (handles ~50 syscalls for BusyBox + Node.js)src/mem.rs— Guest memory access (read/write with bounds checking)src/elf.rs— ELF loader (parses segments, sets up stack with argv/envp/auxv)src/types.rs— VM struct layout (12680 bytes,#[repr(C)], compile-time size assertion)src/exports.rs— WASM exports (vm_create, vm_step, vm_load_elf, debug_*, etc.)src/alloc.rs— Bump allocator for WASM linear memorysrc/host.rs— Host import declarations (console_write, debug_log, etc.)
Key offsets (must stay in sync with JS host code):
0..560— CPU state (x[32], pc, f[32], fcsr, status, exit_code, budget, fault info)560..600— brk/memory (brk_start, brk_current, stack_limit)600..2136— fd_table[64] (24 bytes each: fd_type, host_fd, offset, flags)2216..2768— FsRequest (552 bytes: syscall_nr, fd, args, path[256], path2[256])2768..2792— FsResponse (24 bytes)3680..3936— cwd[256]3936..3972— run state (tid, run_status, ram_base, ram_size, heap_ptr)
When the VM needs filesystem I/O:
- Rust fills
FsRequeststruct (syscall_nr, fd, path, args) - Sets
vm.status = STATUS_FS_PENDING(6) - JS host reads request, processes via MemFS, writes result to
a0register - JS host resets
vm.status = STATUS_OK(0) - VM resumes execution
WASM exports: vm_create, vm_step, vm_load_elf, vm_fs_request_ptr, vm_ram_ptr, vm_ram_size, vm_exit_code, debug_pc, debug_reg, debug_status, vm_bundled_*
WASM imports: memory (SharedArrayBuffer), console_write(fd, ptr, len), debug_log(val), abort_js(), emscripten_random(), emscripten_date_now()
React + Vite app with three-panel IDE layout (FileTree, Editor, Preview/Console).
container/nanovm.mjs— Browser NanoVM wrapper (imports WASM, provides high-level API)container/memfs.mjs— In-memory POSIX filesystemweb/demo/src/vm/runtime.ts— Singleton VM management, wraps NanoVM for Reactweb/demo/src/vm/sw-bridge.ts— Service Worker bridge for HTTP previewweb/demo/src/vm/examples.ts— Example files seeded into VFS
The @container alias in vite.config.ts resolves to container/ (project root).