Build WebAssembly Component Model components in Zig.
Start with a WIT schema, write a Zig core module, and this project wraps it
into a real component: a .wasm file validated by wasm-tools.
Your component can call Rust hosts, and Rust hosts can call your component,
through the same canonical ABI.
The build works like cargo-component for Rust.
A small code generator turns WIT into Zig source, which compiles to
wasm32-freestanding.
Then wasm-tools component embed and component new turn that core module
into a component.
You'll find the full user documentation in docs/.
Start with getting-started to see what you'll need and run through the bundled demos. Then follow your first component to build your own, step by step.
zig build test # native parser, codegen, and runtime unit tests
zig build test-components # component regressions (Python 3, wasm-tools, wasmtime)
zig build demo # builds zig-out/wasm/greeter.component.wasm
zig build dual # builds zig-out/wasm/math.component.wasm
zig build resource # builds zig-out/wasm/counts.component.wasm
zig build http-get # builds zig-out/wasm/http-get.component.wasm
zig build async-basic # builds zig-out/wasm/async-basic.component.wasm
zig build stream-demo # builds zig-out/wasm/stream-demo.component.wasm
zig build wasi-demo # builds zig-out/wasm/wasi-demo.component.wasm
zig build wasi-demo-p3 # builds zig-out/wasm/wasi-demo-p3.component.wasm (WASI 0.3.2)
# Run a component standalone via Wasmtime:
wasmtime run --invoke 'manhattan({x:-3, y:4})' \
zig-out/wasm/greeter.component.wasm
# => 7
# Run the Rust↔Zig greeter demo (Rust host calls into Zig
# component; the Zig component also calls back into the host):
cargo build --release --manifest-path examples/greeter/rust-host/Cargo.toml
./examples/greeter/rust-host/target/release/host \
zig-out/wasm/greeter.component.wasm
# Run the dual-language demo (one Rust host, two components built
# from the same WIT, one in Zig and one with cargo-component):
cargo build --release --manifest-path examples/dual/host/Cargo.toml
(cd examples/dual/rust-impl && cargo component build --release)
./examples/dual/host/target/release/dual-host \
zig-out/wasm/math.component.wasm \
examples/dual/rust-impl/target/wasm32-wasip1/release/math.wasm
# Run the resource demo (constructor / methods / drop lifecycle):
cargo build --release --manifest-path examples/resource/host/Cargo.toml
./examples/resource/host/target/release/counts-host \
zig-out/wasm/counts.component.wasm
# Run the wasi:http GET example through Wasmtime directly:
wasmtime run -S http=y zig-out/wasm/http-get.component.wasm
# Run the async demo (Rust tokio host calls async Zig exports and
# the Zig guest in turn calls an async Rust import):
cargo build --release --manifest-path examples/async-basic/rust-host/Cargo.toml
./examples/async-basic/rust-host/target/release/host \
zig-out/wasm/async-basic.component.wasm
# Run the typed-stream demo (Zig guest streams u32 and u64 values,
# the Rust host drains and verifies each readable end):
cargo build --release --manifest-path examples/stream-demo/rust-host/Cargo.toml
./examples/stream-demo/rust-host/target/release/host \
zig-out/wasm/stream-demo.component.wasmExpected output:
greet("Hello from Rust!") = 16
sum([1,2,3,4,5,100]) = 115
manhattan(point { x: -3, y: 4 }) = 7
format-greeting("world") = "Hi, world!"
captured logs: ["hello Hello from Rust!"]
OK: Rust ↔ Zig component interop is verified in both directions.
src/
wit.zig WIT parser (WASIp3-scoped grammar)
abi.zig Runtime helpers (cabi_realloc backing)
codegen.zig Zig source generator from a parsed WIT world
wasi.zig `Wasi(bindings)` convenience layer for WASI 0.2
wasi3.zig `Wasi3(bindings)` convenience layer for WASI 0.3
wasi_common.zig Pieces shared by both convenience layers
main.zig `zig-wit dump|gen` CLI
tests/
components.py Builds and runs component regressions in Debug and ReleaseSmall.
*.wit, *.zig Fixtures for generated bindings and canonical ABI behavior.
README.md Reproduced failures, validation commands, and coverage limits.
examples/
greeter/ Records, lists, variants, options, results, tuples,
flags, char, indirect params/results.
dual/ Same WIT, one impl in Zig and one in Rust.
resource/ A `counter` resource with the full
constructor/methods/drop lifecycle.
http-get/ A wasi:http GET client. The whole wasi:http /
wasi:io / wasi:cli surface is fed through the
codegen via `wasm-tools component wit`; the
guest contains zero hand-written `@extern`s.
async-basic/ Async-with-callback canonical ABI end-to-end:
`[async-lift]` exports (`succ`, `measure`,
`greet`, `promise`, `relay`) and an
`[async-lower]` import (`clock.tick`). The Rust
host uses `instantiate_async` + `run_concurrent`.
stream-demo/ Typed `stream<T>` end-to-end: the guest streams
`stream<u32>` squares and `stream<u64>` Fibonacci
numbers through one generic producer, exercising
the non-byte stream path that `async-basic`'s
`stream<u8>` skips.
wasi-demo/ Uses the `zig_wasi_components.wasi` convenience
module to exercise clocks, randomness, stdio,
environment, terminal probes, a filesystem
round-trip, a DNS lookup, and an HTTPS GET in a
single guest.
wasi-demo-p3/ The same tour against WASI 0.3.2 via the
`zig_wasi_components.wasi3` module: stream-based
stdio and file I/O, async clock sleeps, a
`stream<directory-entry>` listing, DNS, plus a
`wasi:http/service@0.3.2` guest that serves real
requests under `wasmtime serve` and makes an
outbound fetch through the 0.3 http client
wrappers on each one.
The parser handles all the WIT syntax used by real WASI 0.2.x and 0.3.x packages:
- packages with
ns:name@x.y.z[-suffix]versions, file-scopeuse - interfaces and worlds, with
include ... with { a as b } - cross-package
use foo:bar/baz@1.0.0.{x, y} record,variant,enum,flags, type aliasesresourcewith constructors, methods, static methods,own<T>/borrow<T>stream<T>,future<T>,error-contextmap<K, V>, represented as a slice of key/value pairs using its canonical form,list<tuple<K, V>>asyncfunctions@since(version = ...)/@unstable(feature = ...)/@deprecated(version = ...)gates@external-id("...")annotations- inline interfaces in world imports/exports
- plain-named interface imports/exports:
import users: store;andimport catalog: store;bind the same interface twice under different names, each with its own generated namespace
The project has been checked against the published WIT files for the full
set of these packages at 0.2.12:
wasi:cliwasi:iowasi:clockswasi:randomwasi:socketswasi:filesystemwasi:http
This includes the exit-with-code call and the unstable
wasi:clocks/timezone interface.
WASI 0.3.2 is fully supported.
Every world in its six packages generates bindings that compile and
pass wasm-tools component new.
wasmtime run -S p3: run the command world.wasmtime serve -S p3,cli: run the HTTP service world.
Both run end to end under wasmtime 49.
The generator creates stream and future canon intrinsics for every function
that uses stream<T> or future<T>.
That includes async and sync functions, both at the top level and in resource
methods.
The lift and lower helpers use the canonical layout and support compound
payloads such as directory-entry.
So you can use the wasi3 convenience layer from plain Zig, including the 0.3
stdio, filesystem, and HTTP APIs.
The code generator handles every WIT type in the table below, in both directions. For indirect parameters and returns, it uses the specification's canonical memory layout.
It also generates the required cabi_realloc and cabi_post_* entry points.
The latter follows wit-component's current naming convention, so wasmtime
can connect each function to canon lift.
| WIT type | covered | demo |
|---|---|---|
| primitives | ✓ | greeter |
char |
✓ | greeter/upper-char |
string |
✓ | greeter/greet, format-greeting, log import |
list<T> |
✓ | greeter/sum, total-distance |
record |
✓ | greeter/manhattan, origin import |
variant |
✓ | greeter/classify |
enum, flags (≤32 labels) |
✓ | greeter/perms-popcount |
option<T> |
✓ | greeter/maybe-double |
result<T, E> (return and parameter) |
✓ | greeter/safe-divide, greeter/choose |
tuple<...> |
✓ | greeter/pair, greeter/divmod |
| indirect params (>16 flats) | ✓ | greeter/sum-many |
| indirect results (export and import) | ✓ | greeter/format-greeting, origin import |
resource |
✓ | resource/counter |
stream<T>, future<T>, error-context |
✓ (handle pass-through) | tmp/streamworld.wit |
async func(...) exports |
✓ | async-basic/{succ,measure,greet,promise} |
async func(...) imports |
✓ | async-basic/relay (calls host's clock.tick) |
per-func [stream-*-i] / [future-*-i] |
✓ | async-basic/{greet,promise} |
| root async builtins (waitable-set, ...) | ✓ | used implicitly by async imports |
The project covers the canonical ABI for WIT 0.2.x and 0.3.x in both directions, but two gaps remain:
-
Stream/future intrinsics for sync exports. The per-function intrinsics namespaces are generated for:
- imports
- imported-resource methods
- async exports
But if a sync export or exported resource method mentions
stream<T>orfuture<T>, it doesn't get an intrinsics namespace yet. No WASI world ships such a function, so there's no consumer to check it against.Async exports are fully supported, including:
- eager functions
- typed state machines
See docs/bindings.md.
-
HTTP request bodies in the
wasi3blocking wrapper. The 0.3 host only answers after the request body stream is dropped, but you can only write the body whileclient.sendis in flight. A blocking helper can't interleave those operations.To send a body, you need the state-machine async export form.
wasi3.http.Requestdeliberately has nobodyfield, so trying to use one gives you a compile error.