Introduction
wasmz is a WebAssembly runtime written in Zig, designed to be fast, compact, and easy to embed. It implements a full-featured interpreter with support for modern WebAssembly proposals.
Real-World Testing
wasmz passes real-world WebAssembly module tests:
- esbuild - JavaScript bundler compiled to WASM
- QuickJS - Lightweight JavaScript engine compiled to WASM
- SQLite - Database engine compiled to WASM
These integration tests validate wasmz’s compatibility with production WASM workloads.
WebAssembly Support
wasmz supports all current WebAssembly proposals.
- MVP - All core instructions and validation rules
- Multi-value - Functions and blocks with multiple return values
- Bulk operations - Memory and table bulk operations
- Sign-extension - Sign-extension instructions
- GC - Structs, arrays, and reference types with automatic memory management
- SIMD - 128-bit vector operations
- Exception Handling - Both legacy and new proposal formats
- Threading - Shared memory and atomic operations
WASI Support
WASI Preview 1 - Full implementation
- File system operations (fd_read, fd_write, path_open, etc.)
- Socket operations (sock_accept, sock_recv, sock_send, etc.)
- Environment variables and arguments
- Random number generation
- Process control (proc_exit, proc_raise)
- Clock and time operations
- Polling (poll_oneoff)
Embedding
- Zig API - Native Zig interface with full type safety
- C API - Minimal C ABI for embedding in any language
- CLI tool - Standalone command-line runner
Project Structure
wasmz/
├── src/
│ ├── root.zig # Public API entry point
│ ├── main.zig # CLI implementation
│ ├── capi.zig # C API implementation
│ ├── core/ # Core data types
│ ├── parser/ # WASM binary parser
│ ├── compiler/ # Stack-to-register compiler
│ ├── engine/ # Execution engine
│ ├── vm/ # Virtual machine
│ ├── wasmz/ # High-level API
│ └── wasi/ # WASI implementation
├── include/
│ └── wasmz.h # C API header
├── tests/ # Integration tests
└── docs/ # Documentation
Dependencies
wasmz has minimal dependencies:
- zigrc: Reference counting implementation (inlined in
src/libs/) - libc: Only for the C API
No external libraries or system dependencies are required for the core runtime.
Zig Version
wasmz requires Zig 0.15.2 or compatible version.
License
MIT License - see LICENSE file for details.
Getting Started
This chapter covers installing wasmz and running your first WebAssembly program.
Quick Start
# Clone and build
git clone https://github.com/anomalyco/wasmz.git
cd wasmz
zig build
# Run a WASM file
./zig-out/bin/wasmz examples/hello.wasm
# Call a specific function
./zig-out/bin/wasmz examples/add.wasm add 3 4
Next: Installation for detailed setup instructions.
Installation
Prerequisites
- Zig 0.15.2 - Download from ziglang.org
- Git - For cloning the repository
- make - For build commands
Build from Source
# Clone the repository
git clone https://github.com/anomalyco/wasmz.git
cd wasmz
# Build (ReleaseSafe - recommended for development)
make build
# Build for debugging
make build-debug
# Build for maximum performance
make release
The binary will be at zig-out/bin/wasmz.
Build Options
| Command | Description |
|---|---|
make build-debug | Debug build (unoptimized, fast compile) |
make build | ReleaseSafe build (optimized, safety checks) |
make release | ReleaseFast build (maximum performance) |
make test | Run all unit tests |
make clib | Build C shared library |
Build Mode Differences
The build mode affects panic handling in the CLI binary:
| Mode | Panic Handler | Binary Size | Backtrace |
|---|---|---|---|
| Debug | Full panic handler | Larger | ✅ Readable stack trace |
| ReleaseSafe | Full panic handler | Larger | ✅ Readable stack trace |
| ReleaseFast | Minimal panic handler | ~127 KB smaller | ❌ No backtrace |
| ReleaseSmall | Minimal panic handler | ~127 KB smaller | ❌ No backtrace |
Recommendation: Use make build (ReleaseSafe) for development - it provides optimizations while keeping safety checks and readable error messages. Use make release (ReleaseFast) for production deployments where binary size matters.
Running Tests
# Run all unit tests
make test
# Run integration tests (requires building fixtures first)
zig build sqlite-wasm # Build SQLite WASM fixture
zig build test-sqlite # Run SQLite integration tests
Installation
Install to ~/.local/bin:
# Install debug build
make install
# Install release build
make install-release
After installation, ensure ~/.local/bin is in your PATH.
C Library
Build the C shared library for embedding:
make clib
Output files:
zig-out/lib/libwasmz.so(Linux)zig-out/lib/libwasmz.dylib(macOS)zig-out/lib/libwasmz.dll(Windows)zig-out/include/wasmz.h(header)
Verify Installation
# Check the CLI
wasmz --help
# Or run directly
./zig-out/bin/wasmz --help
# Run a simple test
echo '(module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))' > test.wat
wat2wasm test.wat
wasmz test.wasm add 1 2
# Output: 3
CLI Usage
The wasmz command-line tool runs WebAssembly modules directly from the terminal.
Basic Usage
# List exported functions
wasmz <file.wasm>
# Call a function with i32 arguments
wasmz <file.wasm> <func_name> [i32_args...]
# Run _start with WASI arguments
wasmz <file.wasm> --args "<wasm_args...>"
Examples
List Exports
$ wasmz module.wasm
Exported functions:
add
multiply
greet
Call a Function
# Call "add" with arguments 3 and 4
$ wasmz module.wasm add 3 4
7
WASI Command Model
# Run _start with arguments passed to the WASM module
$ wasmz program.wasm --args "--verbose --output=result.txt"
Reactor Mode
# Initialize reactor and call a function
$ wasmz library.wasm --reactor --func process -- input.txt
Flags
| Flag | Description |
|---|---|
--legacy-exceptions | Use legacy exception handling proposal |
--args "<string>" | Arguments to pass to WASM module (space-separated) |
--func <name> | Exported function to call |
--reactor | Call _initialize before the function |
--mem-stats | Print memory usage after execution |
--mem-limit <MB> | Set memory limit in megabytes |
Memory Statistics
Use --mem-stats to analyze memory usage:
$ wasmz program.wasm --mem-stats
Memory usage:
Linear memory: 1.00 MB (16 pages)
GC heap: 0.12 MB (used 45.2 KB / capacity 128.0 KB)
Shared memory: 0.00 MB (none)
────────────────────────────────
Total: 1.12 MB
Error Handling
When a trap occurs, wasmz prints the trap message:
$ wasmz module.wasm divide 1 0
error: trap: integer divide by zero
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error (file not found, compilation error, etc.) |
| N | WASI proc_exit code (if called by module) |
Zig API
This section documents the Zig API for embedding wasmz in your applications.
Overview
const std = @import("std");
const wasmz = @import("wasmz");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Create engine
var engine = try wasmz.Engine.init(allocator, .{});
defer engine.deinit();
// Compile module
const bytes = try std.fs.cwd().readFileAlloc(allocator, "module.wasm", 1024 * 1024);
defer allocator.free(bytes);
var module = try wasmz.Module.compile(engine, bytes);
defer module.deinit();
// Create store and instance
var store = try wasmz.Store.init(allocator, engine);
defer store.deinit();
var instance = try wasmz.Instance.init(&store, module, .empty);
defer instance.deinit();
// Call function
const result = try instance.call("add", &.{ .{ .i32 = 1 }, .{ .i32 = 2 } });
std.debug.print("Result: {d}\n", .{result.ok.?.readAs(i32)});
}
Core Types
| Type | Description |
|---|---|
Engine | Runtime engine with configuration |
Config | Engine configuration options |
Module | Compiled WebAssembly module |
Store | Runtime context for instances |
Instance | Instantiated module with memory/globals |
Linker | Host function registry |
HostFunc | Host-provided callable |
RawVal | Generic value (i32/i64/f32/f64) |
ExecResult | Execution result (ok or trap) |
Trap | Runtime trap with code |
Next Steps
- Engine & Config - Setting up the runtime
- Module - Compiling WebAssembly
- Store & Instance - Running modules
- Linker & Host Functions - Host integration
- Error Handling - Traps and errors
Engine & Config
The Engine is the core runtime that manages compilation, configuration, and shared resources.
Engine
Initialization
const wasmz = @import("wasmz");
// Default configuration
var engine = try wasmz.Engine.init(allocator, .{});
defer engine.deinit();
// With configuration
var engine = try wasmz.Engine.init(allocator, .{
.legacy_exceptions = true,
.mem_limit_bytes = 256 * 1024 * 1024, // 256 MB limit
});
defer engine.deinit();
Methods
| Method | Description |
|---|---|
init(allocator, config) | Create engine with config |
deinit() | Free engine resources |
Config
Configuration options for the engine:
const Config = struct {
/// Use legacy exception handling proposal
legacy_exceptions: bool = false,
/// Memory limit in bytes (null = unlimited)
mem_limit_bytes: ?u64 = null,
};
Options
| Field | Type | Default | Description |
|---|---|---|---|
legacy_exceptions | bool | false | Use legacy EH proposal |
mem_limit_bytes | ?u64 | null | Max memory allocation |
Example: Memory Limit
var engine = try wasmz.Engine.init(allocator, .{
.mem_limit_bytes = 128 * 1024 * 1024, // 128 MB
});
Example: Legacy Exceptions
For modules using the older exception handling proposal:
var engine = try wasmz.Engine.init(allocator, .{
.legacy_exceptions = true,
});
Thread Safety
- Engine - Reference counting is thread-safe (uses
zigrc.Arc). Multiple threads can clone/deinit independently. - Config - Immutable after creation, safe to share across threads.
Note: While reference counting is atomic, concurrent access to the same Engine instance (e.g., calling config()) is not synchronized. Create separate Engine instances per thread if needed.
Lifecycle
The engine must outlive any stores or modules created from it:
var engine = try wasmz.Engine.init(allocator, .{});
defer engine.deinit(); // Must be called after store/module deinit
var store = try wasmz.Store.init(allocator, engine);
defer store.deinit();
var module = try wasmz.Module.compile(engine, bytes);
defer module.deinit();
Module
A Module represents a compiled WebAssembly module (read-only).
Compilation
Basic Compilation
const bytes = try std.fs.cwd().readFileAlloc(allocator, "module.wasm", max_size);
defer allocator.free(bytes);
var module = try wasmz.Module.compile(engine, bytes);
defer module.deinit();
Arc Module (Reference Counted)
For sharing modules across multiple instances:
// Compile with reference counting
var arc_module = try wasmz.Module.compileArc(engine, bytes);
defer if (arc_module.releaseUnwrap()) |m| {
var mod = m;
mod.deinit();
};
// Retain for each instance
var instance = try wasmz.Instance.init(&store, arc_module.retain(), linker);
Methods
| Method | Description |
|---|---|
compile(engine, bytes) | Compile module from bytes |
compileArc(engine, bytes) | Compile with ref counting |
deinit() | Free module resources |
retain() | Increment ref count (Arc only) |
releaseUnwrap() | Decrement ref count (Arc only) |
Module Information
Exports
var iter = module.exports.iterator();
while (iter.next()) |entry| {
std.debug.print("Export: {s}\n", .{entry.key_ptr.*});
}
// Check for specific export
if (module.exports.get("_start")) |_| {
// Command module
}
Imports
for (module.imported_funcs) |import| {
std.debug.print("Import: {s}::{s}\n", .{ import.module_name, import.func_name });
}
Validation
Modules are validated during compilation. Errors are returned:
var module = wasmz.Module.compile(engine, bytes) catch |err| {
switch (err) {
error.InvalidWasm => std.debug.print("Invalid WASM binary\n", .{}),
error.OutOfMemory => std.debug.print("Out of memory\n", .{}),
else => return err,
}
return;
};
Module Types
Command vs Reactor
| Type | Entry Point | Description |
|---|---|---|
| Command | _start | Runs once, may call proc_exit |
| Reactor | _initialize | Library, multiple function calls |
if (module.exports.get("_start")) |_| {
// Command module
} else if (module.exports.get("_initialize")) |_| {
// Reactor module
}
Thread Safety
- Module - Not thread-safe for concurrent access.
- ArcModule - Reference counting is thread-safe (uses
zigrc.Arc). Multiple threads can safely callretain()/releaseUnwrap().
While ArcModule’s reference counting is atomic, the underlying Module data should not be accessed concurrently. Create separate ArcModule handles per thread and avoid sharing the same Module across threads.
Memory Management
- compile(): Module owned by caller, call
deinit()when done - compileArc(): Reference counted, use
retain()/releaseUnwrap()for sharing
// Single instance - use compile()
var module = try wasmz.Module.compile(engine, bytes);
defer module.deinit();
// Multiple instances - use compileArc()
var arc = try wasmz.Module.compileArc(engine, bytes);
var inst1 = try wasmz.Instance.init(&store1, arc.retain(), linker);
var inst2 = try wasmz.Instance.init(&store2, arc.retain(), linker);
// When done
_ = arc.releaseUnwrap(); // Decrements and frees if zero
Store & Instance
The Store holds runtime context, and Instance is an instantiated module.
Store
The store manages the allocator, engine reference, and runtime state.
Initialization
var store = try wasmz.Store.init(allocator, engine);
defer store.deinit();
Properties
| Property | Type | Description |
|---|---|---|
gc_heap | GCHeap | Garbage-collected heap |
memory_budget | MemoryBudget | Memory tracking |
Memory Budget
Link the memory budget for enforcement:
store.linkBudget(); // Enable memory limits
Instance
Initialization
var instance = try wasmz.Instance.init(&store, module, linker);
defer instance.deinit();
Command Model
Run _start:
if (try instance.runStartFunction()) |result| {
switch (result) {
.ok => std.debug.print("Success\n", .{}),
.trap => |t| std.debug.print("Trap: {s}\n", .{t.code}),
}
}
Reactor Model
Initialize and call functions:
// Call _initialize if present
if (try instance.initializeReactor()) |result| {
switch (result) {
.trap => |t| return error.InitFailed,
.ok => {},
}
}
// Call exported function
const result = try instance.call("process", &args);
Methods
| Method | Description |
|---|---|
init(store, module, linker) | Create instance |
deinit() | Free instance |
runStartFunction() | Run _start if present |
initializeReactor() | Run _initialize if present |
call(name, args) | Call exported function |
isCommand() | Returns true if exports _start |
isReactor() | Returns true if no _start export |
Linker
The linker provides host functions to the instance.
Creating Linker
var linker = wasmz.Linker.empty;
defer linker.deinit(allocator);
Adding Host Functions
fn host_add(ctx: ?*anyopaque, hc: *wasmz.HostContext, params: []const wasmz.RawVal, results: []wasmz.RawVal) wasmz.HostError!void {
const a = params[0].readAs(i32);
const b = params[1].readAs(i32);
results[0] = wasmz.RawVal.from(a + b);
}
try linker.define(allocator, "env", "add", wasmz.HostFunc.init(
null,
host_add,
&[_]wasmz.ValType{ .I32, .I32 },
&[_]wasmz.ValType{.I32},
));
RawVal
Generic value type for all numeric WASM types:
const RawVal = wasmz.RawVal;
// Creating values
const v1 = RawVal.from(@as(i32, 42));
const v2 = RawVal.from(@as(i64, 1000000));
const v3 = RawVal.from(@as(f32, 3.14));
const v4 = RawVal.from(@as(f64, 3.14159265359));
// Reading values
const i = v1.readAs(i32);
const j = v2.readAs(i64);
const f = v3.readAs(f32);
const d = v4.readAs(f64);
ExecResult
Result of function execution:
const result = try instance.call("add", &.{ v1, v2 });
switch (result) {
.ok => |val| {
if (val) |v| {
std.debug.print("Result: {d}\n", .{v.readAs(i32)});
}
},
.trap => |t| {
std.debug.print("Trap: {s}\n", .{@tagName(t.code)});
},
}
Complete Example
const std = @import("std");
const wasmz = @import("wasmz");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var engine = try wasmz.Engine.init(allocator, .{});
defer engine.deinit();
const bytes = try std.fs.cwd().readFileAlloc(allocator, "add.wasm", 1024 * 1024);
defer allocator.free(bytes);
var module = try wasmz.Module.compile(engine, bytes);
defer module.deinit();
var store = try wasmz.Store.init(allocator, engine);
defer store.deinit();
var instance = try wasmz.Instance.init(&store, module, .empty);
defer instance.deinit();
const result = try instance.call("add", &.{
RawVal.from(@as(i32, 3)),
RawVal.from(@as(i32, 4)),
});
if (result.ok) |val| {
std.debug.print("3 + 4 = {d}\n", .{val.readAs(i32)});
}
}
Thread Safety
- Store - Not thread-safe. Contains GC heap and mutable runtime state.
- Instance - Not thread-safe. Contains globals, memory, and execution state.
Create separate Store/Instance per thread for parallel execution. The ArcModule reference can be safely shared (retain/release is atomic), but each thread should have its own Store and Instance.
Linker & Host Functions
The linker connects WASM imports to host-provided functions.
Linker
Creating a Linker
var linker = wasmz.Linker.empty;
defer linker.deinit(allocator);
Defining Functions
try linker.define(
allocator,
"module_name", // Import module name
"func_name", // Import function name
wasmz.HostFunc.init(
null, // Context (optional)
host_function, // Function pointer
&[_]wasmz.ValType{ .I32 }, // Parameter types
&[_]wasmz.ValType{ .I32 }, // Result types
),
);
Methods
| Method | Description |
|---|---|
define(alloc, module, name, func) | Register a host function |
get(module, name) | Look up a function |
deinit(alloc) | Free linker resources |
HostFunc
Creating Host Functions
fn host_print(
ctx: ?*anyopaque,
hc: *wasmz.HostContext,
params: []const wasmz.RawVal,
results: []wasmz.RawVal,
) wasmz.HostError!void {
const value = params[0].readAs(i32);
std.debug.print("Value: {d}\n", .{value});
// No return value - results is empty
}
With Context
const MyContext = struct {
counter: u32,
};
fn host_increment(
ctx: ?*anyopaque,
hc: *wasmz.HostContext,
params: []const wasmz.RawVal,
results: []wasmz.RawVal,
) wasmz.HostError!void {
const my_ctx: *MyContext = @ptrCast(@alignCast(ctx.?));
my_ctx.counter += params[0].readAs(u32);
results[0] = wasmz.RawVal.from(@as(i32, @intCast(my_ctx.counter)));
}
var my_ctx = MyContext{ .counter = 0 };
try linker.define(allocator, "env", "increment", wasmz.HostFunc.init(
&my_ctx,
host_increment,
&[_]wasmz.ValType{.I32},
&[_]wasmz.ValType{.I32},
));
HostContext
The HostContext provides access to runtime state:
fn host_memory_access(
ctx: ?*anyopaque,
hc: *wasmz.HostContext,
params: []const wasmz.RawVal,
results: []wasmz.RawVal,
) wasmz.HostError!void {
// Access instance memory
const offset = @as(usize, @intCast(params[0].readAs(u32)));
const memory = hc.instance.memory;
const byte = memory.readByte(offset);
results[0] = wasmz.RawVal.from(@as(i32, byte));
}
Properties
| Property | Description |
|---|---|
instance | Access to the instance |
store | Access to the store |
ValType
Value types for function signatures:
const ValType = enum {
I32,
I64,
F32,
F64,
FuncRef,
ExternRef,
};
WASI Integration
Add WASI functions to the linker:
const wasi = @import("wasi").preview1;
var wasi_host = wasi.Host.init(allocator);
defer wasi_host.deinit();
// Set command-line arguments
try wasi_host.setArgs(&[_][]const u8{ "program.wasm", "--verbose" });
// Add to linker
try wasi_host.addToLinker(&linker, allocator);
Error Handling from Host
Return errors from host functions:
fn host_may_fail(
ctx: ?*anyopaque,
hc: *wasmz.HostContext,
params: []const wasmz.RawVal,
results: []wasmz.RawVal,
) wasmz.HostError!void {
const value = params[0].readAs(i32);
if (value < 0) {
return wasmz.HostError.Trap; // Will cause a trap
}
results[0] = wasmz.RawVal.from(value * 2);
}
Complete Example
const std = @import("std");
const wasmz = @import("wasmz");
fn host_add(
_: ?*anyopaque,
_: *wasmz.HostContext,
params: []const wasmz.RawVal,
results: []wasmz.RawVal,
) wasmz.HostError!void {
const a = params[0].readAs(i32);
const b = params[1].readAs(i32);
results[0] = wasmz.RawVal.from(a + b);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Setup
var engine = try wasmz.Engine.init(allocator, .{});
defer engine.deinit();
var store = try wasmz.Store.init(allocator, engine);
defer store.deinit();
// Create linker with host function
var linker = wasmz.Linker.empty;
defer linker.deinit(allocator);
try linker.define(allocator, "env", "host_add", wasmz.HostFunc.init(
null,
host_add,
&[_]wasmz.ValType{ .I32, .I32 },
&[_]wasmz.ValType{.I32},
));
// Load and run WASM that imports "env::host_add"
const bytes = try std.fs.cwd().readFileAlloc(allocator, "module.wasm", 1024 * 1024);
defer allocator.free(bytes);
var module = try wasmz.Module.compile(engine, bytes);
defer module.deinit();
var instance = try wasmz.Instance.init(&store, module, linker);
defer instance.deinit();
_ = try instance.runStartFunction();
}
Error Handling
Traps
A Trap represents a runtime error in WebAssembly execution.
TrapCode
pub const TrapCode = enum {
Unreachable,
IntegerDivisionByZero,
IntegerOverflow,
IndirectCallToNull,
UndefinedElement,
UninitializedElement,
OutOfBoundsMemoryAccess,
OutOfBoundsTableAccess,
IndirectCallTypeMismatch,
StackOverflow,
OutOfMemory,
// ... more codes
};
ExecResult
Function calls return an ExecResult:
const result = try instance.call("func", &args);
switch (result) {
.ok => |val| {
// Success - val may be null for void functions
if (val) |v| {
std.debug.print("Result: {d}\n", .{v.readAs(i32)});
}
},
.trap => |trap| {
// Trap occurred
std.debug.print("Trap: {s}\n", .{@tagName(trap.code)});
// Get detailed message
const msg = try trap.allocPrint(allocator);
defer allocator.free(msg);
std.debug.print("Details: {s}\n", .{msg});
},
}
Trap Message
Get a human-readable trap message:
if (result.trap) |trap| {
const msg = try trap.allocPrint(allocator);
defer allocator.free(msg);
std.debug.print("Trap: {s}\n", .{msg});
}
Host Errors
Host functions can return errors:
fn host_divide(
_: ?*anyopaque,
_: *wasmz.HostContext,
params: []const wasmz.RawVal,
results: []wasmz.RawVal,
) wasmz.HostError!void {
const a = params[0].readAs(i32);
const b = params[1].readAs(i32);
if (b == 0) {
return wasmz.HostError.Trap;
}
results[0] = wasmz.RawVal.from(@divTrunc(a, b));
}
Common Errors
Module Compilation
var module = wasmz.Module.compile(engine, bytes) catch |err| {
switch (err) {
error.InvalidWasm => {
std.debug.print("Invalid WASM binary\n", .{});
},
error.UnsupportedFeature => {
std.debug.print("Feature not supported\n", .{});
},
error.OutOfMemory => {
std.debug.print("Out of memory\n", .{});
},
else => return err,
}
return;
};
Instantiation
var instance = wasmz.Instance.init(&store, module, linker) catch |err| {
switch (err) {
error.ImportNotSatisfied => {
std.debug.print("Missing imports:\n", .{});
for (module.imported_funcs) |imp| {
if (linker.get(imp.module_name, imp.func_name) == null) {
std.debug.print(" {s}::{s}\n", .{ imp.module_name, imp.func_name });
}
}
},
error.ImportSignatureMismatch => {
std.debug.print("Import signature mismatch\n", .{});
},
else => return err,
}
return;
};
Memory Limit
When memory limit is exceeded:
var engine = try wasmz.Engine.init(allocator, .{
.mem_limit_bytes = 64 * 1024 * 1024, // 64 MB
});
// If WASM tries to grow memory beyond limit:
// result.trap.code == .OutOfMemory
Stack Overflow
When call stack is exhausted:
// Recursive function without base case
// result.trap.code == .StackOverflow
C API
The C API allows embedding wasmz in any C-compatible language.
Header
#include <wasmz.h>
Lifecycle
Engine
// Create engine
wasmz_engine_t *engine = wasmz_engine_new();
// With memory limit
wasmz_engine_t *engine = wasmz_engine_new_with_limit(256 * 1024 * 1024);
// Destroy
wasmz_engine_delete(engine);
Store
wasmz_store_t *store = wasmz_store_new(engine);
// ... use store ...
wasmz_store_delete(store);
Module
// Load bytes
uint8_t *bytes = /* ... */;
size_t len = /* ... */;
// Compile
wasmz_module_t *module = NULL;
wasmz_error_t *err = wasmz_module_new(engine, bytes, len, &module);
if (err) {
printf("Error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
return;
}
// Destroy
wasmz_module_delete(module);
Instance
wasmz_instance_t *instance = NULL;
wasmz_error_t *err = wasmz_instance_new(store, module, &instance);
if (err) {
printf("Error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
return;
}
// Destroy
wasmz_instance_delete(instance);
Values
typedef struct {
wasmz_val_kind_t kind; // I32, I64, F32, F64
union {
int32_t i32;
int64_t i64;
float f32;
double f64;
} of;
} wasmz_val_t;
// Constructors
wasmz_val_t v = wasmz_val_i32(42);
wasmz_val_t v = wasmz_val_i64(1000000);
wasmz_val_t v = wasmz_val_f32(3.14f);
wasmz_val_t v = wasmz_val_f64(3.14159);
Function Calls
Command Model
// Run _start
wasmz_error_t *err = wasmz_instance_call_start(instance);
if (err) {
printf("Error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
}
Reactor Model
// Initialize
wasmz_error_t *err = wasmz_instance_initialize(instance);
if (err) {
printf("Init error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
return;
}
// Call function
wasmz_val_t args[2] = { wasmz_val_i32(3), wasmz_val_i32(4) };
wasmz_val_t result;
err = wasmz_instance_call(instance, "add", args, 2, &result, 1);
if (err) {
printf("Call error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
return;
}
printf("Result: %d\n", result.of.i32);
Module Type Detection
if (wasmz_instance_is_command(instance)) {
// Has _start export
wasmz_instance_call_start(instance);
}
if (wasmz_instance_is_reactor(instance)) {
// No _start export - library mode
wasmz_instance_initialize(instance);
wasmz_instance_call(instance, "func", args, n, results, m);
}
Error Handling
// All functions return wasmz_error_t* on error
// NULL means success
wasmz_error_t *err = wasmz_module_new(engine, bytes, len, &module);
if (err != NULL) {
const char *msg = wasmz_error_message(err);
fprintf(stderr, "Error: %s\n", msg);
wasmz_error_delete(err);
return 1;
}
Complete Example
#include <stdio.h>
#include <stdlib.h>
#include "wasmz.h"
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <file.wasm>\n", argv[0]);
return 1;
}
// Load file
FILE *f = fopen(argv[1], "rb");
if (!f) {
perror("fopen");
return 1;
}
fseek(f, 0, SEEK_END);
size_t len = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *bytes = malloc(len);
fread(bytes, 1, len, f);
fclose(f);
// Create engine, store, module
wasmz_engine_t *engine = wasmz_engine_new();
wasmz_store_t *store = wasmz_store_new(engine);
wasmz_module_t *module = NULL;
wasmz_error_t *err = wasmz_module_new(engine, bytes, len, &module);
if (err) {
fprintf(stderr, "Compile error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
goto cleanup;
}
// Create instance
wasmz_instance_t *instance = NULL;
err = wasmz_instance_new(store, module, &instance);
if (err) {
fprintf(stderr, "Instantiate error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
goto cleanup;
}
// Run _start or call function
if (wasmz_instance_is_command(instance)) {
err = wasmz_instance_call_start(instance);
if (err) {
fprintf(stderr, "Runtime error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
}
} else {
err = wasmz_instance_initialize(instance);
if (err) {
fprintf(stderr, "Init error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
}
wasmz_val_t result;
err = wasmz_instance_call(instance, "add", NULL, 0, &result, 0);
if (err) {
fprintf(stderr, "Call error: %s\n", wasmz_error_message(err));
wasmz_error_delete(err);
}
}
wasmz_instance_delete(instance);
wasmz_module_delete(module);
cleanup:
wasmz_store_delete(store);
wasmz_engine_delete(engine);
free(bytes);
return 0;
}
Building
# Build library
zig build clib
# Link against it
gcc -o myapp myapp.c -Lzig-out/lib -lwasmz -Izig-out/include
Thread Safety
- Engine - Reference counting is thread-safe. Concurrent access to the same instance is not synchronized.
- Module - Reference counting is thread-safe. The underlying module data should not be accessed concurrently.
- Store - Not thread-safe. Contains GC heap and mutable state.
- Instance - Not thread-safe. Contains mutable execution state.
For multi-threaded applications, create separate Store and Instance per thread. Engine and Module handles can be shared safely (retain/release is atomic).
Limitations
The C API is intentionally minimal. For full functionality, use the Zig API.
| Feature | C API | Zig API | Reason |
|---|---|---|---|
| Host function registration | ❌ | ✅ | Requires function pointer callbacks and type registration |
| GC reference types | ❌ | ✅ | wasmz_val_t only supports i32/i64/f32/f64 |
| SIMD (V128) | ❌ | ✅ | V128 requires 16 bytes, exceeds wasmz_val_t union size |
Workarounds
- Host functions: Use Zig API with
Linker.define(), or pre-compile WASM modules that don’t require imports - GC types: Current limitation - use numeric types only
- SIMD: Functions using SIMD can still be called, but values cannot be passed/returned through C API
WASI Support
wasmz implements WASI Preview 1 (snapshot 1), enabling WebAssembly modules to interact with the host system.
Implementation Status
All WASI Preview 1 functions are implemented:
Environment
| Function | Status |
|---|---|
args_get | ✅ |
args_sizes_get | ✅ |
environ_get | ✅ |
environ_sizes_get | ✅ |
Clock
| Function | Status |
|---|---|
clock_res_get | ✅ |
clock_time_get | ✅ |
File Descriptors
| Function | Status |
|---|---|
fd_advise | ✅ |
fd_allocate | ✅ |
fd_close | ✅ |
fd_datasync | ✅ |
fd_fdstat_get | ✅ |
fd_fdstat_set_flags | ✅ |
fd_fdstat_set_rights | ✅ |
fd_filestat_get | ✅ |
fd_filestat_set_size | ✅ |
fd_filestat_set_times | ✅ |
fd_pread | ✅ |
fd_prestat_get | ✅ |
fd_prestat_dir_name | ✅ |
fd_pwrite | ✅ |
fd_read | ✅ |
fd_readdir | ✅ |
fd_renumber | ✅ |
fd_seek | ✅ |
fd_sync | ✅ |
fd_tell | ✅ |
fd_write | ✅ |
Path Operations
| Function | Status |
|---|---|
path_create_directory | ✅ |
path_filestat_get | ✅ |
path_filestat_set_times | ✅ |
path_link | ✅ |
path_open | ✅ |
path_readlink | ✅ |
path_remove_directory | ✅ |
path_rename | ✅ |
path_symlink | ✅ |
path_unlink_file | ✅ |
Polling
| Function | Status |
|---|---|
poll_oneoff | ✅ |
Process
| Function | Status |
|---|---|
proc_exit | ✅ |
proc_raise | ✅ |
sched_yield | ✅ |
Random
| Function | Status |
|---|---|
random_get | ✅ |
Sockets
| Function | Status |
|---|---|
sock_accept | ✅ |
sock_recv | ✅ |
sock_send | ✅ |
sock_shutdown | ✅ |
CLI Integration
Passing Arguments
# Pass arguments to WASM module
wasmz program.wasm --args "arg1 arg2 'arg with spaces'"
Environment Variables
Environment variables are automatically inherited from the host process.
Zig API Integration
const wasi = @import("wasi").preview1;
// Create WASI host
var wasi_host = wasi.Host.init(allocator);
defer wasi_host.deinit();
// Set arguments
try wasi_host.setArgs(&[_][]const u8{
"program.wasm",
"--verbose",
"input.txt",
});
// Set environment variables
try wasi_host.setEnv("MY_VAR", "value");
// Pre-open directory
try wasi_host.preopenDir("/data", "/data");
// Add to linker
var linker = wasmz.Linker.empty;
try wasi_host.addToLinker(&linker, allocator);
Pre-opened Directories
WASI uses pre-opened directories for filesystem access. By default:
/is pre-opened as the current working directory- Additional directories can be pre-opened via the API
// Pre-open specific directories
try wasi_host.preopenDir("/tmp", "/tmp");
try wasi_host.preopenDir("/home/user/data", "/data");
Exit Code
When a WASM module calls proc_exit, the exit code is returned:
const result = try instance.call("_start", &.{});
if (result.trap) |trap| {
if (trap.code == .Exit) {
const exit_code = trap.exit_code;
// Handle exit code
}
}
Exit Callback
Register a callback for proc_exit:
fn onExit(code: u32, ctx: ?*anyopaque) void {
std.debug.print("Module exited with code {d}\n", .{code});
}
wasi_host.setOnExit(onExit, &my_context);
Preview 2
WASI Preview 2 (Component Model) is not yet supported.
Currently, almost no languages are actually using the Preview2 proposal either, Planned for future releases.
Architecture
This section describes wasmz’s internal architecture for contributors.
Overview
┌─────────────────────────────────────────────────────────────┐
│ Public API │
│ (root.zig - Engine, Module, Store, Instance, Linker) │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ wasmz Module │
│ (High-level API implementation) │
└─────────────────────────────────────────────────────────────┘
│
┌──────────────┬──────────────┼──────────────┬───────────────┐
│ Parser │ Compiler │ VM │ WASI │
│ │ │ │ │
│ Binary │ Stack-to- │ Interpreter │ Preview 1 │
│ Parser │ Register │ Engine │ Host │
│ │ Compiler │ │ │
└──────────────┴──────────────┴───────────────┴───────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Core Types │
│ (Value types, ref types, heap types, trap, etc.) │
└─────────────────────────────────────────────────────────────┘
Pipeline
- Parse - Binary parser reads the WASM module
- Compile - Stack-to-register IR transformation
- Execute - VM interpreter runs compiled IR
Note: wasmz does not implement a validator. Use external tools like wasm-tools to validate WASM modules before execution.
Key Directories
| Directory | Purpose |
|---|---|
src/core/ | Core data types (types, values, traps) |
src/parser/ | WASM binary parser |
src/compiler/ | IR generation and optimization |
src/engine/ | Function type handling, config |
src/vm/ | Virtual machine, GC heap |
src/wasmz/ | High-level API implementation |
src/wasi/ | WASI system interface |
src/validator/ | Placeholder (not implemented) |
src/libs/ | Vendored dependencies |
Next Sections
- Parser - Binary parsing implementation
- Compiler - Stack-to-register compilation
- VM & Execution - Interpreter execution engine
- Garbage Collection - GC heap implementation
Parser
The parser reads WebAssembly binary format incrementally.
Design
The parser is designed for streaming - it can parse modules larger than available memory by processing sections incrementally.
Interface
const Parser = parser_mod.Parser.init();
while (true) {
const n = try reader.readSliceShort(pending_buf[pending_len..]);
const eof = n == 0;
var input = pending_buf[0 .. pending_len + n];
while (true) {
switch (parser.parse(input, eof)) {
.parsed => |result| {
// Handle payload
switch (result.payload) {
.module_header => |header| { /* ... */ },
.func => |func| { /* ... */ },
.code_section => |code| { /* ... */ },
else => {},
}
input = input[result.consumed..];
},
.need_more_data => {
// Buffer remaining data and read more
break;
},
.end => return,
.err => |e| return e,
}
}
}
Payloads
The parser emits payloads for each parsed element:
| Payload | Description |
|---|---|
module_header | Module magic and version |
type_section | Function types |
import_section | Imports |
func_section | Function declarations |
code_section | Function bodies |
data_section | Data segments |
elem_section | Element segments |
global_section | Globals |
memory_section | Memories |
table_section | Tables |
export_section | Exports |
start_section | Start function |
Validation
wasmz does not implement a full validator. Use external tools to validate WASM modules:
- wasm-tools - Recommended
- wabt - WebAssembly Binary Toolkit
Basic sanity checks are performed during parsing (e.g., section structure, index bounds).
Memory Model
The parser allocates:
- Payload data - Temporary, freed after handling
- Module metadata - Types, imports, exports (owned by Module)
Key Files
| File | Purpose |
|---|---|
src/parser/root.zig | Parser implementation |
src/parser/payload.zig | Payload types |
src/parser/range.zig | Source ranges for debugging |
src/parser/helper.zig | Parsing utilities |
Parallel Compilation
The parser can trigger parallel compilation:
// For each code section, after parsing body:
// 1. validate body
// 2. lower body (compile to register machine)
// These can run in separate threads
Extension Proposals
New proposals are handled by:
- Adding new payload types
- Adding new opcodes to the compiler
- Adding new validation rules
Compiler
The compiler transforms WebAssembly’s stack machine into a register-based IR for efficient interpretation.
Purpose
WebAssembly is a stack machine - values are pushed and popped from an operand stack. This is inefficient for interpretation. The compiler transforms this into a register-based IR where values are stored in named registers.
Pipeline
WASM Bytes → Parser → Stack Instructions → Validator → Compiler → Register IR
↓
Optimizer
↓
Code Gen (bytecode)
Lowering Process
Stack Machine
local.get 0
local.get 1
i32.add
local.set 2
Register IR
r0 = local[0]
r1 = local[1]
r2 = i32_add(r0, r1)
local[2] = r2
IR Structure
const IR = struct {
instructions: []Instruction,
registers: RegisterInfo,
locals: []LocalInfo,
blocks: []BlockInfo,
};
const Instruction = struct {
opcode: Opcode,
dst: ?Register,
src1: ?Register,
src2: ?Register,
// ...
};
Key Files
| File | Purpose |
|---|---|
src/compiler/root.zig | Compiler entry point |
src/compiler/ir.zig | IR data structures |
src/compiler/translate.zig | Stack-to-IR translation |
src/compiler/lower.zig | Modern lowering |
src/compiler/lower_legacy.zig | Legacy EH lowering |
src/compiler/value_stack.zig | Simulated operand stack |
Internal Checks
The compiler performs internal checks during lowering (not full WASM validation):
- Type checking - Operands match instruction requirements
- Reachability - Unreachable code is handled
- Block typing - Block inputs/outputs match
Note: These are runtime checks for compilation, not WASM specification validation. Use external tools for full validation.
Block Handling
Blocks (block, loop, if) are compiled with:
- Separate register scopes
- Branch targets
- Result values
// block $b (result i32)
// ... instructions ...
// br $b (value)
// end
Exception Handling
Two proposals are supported:
New Proposal
try $label
... instructions ...
catch $label
... exception handler ...
end
Legacy Proposal
try
... instructions ...
catch
... handler ...
rethrow
delegate $label
end
Controlled by Config.legacy_exceptions.
Thread Safety
Compilation of function bodies can be parallelized:
// Each code section can be compiled independently
for (module.functions) |func, i| {
// spawn thread for compile(func)
}
SIMD
SIMD instructions are handled specially:
- Vector operations execute directly
- SIMD-specific lowering rules
See src/core/simd/ for implementation.
Optimization
Current optimizations:
- Dead code elimination - Remove unreachable instructions
- Register coalescing - Reduce register count
- Constant folding - Evaluate constants at compile time
Future optimizations:
- Value numbering
- Common subexpression elimination
VM & Execution
The virtual machine executes compiled IR.
Execution Model
The VM is a register-based interpreter:
- Load compiled bytecode
- Execute instructions sequentially
- Handle branches and calls
- Return results or traps
Key Components
Function Execution
pub fn executeFunction(
store: *Store,
func: *const Func,
args: []const RawVal,
) ExecResult {
// Setup frame
// Execute instructions
// Return result
}
Instruction Dispatch
switch (opcode) {
.I32Add => {
const a = frame.getRegister(inst.src1).readAs(i32);
const b = frame.getRegister(inst.src2).readAs(i32);
frame.setRegister(inst.dst, RawVal.from(a + b));
},
.Call => {
// Handle function call
},
.Br => {
// Handle branch
},
// ...
}
Call Stack
Each call creates a frame:
const Frame = struct {
return_ip: usize,
return_frame: ?*Frame,
locals: []RawVal,
module: *const Module,
func_index: u32,
};
Memory
Linear memory is managed per-instance:
const Memory = struct {
data: []u8,
min: u32,
max: ?u32,
pub fn readByte(self: *Memory, offset: usize) u8;
pub fn writeByte(self: *Memory, offset: usize, value: u8);
pub fn grow(self: *Memory, pages: u32) !void;
};
Table
Tables store function references for indirect calls:
const Table = struct {
elements: []?FuncRef,
min: u32,
max: ?u32,
};
Global
Globals store mutable state:
const Global = struct {
value: RawVal,
mutable: bool,
};
Traps
Traps abort execution with an error code:
pub const TrapCode = enum {
Unreachable,
IntegerDivisionByZero,
IntegerOverflow,
// ...
};
Key Files
| File | Purpose |
|---|---|
src/vm/root.zig | VM entry point, ExecResult |
src/engine/root.zig | Engine implementation |
src/engine/func_ty.zig | Function type handling |
src/engine/code_map.zig | Compiled code storage |
Branch Handling
Branches use continuation-passing style:
// br $label
// Jump to block label, pass values
const block = frame.getBlock(inst.label);
frame.setValues(block.params);
frame.ip = block.start;
Function Calls
Direct Calls
// call $func
const callee = module.getFunc(inst.func_index);
try pushFrame(callee, args);
Indirect Calls
// call_indirect $type
const table_index = frame.getRegister(inst.src).readAs(u32);
const func_ref = table.elements[table_index];
const sig = module.types[inst.type_index];
if (!func_ref.signature.matches(sig)) {
return Trap{ .code = .IndirectCallTypeMismatch };
}
try pushFrame(func_ref, args);
Host Calls
Host functions are called through HostFunc:
pub const HostFunc = struct {
context: ?*anyopaque,
callback: *const fn (...) HostError!void,
param_types: []const ValType,
result_types: []const ValType,
};
Garbage Collection
wasmz implements the WebAssembly GC proposal with a managed heap.
GC Proposal Overview
The GC proposal adds:
- Structs - Fixed-size records with typed fields
- Arrays - Variable-size sequences of typed elements
- Reference Types - References to GC heap objects
GC Algorithm
wasmz uses a tri-color mark-and-sweep collector with an explicit worklist.
Allocation Strategy
The heap uses a free-list allocator with bump allocation fallback:
- Free-list search - Find a block >= requested size
- Block splitting - Split if remaining space can hold another FreeBlock
- Bump allocation - Fallback when no suitable free block exists
pub const GcHeap = struct {
bytes: []u8, // Contiguous byte buffer
free_list: FreeList, // Singly-linked list of free blocks
used: u32, // Bytes currently in use
live_objects: ArrayList(AllocationInfo), // Track all allocations
};
Collection Phases
Phase 1: Mark
- Seed worklist with root references (call frames, globals)
- Process worklist iteratively (BFS traversal):
- Pop object from worklist
- Mark object by setting mark bit in header
- Enqueue all child references
- Continue until worklist is empty
Phase 2: Sweep
- Iterate all live_objects in reverse
- If marked: clear mark bit (still live)
- If unmarked: free the block, remove from live list
pub fn collect(
self: *Self,
roots: []const GcRef,
composite_types: []const CompositeType,
struct_layouts: []const ?StructLayout,
array_layouts: []const ?ArrayLayout,
) void {
// Mark phase: iterative BFS with explicit worklist
var worklist = ArrayList(u32){};
for (roots) |ref| {
if (ref.isHeapRef()) {
const hdr = self.header(ref.asHeapIndex());
if (!hdr.isMarked()) {
hdr.setMark();
worklist.append(ref.asHeapIndex());
}
}
}
while (worklist.pop()) |idx| {
// Trace child references...
}
// Sweep phase
for (live_objects) |info| {
if (hdr.isMarked()) {
hdr.clearMark();
} else {
self.free(info.index, info.size);
}
}
}
Why Tri-Color Mark-and-Sweep?
- No stack overflow - Explicit worklist avoids deep recursion
- Simple implementation - Two-phase algorithm is easy to understand
- Incremental potential - Worklist design allows future incremental collection
Object Layout
Each GC object has:
- Header (8 bytes) - Metadata for GC and type information
- Payload - Field data for structs, length + elements for arrays
const GcHeader = struct {
kind_bits: u32, // High 6 bits = GcKind, bit 0 = mark bit
type_index: u32, // Type index for concrete types
};
GcKind
High 6 bits identify the object kind for subtype checking:
| Kind | Description |
|---|---|
Any | Top type for references |
Eq | Equality comparable types |
I31 | Unboxed 31-bit integer |
Struct | Struct object |
Array | Array object |
Func | Function reference |
Extern | External reference |
Exception | Exception object (internal) |
Mark Bit
Bit 0 of kind_bits is used for the GC mark phase:
fn setMark(self: *GcHeader) void {
self.kind_bits |= MARK_BIT;
}
fn isMarked(self: GcHeader) bool {
return (self.kind_bits & MARK_BIT) != 0;
}
GcRef
References are encoded as 32-bit indices:
const GcRef = struct {
// Index 0 = null
// High bits encode the kind (heap, i31, func, extern)
pub fn isHeapRef(self: GcRef) bool;
pub fn asHeapIndex(self: GcRef) ?u32;
pub fn encode(index: u32) GcRef;
};
Heap Types
const HeapType = union(enum) {
func: void,
extern: void,
any: void,
eq: void,
i31: void,
struct_type: *StructType,
array_type: *ArrayType,
// ...
};
Structs
const StructType = struct {
fields: []FieldType,
const FieldType = struct {
type: ValType,
mutable: bool,
};
};
WASM Example
(type $point (struct (field i32) (field i32)))
(func $create_point (param $x i32) (param $y i32) (result (ref $point))
struct.new $point
local.get $x
local.get $y
)
(func $get_x (param $p (ref $point)) (result i32)
local.get $p
struct.get $point 0
)
Arrays
const ArrayType = struct {
element_type: ValType,
mutable: bool,
};
WASM Example
(type $int_array (array (mut i32)))
(func $create_array (param $len i32) (result (ref $int_array))
local.get $len
i32.const 0
array.new $int_array
)
(func $get_element (param $arr (ref $int_array)) (param $idx i32) (result i32)
local.get $arr
local.get $idx
array.get $int_array
)
i31 References
Small integers stored unboxed:
const i31ref = struct {
value: i31, // 31-bit signed integer
};
WASM Example
(func $wrap_i31 (param $i i32) (result (ref i31))
ref.i31
local.get $i
)
(func $unwrap_i31 (param $r (ref i31)) (result i32)
local.get $r
i31.get_s
)
Key Files
| File | Purpose |
|---|---|
src/vm/gc/root.zig | GC entry point |
src/vm/gc/heap.zig | Heap management |
src/vm/gc/header.zig | Object header |
src/vm/gc/layout.zig | Object layout calculation |
src/core/gc_ref.zig | Reference type |
src/core/heap_type.zig | Heap type definitions |
src/core/ref_type.zig | Reference type definitions |
Memory Management
Allocation
// In struct.new or array.new
const total_size = HEADER_SIZE + payload_size;
const ref = gc_heap.alloc(total_size) orelse return error.OutOfMemory;
// Initialize header
const hdr = gc_heap.getHeader(ref);
hdr.kind_bits = GcKind.Struct;
hdr.type_index = type_index;
// Initialize fields...
Heap Growth
The heap grows exponentially (2x) when full:
const new_len = @max(min_needed, current_size * 2);
self.bytes = self.allocator.realloc(self.bytes, new_len);
Collection Trigger
Collection is triggered when memory limit is exceeded:
if (self.budget) |b| {
if (!b.canGrow(additional)) {
// Trigger GC and retry
gc_heap.collect(roots, composite_types, layouts);
}
}
No Write Barriers
Since wasmz uses mark-and-sweep, no write barriers are needed. References are traced during the mark phase by walking the object graph from roots.
Contributing
Thank you for your interest in contributing to wasmz!
Development Setup
Prerequisites
- Zig 0.15.2
- Git
Clone and Build
git clone https://github.com/anomalyco/wasmz.git
cd wasmz
make build
Running Tests
# Run all tests
make test
Project Structure
src/
├── root.zig # Public API exports
├── main.zig # CLI implementation
├── capi.zig # C API implementation
├── core/ # Core types (no dependencies)
│ ├── root.zig
│ ├── func_type.zig
│ ├── ref_type.zig
│ ├── heap_type.zig
│ ├── trap.zig
│ └── ...
├── parser/ # WASM binary parser
│ ├── root.zig
│ ├── payload.zig
│ └── tests/
├── compiler/ # Stack-to-register compiler
│ ├── root.zig
│ ├── ir.zig
│ ├── translate.zig
│ └── tests/
├── engine/ # Execution engine
│ ├── root.zig
│ ├── config.zig
│ └── func_ty.zig
├── vm/ # Virtual machine
│ ├── root.zig
│ └── gc/
├── wasmz/ # High-level API
│ ├── module.zig
│ ├── store.zig
│ ├── instance.zig
│ ├── host.zig
│ └── tests/
├── wasi/ # WASI implementation
│ ├── root.zig
│ └── preview1/
└── libs/ # Vendored dependencies
└── zigrc/
License
By contributing, you agree that your contributions will be licensed under the MIT License.