A fork of bytecodealliance/wasm-micro-runtime ported from C to Zig and maintained with AI assistance. It passes the WebAssembly/spec test suite of 20k+ tests. It supports the Component Model. It has a very fast cold start, small engine binary size with no dependencies, and is easy to build & fork.
Wasmtime is currently about 2.1x faster in CoreMark steady-state throughput. For cold start, this repo's in-process JIT mode is instead ~4.9x faster than wasmtime's default run on small modules — see docs/bench/jit-cold-start-comparison-2026-07-10.md for the full measured comparison (AOT, JIT .fast/.full presets, and wasmtime, across both cold-start and steady-state axes). Wasmtime has years of production usage and use with a proven track record and security audits.
Install pre-built binaries from GitHub Releases with ghr:
$ ghr install cataggar/wamrSee INSTALL.md for alternative installation methods (winget, uv, pip) and detailed instructions.
- wamrc: AOT compiler — compile a
.wasmmodule to a native.cwasmbinary (wamrc compile foo.wasm) - wamr: run a precompiled
.cwasmfile (wamr run foo.cwasm), or a plain.wasmmodule directly when built with-Djit=true— see JIT mode below
By default wamr is AOT-only: it links only the runtime, not the
compiler, so it stays small (~17 MB) and requires a precompiled
.cwasm artifact for every module. Building with -Djit=true instead
links the compiler in and adds an opt-in in-process JIT: wamr run foo.wasm compiles the module in memory and executes it in the same
process, one command, no .cwasm artifact ever hits disk —
wasmtime run-style ergonomics, at the cost of
a bigger binary (~25 MB) and per-invocation compile latency.
| AOT-only (default) | JIT (-Djit=true) |
|
|---|---|---|
| Build | zig build -Doptimize=ReleaseSafe |
zig build -Doptimize=ReleaseSafe -Djit=true |
| Core wasm | wamrc compile foo.wasm → wamr run foo.cwasm |
wamr run foo.wasm |
| Component | wamrc compile-component foo.wasm → wamr run foo.wasm (auto-detects the sidecar manifest) |
wamr run foo.wasm (no manifest needed) |
| One-shot testing | wamrc run foo.wasm (compiles, then spawns wamr as a subprocess) |
wamr run foo.wasm (single process, no subprocess) |
| Binary size / compiler | Small; no compiler linked in | Larger; compiler linked in |
Both flows produce identical guest-observable behavior — the JIT path
reuses the exact same compiler (src/compiler) and AOT loader/runtime
(src/runtime/aot) as the two-step flow, just without the disk
round-trip. wamr serve (the wasi:http server) supports the same
JIT fallback for components with no sidecar manifest. See issue
#863 for the full plan
and design rationale.
By default the JIT compiles with a fast/baseline pass preset tuned for
low compile latency rather than peak steady-state throughput (set
WAMR_JIT_FULL_OPT=1 to opt into the same fully-optimized pipeline
wamrc compile uses, at the cost of compile latency). See
docs/bench/jit-cold-start-comparison-2026-07-10.md
for measured cold-start and throughput numbers across every mode
(two-step AOT, wamrc run, both JIT presets, and wasmtime).
Requires Zig 0.16. No other dependencies.
$ git clone https://github.com/cataggar/wamr
$ cd wamr
$ zig buildFor release builds:
$ zig build -Doptimize=ReleaseSafeReleaseSafe is the primary source-build recommendation. The 2026-05-29 optimize-mode comparison shows cold-start noop.cwasm at ×0.54 Safe/Fast and SIMD interpreter rows at ×0.99 median on the project VM, while also showing ReleaseSafe catching a CoreMark AOT compiler invariant before timing; see docs/bench/optimize-mode-comparison-2026-05-29.md.
Cross-compilation works out of the box:
$ zig build -Dtarget=aarch64-linux -Doptimize=ReleaseSafe
$ zig build -Dtarget=aarch64-macos -Doptimize=ReleaseSafe
$ zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSafeUnit tests:
$ zig build testSpec tests:
$ zig build
$ ./zig-out/bin/spec-test-runner tests/spec-jsonWASI conformance (WebAssembly/wasi-testsuite):
$ git submodule update --init tests/wasi-testsuite
$ pip install -r tests/wasi-testsuite/test-runner/requirements.txt
$ zig build wasi-testsuite # WASI Preview 1 + Preview 2 — 70 / 72 passing
$ zig build wasi-p3-testsuite-unfiltered # WASI Preview 3 — 41 / 41 passing
$ zig build wasi-p3-testsuite-jit-unfiltered -Djit=true # Same 41-fixture contract with no sidecars
$ zig build wasi-p3-parity # Same fixtures via wamr + wasmtime, diff the reportsThe upstream do_wait timeout is 5s; that matches GitHub Actions runner
timings but is tight on slow developer VMs (e.g. http-fields takes
~11s on the project Azure dev VM). Set WAMR_TESTSUITE_TIMEOUT=<seconds>
to override it — see
tests/wasi-testsuite-runner-patch/
(#583 A7).
The suite drives the freshly-built wamr CLI through the in-tree adapter at
tests/wasi-testsuite-adapter/wamr-zig.py
and applies the curated expectations at
tests/wasi-testsuite-expectations.toml (Preview 1 / 2)
and tests/wasi-p3-testsuite-expectations.toml
(Preview 3, currently empty). The unfiltered P3 contract requires all 41
fixtures to execute and pass. Every entry
in either expectation file must carry a one-line rationale and a follow-up issue number.
When a previously-skipped test starts passing, delete the entry — the suite is
the gate against regressions in already-shipped WASI host functions.
zig build wasi-p3-parity is the cross-runtime gate (#583
C1): it runs the same
wasm32-wasip3 corpus through wamr and upstream
Wasmtime (CI pin v46.0.1, matching the
updated upstream suite) and diffs the JSON reports via
scripts/diff-testsuite-reports.py.
The classifier exits non-zero on:
- Regressions — wamr fails a fixture that Wasmtime still passes (a true wamr regression).
- Stale skip-list entries — a fixture listed in
tests/wasi-p3-parity-skip.jsonis no longer in the wamr-pass / Wasmtime-fail shape (e.g. the upstream Wasmtime / wasi-testsuite fix has landed and the entry must be retired). - Undocumented Wasmtime-side bugs under
--strict— a fixture wamr passes but Wasmtime fails for which the skip-list has no tracking entry.
The currently empty skip-list at
tests/wasi-p3-parity-skip.json
maps each known Wasmtime-side or fixture-side bug to an upstream
tracking issue. Entries listed there are documented deltas — they
are reported on stderr but never fail the parity gate. Wasmtime
v46.0.1 passes all 41 fixtures in the updated corpus. To document a new Wasmtime delta,
file an upstream issue at bytecodealliance/wasmtime (or
WebAssembly/wasi-testsuite for fixture bugs) and add an entry
keyed by fixture name with the tracking URL as the value. The CI
workflow at
.github/workflows/wasi-p3-parity.yml
runs the gate on push to main and nightly and is required — a
new wamr regression or new undocumented Wasmtime delta blocks the
merge queue.
wamr ships the WASI 0.2.x and 0.3.0 interface surface (wasi:cli,
wasi:clocks, wasi:filesystem, wasi:http, wasi:io, wasi:random,
wasi:sockets). The Preview 1 gate is green (zig build wasi-testsuite
— 70 / 72 Preview 1 fixtures; the 2 skips are a narrow
environ-inheritance behavioral mismatch and a flaky upstream fixture
that compares clock reads without accounting for second rollover, not
crashes). The Preview 3
gate (zig build wasi-p3-testsuite-unfiltered) executes all 41 fixtures:
all 41 pass through both manifest AOT and no-sidecar JIT after
#881 and
#905.
Outbound HTTP and
HTTPS issue real requests via std.http.Client and Zig 0.16's
std.crypto.tls.
See docs/wasi.md for the full feature matrix —
interface → version → method count → fixture pass-rate → known
limitations — and #583
for post-Preview-3 hardening items.
To exercise the real outbound HTTPS path in unit tests (off by default so CI stays hermetic):
$ zig build test -Dnetwork_tests=trueThe wasi:http/handler@0.3.0 incoming-handler server (PRs #580 + #595)
accepts plaintext HTTP/1.1 — keep-alive, chunked Transfer-Encoding,
response trailers, and 431 / 413 oversize limits are all in. HTTPS
termination (#609) is
live:
$ wamr serve --addr <addr> --tls-cert=<cert.pem> --tls-key=<key.pem> app.wasm
$ wamr serve --addr <addr> --tls-pem=<combined.pem> app.wasm # cert + key in one fileThe cert chain (std.crypto.Certificate.Bundle) and PEM-encoded private
key (PKCS#8 / RSA / EC) are parsed + validated at startup, so a missing
file, malformed PEM, or key/cert mismatch surfaces before bind(2).
Each accepted connection is upgraded with a TLS 1.3 server-side handshake
(RSA and ECDSA server certificates) before HTTP/1.1 is served over the
encrypted stream. Server-side TLS comes from the pure-Zig
cataggar/tls.zig dependency —
Zig 0.16's std.crypto.tls ships only a client. When --tls-cert /
--tls-pem is omitted the listener serves plaintext HTTP/1.1.
wasi:config@0.2.0-rc.1 (store.get / store.get-all) is wired
through a layered host adapter (#583 B6). Guest components import
wasi:config/store@0.2.0-rc.1 and read string → string pairs from
two sources:
- Environment variables prefixed with
WAMR_CONFIG_. The prefix is stripped and the remainder lower-cased ASCII (WAMR_CONFIG_API_KEY=secret→api_key=secret). - JSON file passed via
--config-store=PATH. The file must be a flat object of string values ({"key":"value", …}); nested objects, arrays, or non-string scalars are rejected at startup.
Precedence: file overrides env. When the same lower-cased key appears in both layers, the env entry is dropped and the file value wins. Surviving env-only entries are appended after the file entries.
$ cat config.json
{ "host": "api.example.com", "timeout_ms": "5000" }
$ WAMR_CONFIG_HOST=localhost WAMR_CONFIG_DEBUG=1 \
wamr run --config-store=config.json my-component.wasm
# Guest sees: host=api.example.com (file wins), timeout_ms=5000 (file),
# debug=1 (env-only).The in-memory store never surfaces the error arms (upstream /
io) defined by wasi:config@0.2.0-rc.1 — every lookup returns
Ok(Some) or Ok(None). Those arms are reserved for future Vault /
Kubernetes ConfigMaps / etc. back-ends.