⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.1503+1f1bee62eOn an older Zig?

What WebAssembly Is

WebAssembly (wasm) is a binary instruction format for a stack machine. It is a compile target, not a language: you write Zig, C, or Rust, and the compiler emits a .wasm module. An engine (a browser, Node, wasmtime) loads that module and runs it in a sandbox at close to native speed.

This whole site is the demonstration. Every snippet you have read is Zig compiled to wasm32-wasi, and the Run button hands that exact module to a WASI shim in your browser. Press it below and the code reports the target it was built for.

const std = @import("std");
const builtin = @import("builtin");

pub fn main(init: std.process.Init) !void {
    var buf: [256]u8 = undefined;
    var fw = std.Io.File.stdout().writerStreaming(init.io, &buf);
    const out = &fw.interface;

    try out.print("cpu arch: {t}\n", .{builtin.cpu.arch});
    try out.print("os tag:   {t}\n", .{builtin.os.tag});
    try out.print("pointer:  {d} bits\n", .{@bitSizeOf(usize)});

    try out.flush();
}

Module, instance, linear memory

Three words carry most of the model.

A module is the compiled .wasm file: code and a description of what it imports and exports. It is inert, like an unloaded shared library.

An instance is a module that an engine has loaded and wired up. Instantiating resolves the module’s imports (functions the host provides) and gives it its memory. You can instantiate the same module more than once; each instance has its own state.

Linear memory is the instance’s heap: one flat, contiguous array of bytes, addressed from zero, that the module and the host both see. It starts at some number of 64 KiB pages and can grow. A Zig pointer inside a wasm module is just an offset into this array. There is no other memory the module can touch.

Only numbers cross a call

A wasm function signature can only mention four types: i32, i64, f32, f64. That is the entire boundary vocabulary. There is no string type, no struct, no array that a call can pass or return.

Anything larger travels as bytes in linear memory, referred to by an i32 offset and (usually) an i32 length. A Zig []const u8 slice becomes a pointer and a length; across the wasm boundary that is two i32s. The linear memory recipe works through the pattern in full.

The sandbox

A wasm module has no ambient authority. It cannot open a file, read the clock, or make a network call on its own. It can only do what its imports let it do. A module that was handed no imports can compute and nothing else.

This is why the same .wasm runs unchanged in a browser tab and on a server: capability comes from the host, not from the binary. It is also why running an untrusted module is reasonable in a way that running an untrusted native binary is not.

Three ways Zig reaches wasm

The difference is the target triple you pass, which decides what the sandbox is allowed to reach.

TargetWhat it gives youUse it for
wasm32-freestandingNothing but wasm itself. No libc, no OS. You export functions and manage memory.Modules a JavaScript or native host drives.
wasm32-wasiA POSIX-shaped system interface: files, args, clock, stdout, an entry point.Command-line programs that run under Node, wasmtime, or Wasmer.
wasm32-emscriptenThe Emscripten runtime and its libc/JS glue.Porting a large C/C++ codebase that expects that environment.

This guide verifies against wasm32-wasi because it needs stdout to diff, and the freestanding recipe shows the smaller target.

Command and reactor

Two shapes of module, split by whether they have an entry point.

A command has a _start function. The engine calls it once, it runs to completion like a main, and the instance is spent. Every WASI program in this guide is a command. A Zig file with pub fn main compiles to one.

A reactor has no entry point. The engine instantiates it and then calls its exported functions on demand, many times, keeping the instance alive between calls. This is the shape you want when JavaScript calls into Zig. You build it with -fno-entry and name the functions to export.

Which one you need decides which recipe applies. The next pages build each in turn: a WASI command you run from a shell, a reactor called from JavaScript, the memory handshake between them, a freestanding module, and finally loading one in a browser.