wick is a small, dependency-free WebAssembly interpreter for embedding plugin systems in Zig applications.
It runs wasm 1.0 core modules by direct interpretation — no JIT, no codegen, no libc. An std.mem.Allocator is the only thing it asks of its host. It's built for one job: letting an application execute third-party code where the host controls every doorway (imports are the only way out of the sandbox) and every budget (memory ceiling, call depth, instruction fuel).
Highlights
One-call setup, typed calls in both directions.
var plugin = try wick.Plugin.init(allocator, wasm_bytes, .{
.imports = &imports,
.user_data = &host,
.limits = .{ .fuel = 10_000_000, .max_memory_pages = 64 },
});
defer plugin.deinit();
const add = try plugin.func("add", fn (i32, i32) i32);
const sum = try add.call(.{ 2, 3 });wick.bind generates host-function argument unpacking from native Zig signatures (including []const u8 parameters resolved through guest memory with bounds checks); wick.guest.func does the mirror image for guest exports, checking signatures at resolve time so a missing or mismatched export fails before any guest code runs.
Contained by construction. Per-instance Limits bound instruction fuel, call depth, value-stack size, and linear memory. A runaway plugin returns error.OutOfFuel instead of hanging the host, and the instance stays usable afterward. The decoder bounds every section count against the bytes backing it, so a forged header can't drive a giant allocation.
Fast for an interpreter. Function bodies are translated once at decode time into pre-decoded IR — immediates unpacked, branch targets resolved to direct indices, stack heights validated — then run through a threaded-dispatch loop over preallocated stacks. Nothing inside the interpreter loop allocates. Roughly 800–1550 MIPS and 126M host calls/second on the bundled benchmarks. See ROADMAP.md for the optimization phases, their measurements, and one phase that was measured and rejected.
Getting started
zig fetch --save git+https://github.com/ooyeku/wick#v0.1.0Requires Zig 0.16.0 or newer. zig build example compiles a real Zig plugin to wasm32-freestanding and runs a host that drives it — typed calls, host callbacks, guest-side allocation, direct memory reads, and fuel containment of a runaway export.
Scope
Implemented: full control flow, i32/i64/f32/f64 arithmetic and conversions (including trapping and saturating float→int truncations), linear memory with bounded growth, globals, funcref tables with runtime-type-checked call_indirect, active data and element segments, the start function, and the bulk-memory ops LLVM toolchains emit.
Not implemented for now: SIMD, threads, multi-memory, reference types beyond funcref tables, and WASI. Modules using them fail with a typed error rather than running incorrectly.
Verified against the official WebAssembly spec test suite (zig build spec), plus unit tests and decode/execute fuzz targets.