Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

wasmz logo

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

CommandDescription
make build-debugDebug build (unoptimized, fast compile)
make buildReleaseSafe build (optimized, safety checks)
make releaseReleaseFast build (maximum performance)
make testRun all unit tests
make clibBuild C shared library

Build Mode Differences

The build mode affects panic handling in the CLI binary:

ModePanic HandlerBinary SizeBacktrace
DebugFull panic handlerLarger✅ Readable stack trace
ReleaseSafeFull panic handlerLarger✅ Readable stack trace
ReleaseFastMinimal panic handler~127 KB smaller❌ No backtrace
ReleaseSmallMinimal 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

FlagDescription
--legacy-exceptionsUse legacy exception handling proposal
--args "<string>"Arguments to pass to WASM module (space-separated)
--func <name>Exported function to call
--reactorCall _initialize before the function
--mem-statsPrint 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

CodeMeaning
0Success
1Error (file not found, compilation error, etc.)
NWASI 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

TypeDescription
EngineRuntime engine with configuration
ConfigEngine configuration options
ModuleCompiled WebAssembly module
StoreRuntime context for instances
InstanceInstantiated module with memory/globals
LinkerHost function registry
HostFuncHost-provided callable
RawValGeneric value (i32/i64/f32/f64)
ExecResultExecution result (ok or trap)
TrapRuntime trap with code

Next Steps

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

MethodDescription
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

FieldTypeDefaultDescription
legacy_exceptionsboolfalseUse legacy EH proposal
mem_limit_bytes?u64nullMax 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

MethodDescription
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

TypeEntry PointDescription
Command_startRuns once, may call proc_exit
Reactor_initializeLibrary, 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 call retain()/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

PropertyTypeDescription
gc_heapGCHeapGarbage-collected heap
memory_budgetMemoryBudgetMemory 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

MethodDescription
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

MethodDescription
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

PropertyDescription
instanceAccess to the instance
storeAccess 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.

#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.

FeatureC APIZig APIReason
Host function registrationRequires function pointer callbacks and type registration
GC reference typeswasmz_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

FunctionStatus
args_get
args_sizes_get
environ_get
environ_sizes_get

Clock

FunctionStatus
clock_res_get
clock_time_get

File Descriptors

FunctionStatus
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

FunctionStatus
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

FunctionStatus
poll_oneoff

Process

FunctionStatus
proc_exit
proc_raise
sched_yield

Random

FunctionStatus
random_get

Sockets

FunctionStatus
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

  1. Parse - Binary parser reads the WASM module
  2. Compile - Stack-to-register IR transformation
  3. 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

DirectoryPurpose
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

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:

PayloadDescription
module_headerModule magic and version
type_sectionFunction types
import_sectionImports
func_sectionFunction declarations
code_sectionFunction bodies
data_sectionData segments
elem_sectionElement segments
global_sectionGlobals
memory_sectionMemories
table_sectionTables
export_sectionExports
start_sectionStart function

Validation

wasmz does not implement a full validator. Use external tools to validate WASM modules:

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

FilePurpose
src/parser/root.zigParser implementation
src/parser/payload.zigPayload types
src/parser/range.zigSource ranges for debugging
src/parser/helper.zigParsing 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:

  1. Adding new payload types
  2. Adding new opcodes to the compiler
  3. 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

FilePurpose
src/compiler/root.zigCompiler entry point
src/compiler/ir.zigIR data structures
src/compiler/translate.zigStack-to-IR translation
src/compiler/lower.zigModern lowering
src/compiler/lower_legacy.zigLegacy EH lowering
src/compiler/value_stack.zigSimulated operand stack

Internal Checks

The compiler performs internal checks during lowering (not full WASM validation):

  1. Type checking - Operands match instruction requirements
  2. Reachability - Unreachable code is handled
  3. 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:

  1. Dead code elimination - Remove unreachable instructions
  2. Register coalescing - Reduce register count
  3. 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:

  1. Load compiled bytecode
  2. Execute instructions sequentially
  3. Handle branches and calls
  4. 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

FilePurpose
src/vm/root.zigVM entry point, ExecResult
src/engine/root.zigEngine implementation
src/engine/func_ty.zigFunction type handling
src/engine/code_map.zigCompiled 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:

  1. Free-list search - Find a block >= requested size
  2. Block splitting - Split if remaining space can hold another FreeBlock
  3. 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

  1. Seed worklist with root references (call frames, globals)
  2. Process worklist iteratively (BFS traversal):
    • Pop object from worklist
    • Mark object by setting mark bit in header
    • Enqueue all child references
  3. Continue until worklist is empty

Phase 2: Sweep

  1. Iterate all live_objects in reverse
  2. If marked: clear mark bit (still live)
  3. 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:

  1. Header (8 bytes) - Metadata for GC and type information
  2. 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:

KindDescription
AnyTop type for references
EqEquality comparable types
I31Unboxed 31-bit integer
StructStruct object
ArrayArray object
FuncFunction reference
ExternExternal reference
ExceptionException 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

FilePurpose
src/vm/gc/root.zigGC entry point
src/vm/gc/heap.zigHeap management
src/vm/gc/header.zigObject header
src/vm/gc/layout.zigObject layout calculation
src/core/gc_ref.zigReference type
src/core/heap_type.zigHeap type definitions
src/core/ref_type.zigReference 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.