WASM Toolchains in SPy
WebAssembly: a quick primer¶
WebAssembly (WASM) is a binary instruction format designed to run at near-native speed in a sandboxed environment. Although it was originally designed for browsers, WASM can run anywhere a suitable host runtime exists — including desktop applications, servers, and embedded systems.
A key concept is the host/guest relationship: a WASM module (the guest) cannot do anything on its own. It has no access to the filesystem, network, or even standard output unless the host explicitly provides those capabilities as imports. This strong isolation is both a security feature and a portability mechanism.
Two standards define how a host can expose capabilities to a WASM guest:
WASI (WebAssembly System Interface): a portable, OS-like API covering file I/O, clocks, random numbers, etc. A WASM module compiled for WASI can run in any WASI-compatible runtime.
Emscripten ABI: a browser-oriented convention where a JavaScript glue layer provides emulated POSIX APIs (memory allocation, stdio, pthreads, …) to the WASM guest.
Two toolchains, two target environments¶
Both Zig and Emscripten (emcc) can compile C code to .wasm files, and both use LLVM/Clang under the hood. The difference is not the output format but the runtime environment they target.
Zig — for libspy.wasm (loaded by the interpreter)¶
Zig ships a self-contained C cross-compiler (zig cc) that requires no external toolchain installation. It can target bare WASM (wasm32-freestanding) or WASI (wasm32-wasi), producing a lean WASM module with only the imports that the host explicitly declares.
This is how libspy is built (make -C spy/libspy). The resulting libspy.wasm is loaded at interpreter startup by the Python process via wasmtime. SPy’s Python code acts as the host and provides the small set of imports that libspy needs: callbacks for debug logging, panic signalling, and turning WASM panics into Python SPyError exceptions. There is no JavaScript involved at any point.
The practical advantage is significant: contributors only need Zig, with no dependency on Emscripten or any browser toolchain.
Emscripten — for compiling SPy programs to run in the browser¶
When a SPy program is compiled with spy build --target emscripten, Emscripten is used instead. Emscripten produces a .wasm file together with a JavaScript glue layer. This glue emulates POSIX APIs in the browser environment, handles memory management, and wires the module into the browser’s event loop.
This output is what powers the SPy playground: the compiled SPy program runs as WASM inside the browser, with Emscripten’s JS layer bridging it to browser APIs.
Summary¶
Zig (wasm32-wasi / freestanding) | Emscripten | |
|---|---|---|
| Used for | libspy.wasm — the interpreter’s runtime library | Compiling SPy programs for the browser |
| Host environment | wasmtime inside Python | Browser JavaScript engine |
| JS glue layer | No | Yes |
| External toolchain needed | No — zig cc is self-contained | Yes — emcc must be installed |
| POSIX emulation | No — only explicit host imports | Yes — full emulated libc |
In short: Zig produces the WASM that lives inside the Python interpreter, while Emscripten produces the WASM that lives inside a browser. They look similar from the outside but serve entirely different host environments, which is why SPy uses both.