A WebAssembly to x86-64 AOT compiler. Zero dependencies. No LLVM. No runtime. Just wasm in, Linux ELF out.
Named after the condiment after I was sitting eating some sushi at St Pierres(if you know you know). Because it's small, it burns through things fast, and too much will make you cry. Please don't ask.
Wasabi takes a .wasm binary and produces a native Linux x86-64 executable. No interpreter, no JIT, no intermediate representation. The wasm stack machine maps directly to registers, functions compile to native calls, and the output is a static ELF binary that runs without any runtime support.
This has been sitting in my folder for a while. It started as a "how hard can it be" experiment and now it passes the WebAssembly spec test suite, so apparently the answer was "not that hard if you don't mind hand-encoding x86 instructions."
Tested against the official WebAssembly spec test suite:
| Test | Pass | Total | |
|---|---|---|---|
| i32 | 374 | 374 | 100% |
| i64 | 384 | 384 | 100% |
| f32 | 2500 | 2500 | 100% |
| f64 | 2500 | 2500 | 100% |
5,758 tests. Zero failures.
I had to wait like 2 hours just for the tests to run so boy was I happy when it worked.
make
Yep, that's it. GCC and a pulse.
wasabi input.wasm -o output
The output is a Linux ELF binary. Run it directly or under WSL.
wasabi program.wasm -o program
./program
There's also an invoke mode for calling individual exports (used by the spec test runner):
wasabi --invoke "add" i32:1 i32:2 module.wasm -o test
./test | xxd
Other flags:
wasabi --dump input.wasm # dump module structure
wasabi --disasm input.wasm # disassemble wasm opcodes
Requires WSL with wast2json installed (from wabt).
python tests/run_spec.py --all --filter i32
python tests/run_spec.py --all
- Full integer arithmetic (i32, i64) including all edge cases
- Full floating point (f32, f64) with correct NaN propagation and signed-zero semantics
- Locals, globals, memory loads/stores (all widths and sign extensions)
- Control flow: block, loop, if/else, br, br_if, br_table, call, call_indirect, return
- WASI: fd_write, fd_read, proc_exit, args, environ, clock_time_get, path_open, and more
- Data segments, element segments, function tables
- Saturating truncation (0xFC prefix)
- memory.copy, memory.fill, memory.init
- memory.grow / memory.size (runtime page counter, 16MB growable address space)
- SIMD
- Threads
- Multi-value returns
- Tail calls
- RISC-V and ARM (Still needing to do my ARM assembler for dummies course)
- Any kind of optimisation (it's fast enough, and correctness comes first)
wasm_decode.c decode .wasm binary into module structs
wasm_compile.c translate wasm opcodes to x86-64 machine code
x86_emit.c x86-64 instruction encoder (hand-rolled, no assembler)
elf_emit.c ELF binary writer
wasi.c WASI syscall implementations
main.c CLI and ELF patching
The wasm virtual stack maps directly to x86 registers (RAX, RCX, RDX, RSI, RDI, R8-R11) with spill to the native stack. RBX holds the linear memory base. R12 holds the data segment address. R14 holds the text base for indirect calls.
Same reason as BarraCUDA. I wanted to understand the thing, so I built the thing. Compilers are the most fun you can have with a keyboard, and wasm is a surprisingly clean target to compile from — it's basically a well-typed stack machine with no ambiguity, which is more than I can say for most things I've had to parse.
Apache 2.0. See LICENSE.