Zig WASM Nodes
Zig produces compact, zero-overhead WASM binaries and gives you direct control over memory. The template uses wit-bindgen C bindings imported via @cImport, so you get type-safe access to the Flow-Like host without any runtime overhead.
Prerequisites
Section titled “Prerequisites”- Zig 0.14+
- wasm-tools — component model tooling
- wit-bindgen — WIT binding generator
- Cargo — needed to install wasm-tools and wit-bindgen
# Install tooling (or use `mise run setup` from the template)cargo install wasm-toolsThe build also needs a WASI preview1 reactor adapter. The setup task downloads it automatically:
curl -fsSL -o wasi_snapshot_preview1.reactor.wasm \ "https://github.com/bytecodealliance/wasmtime/releases/download/v29.0.1/wasi_snapshot_preview1.reactor.wasm"Project Structure
Section titled “Project Structure”wasm-node-zig/├── build.zig # Zig build config (wasm32-wasi target)├── build.zig.zon # Package manifest├── flow-like.toml # Flow-Like package manifest├── mise.toml # Build tasks (setup, generate, build)├── src/│ └── main.zig # Node implementation└── wit/ └── flow-like-node.wit # WIT interface definitionAfter running mise run generate, a gen/ directory appears with the C bindings:
gen/├── flow_like_node.c├── flow_like_node.h└── flow_like_node_component_type.oQuick Start
Section titled “Quick Start”Build Configuration
Section titled “Build Configuration”The build script targets wasm32-wasi, links the generated C bindings, and exports functions dynamically:
const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .wasi, }); const optimize = b.standardOptimizeOption(.{});
const lib = b.addExecutable(.{ .name = "node", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, });
// wit-bindgen-c generated sources (run `mise run generate` first) lib.addIncludePath(b.path("gen")); lib.addCSourceFiles(.{ .files = &.{"gen/flow_like_node.c"}, .flags = &.{"-std=c11"}, }); lib.addObjectFile(b.path("gen/flow_like_node_component_type.o"));
lib.linkLibC(); lib.entry = .disabled; lib.rdynamic = true;
b.installArtifact(lib);}Node Implementation
Section titled “Node Implementation”The WIT bindings are imported as C headers via @cImport. Here’s the core pattern:
const std = @import("std");
// Import the wit-bindgen-c generated headerconst wit = @cImport({ @cInclude("flow_like_node.h");});
const ABI_VERSION: u32 = 1;const allocator = std.heap.page_allocator;Reading Inputs & Writing Outputs
Section titled “Reading Inputs & Writing Outputs”The Context struct wraps the WIT import functions for ergonomic use:
const Context = struct { result: ExecutionResult,
fn getString(self: *const Context, name: []const u8, default: []const u8) []const u8 { const raw = self.getInputRaw(name) orelse return default; if (raw.len >= 2 and raw[0] == '"' and raw[raw.len - 1] == '"') { return raw[1 .. raw.len - 1]; // strip JSON quotes } return raw; }
fn getI64(self: *const Context, name: []const u8, default: i64) i64 { const raw = self.getInputRaw(name) orelse return default; return std.fmt.parseInt(i64, raw, 10) catch default; }
fn setOutput(self: *Context, name: []const u8, json_value: []const u8) void { var wname = toWitString(name); var wval = toWitString(json_value); wit.flow_like_node_pins_set_output(&wname, &wval); // ... track in result }
fn success(self: *Context) []const u8 { self.activateExec("exec_out"); return self.result.toJson(); }};Run Handler
Section titled “Run Handler”fn handleRun(ctx: *Context) []const u8 { const input_text = ctx.getString("input_text", ""); const multiplier = ctx.getI64("multiplier", 1);
ctx.log(0, "Processing input text");
// Build output var buf = std.ArrayList(u8).init(allocator); var i: i64 = 0; while (i < multiplier) : (i += 1) { buf.appendSlice(input_text) catch {}; }
ctx.setOutput("output_text", jsonQuote(8192, buf.items)); ctx.setOutput("char_count", std.fmt.allocPrint(allocator, "{d}", .{buf.items.len}) catch "0");
return ctx.success();}WIT Exports
Section titled “WIT Exports”Every node must export these four functions:
export fn exports_flow_like_node_get_node(ret: *wit.flow_like_node_string_t) void { setWitResult(buildNodeJson(), ret);}
export fn exports_flow_like_node_get_nodes(ret: *wit.flow_like_node_string_t) void { const json = std.fmt.allocPrint(allocator, "[{s}]", .{buildNodeJson()}) catch "[]"; setWitResult(json, ret);}
export fn exports_flow_like_node_run(input: *wit.flow_like_node_string_t, ret: *wit.flow_like_node_string_t) void { _ = input; var ctx = Context.init(); setWitResult(handleRun(&ctx), ret);}
export fn exports_flow_like_node_get_abi_version() u32 { return ABI_VERSION;}The template uses mise for task orchestration:
# One-time setup: install wasm-tools, wit-bindgen, download WASI adaptermise run setup
# Generate C bindings from WITmise run generate
# Build the WASM component (generates node.wasm)mise run buildThe build pipeline:
wit-bindgen cgenerates C bindings from the WIT filezig build -Doptimize=ReleaseSmallcompiles to a core WASM modulewasm-tools component newwraps the core module into a WASM Component with the WASI adapter
Output: node.wasm
Manual Build (without mise)
Section titled “Manual Build (without mise)”# Step 1: Generate bindingswit-bindgen c --world flow-like-node --out-dir gen wit/flow-like-node.wit
# Step 2: Compilezig build -Doptimize=ReleaseSmall
# Step 3: Create componentwasm-tools component new zig-out/bin/node.wasm \ --adapt wasi_snapshot_preview1=wasi_snapshot_preview1.reactor.wasm \ -o node.wasmTesting
Section titled “Testing”mise run test # runs zig build testmise run clean # removes zig-out/, .zig-cache/, gen/, node.wasmRelated
Section titled “Related”- Overview — How WASM nodes work
- Package Manifest — Full manifest reference
- Rust WASM Nodes — Recommended language with SDK macros