zig
npx skills add https://github.com/tkersey/dotfiles --skill zig
Agent 安装分布
Skill 文档
Zig
When to use
- Editing
.zigfiles. - Modifying
build.zigorbuild.zig.zon. - Defining custom
zig buildsteps (for examplezig build ci) and CI/build-cache wiring. - Zig builds/tests, dependencies, cross-compilation.
- Any Zig work requires fuzz testing (coverage-guided or fuzz-style).
- Performance tuning: SIMD (
std.simd/@Vector) and threading (std.Thread.Pool). - Comptime, reflection, codegen.
- Allocators, ownership, zero-copy parsing.
- C interop.
Baseline (required)
- Zig 0.15.2.
- Integrated fuzzer is the default:
std.testing.fuzz+zig build test --fuzz. - No compatibility work for older Zig unless explicitly requested.
Quick start
# Toolchain (required)
zig version # must be 0.15.2
# Initialize (creates build.zig + src/main.zig)
zig init
# or (smaller template)
zig init --minimal
# NOTE: --minimal does NOT add a `test` build step; `zig build test` / `--fuzz`
# will fail unless you add a test step to build.zig.
# Format
zig fmt src/main.zig
# Build/run/test (build.zig present)
zig build
zig build run
zig build test
# Fuzz (integrated fuzzer)
# Requires a `test` step in build.zig (not present in --minimal template).
zig build test --fuzz
# Single-file test/run
zig test src/main.zig
zig run src/main.zig
# Trigger audit (session-level proxy via seq)
uv run python codex/skills/zig/scripts/zig_trigger_audit.py --root ~/.codex/sessions
uv run python codex/skills/zig/scripts/zig_trigger_audit.py --root ~/.codex/sessions --since 2026-02-06T00:00:00Z
uv run python codex/skills/zig/scripts/zig_trigger_audit.py --root ~/.codex/sessions --since 2026-02-06T00:00:00Z --strict-implicit --format json --output /tmp/zig-audit.json
# Output includes split explicit-vs-implicit recall/precision plus filtered implicit-denominator counts.
uv run python codex/skills/zig/scripts/test_zig_trigger_audit.py
Workflow (correctness -> speed)
- State the contract: input domain, outputs, invariants, error model, complexity target.
- Build a reference implementation (simple > fast) and keep it in-tree for diffing.
- Unit tests: edge cases + regressions.
- Differential fuzz: compare optimized vs reference in Debug/ReleaseSafe.
- Optimize in order: algorithm -> data layout -> SIMD -> threads -> micro.
- Re-run fuzz/tests after every optimization; benchmark separately in ReleaseFast.
Correctness mandate (non-negotiable)
- Every Zig change earns at least one correctness signal.
- For parsing/arith/memory/safety-sensitive code, that signal is fuzzing.
- Prefer differential fuzzing (optimized vs reference) so behavior is proven, not inferred.
- Default harness:
std.testing.fuzz+zig build test --fuzz(Zig 0.15.2 baseline). - Time-agnostic: no prescribed fuzz duration; run it as long as practical and always persist findings.
- Run fuzz in
Debug/ReleaseSafeso safety checks stay on; benchmark separately inReleaseFast. - Allocator-using code also runs
std.testing.checkAllAllocationFailures. - If fuzzing cannot run locally (e.g., macOS
InvalidElfMagiccrash), state why and add a follow-up (seed corpus + repro test); run fuzz in Linux/CI or external harness.
Performance quick start (host CPU)
# High-performance build for local benchmarking
zig build-exe -O ReleaseFast -mcpu=native -fstrip src/main.zig
# Emit assembly / optimized IR for inspection
zig build-exe -O ReleaseFast -mcpu=native -femit-asm src/main.zig
zig build-exe -O ReleaseFast -mcpu=native -femit-llvm-ir src/main.zig # requires LLVM extensions
# Build.zig projects (when using b.standardTargetOptions / standardOptimizeOption)
zig build -Doptimize=ReleaseFast -Dtarget=native -Dcpu=native
Common commands
# Release
zig build -Doptimize=ReleaseFast
# Release + LTO (requires LLVM extensions)
zig build-exe -O ReleaseFast -mcpu=native -flto -fstrip src/main.zig
# Cross-compile
zig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-macos
# Clean artifacts
rm -rf zig-out zig-cache
Optimization stance (for generated code)
- Prefer algorithmic wins first; then data layout; then SIMD; then threads; then micro-tuning.
- Keep hot loops allocation-free; treat allocations as a correctness smell in kernels.
- Prefer contiguous slices and SoA layouts; avoid pointer chasing in the hot path.
- Avoid false sharing: make per-thread outputs cache-line separated (e.g.
align(std.atomic.cache_line)). - Help the optimizer: branchless vector loops,
@branchHint(.likely/.unlikely), and simple control flow. - Keep fast paths portable:
std.simd.suggestVectorLength(T)+ scalar fallback; thread-pool usage already degrades onbuiltin.single_threaded.
SIMD / vectorization playbook
Principles:
- Use explicit vectors when you need guaranteed SIMD (
@Vector); rely on auto-vectorization only as a bonus. - Derive lane count from
std.simd.suggestVectorLength(T)so the same code scales across targets. - Keep vector loops straight-line: no function pointers, no complex branching, no hidden allocations.
- Handle tails (remainder elements) with a scalar loop.
- Alignment matters on some targets (notably ARM); when tuning, consider a scalar prologue until aligned to the block size.
SIMD template: reduce a slice
const std = @import("std");
pub fn sumF32(xs: []const f32) f32 {
if (xs.len == 0) return 0;
if (!@inComptime()) if (std.simd.suggestVectorLength(f32)) |lanes| {
const V = @Vector(lanes, f32);
var i: usize = 0;
var acc: V = @splat(0);
while (i + lanes <= xs.len) : (i += lanes) {
const v: V = xs[i..][0..lanes].*;
acc += v;
}
var total: f32 = @reduce(.Add, acc);
while (i < xs.len) : (i += 1) total += xs[i];
return total;
}
var total: f32 = 0;
for (xs) |x| total += x;
return total;
}
SIMD scanning pattern (mask + reduce)
- Compare a vector against a scalar mask:
matches = block == @as(Block, @splat(value)). - Detect any matches:
if (@reduce(.Or, matches)) { ... }. - Find the first match index:
std.simd.firstTrue(matches).?.
SIMD delimiter bitmask pattern (CSV-class scanners)
Use a bitmask when you need all match positions in a block (delimiter, quote, CR, LF), not just the first hit.
const std = @import("std");
fn delimMask(block: @Vector(16, u8), delim: u8, quote: u8, cr: u8, lf: u8) u16 {
const matches: @Vector(16, bool) =
(block == @as(@Vector(16, u8), @splat(delim))) or
(block == @as(@Vector(16, u8), @splat(quote))) or
(block == @as(@Vector(16, u8), @splat(cr))) or
(block == @as(@Vector(16, u8), @splat(lf)));
return @bitCast(matches);
}
fn walkMatches(mask0: u16) void {
var mask = mask0;
while (mask != 0) {
const idx = @ctz(mask); // 0-based lane index
_ = idx;
mask &= mask - 1; // clear lowest set bit
}
}
- Validate lane mapping with a unit test: after
@bitCast, LSB corresponds to lane/index0.
Loop shaping tips (stdlib-proven)
- Unroll short inner loops with
inline forto cut bounds checks (seestd.mem.indexOfScalarPos). - Use
std.simd.suggestVectorLength(T)to match stdlibâs preferred alignment and vector width. - Guard vector paths with
!@inComptime()and!std.debug.inValgrind()when doing anything tricky.
Threads / parallelism playbook
Principles:
- Only thread if you can amortize scheduling + cache effects (tiny slices usually lose).
- Partition work by contiguous ranges; avoid shared writes and shared locks in the hot path.
- Use a thread pool (
std.Thread.Pool) + wait group (std.Thread.WaitGroup), not “spawn a thread per task”. - Make tasks coarse: ~
cpu_countto ~8*cpu_counttasks, each doing a SIMD inner loop. - Reduce results at the end; avoid atomics unless you truly need streaming aggregation.
Thread pool template (data-parallel)
const std = @import("std");
const builtin = @import("builtin");
fn sumChunk(xs: []const f32, out: *f32) void {
// Each task uses the SIMD kernel.
out.* = sumF32(xs);
}
pub fn sumParallel(xs: []const f32) !f32 {
if (xs.len == 0) return 0;
// For throughput-oriented programs, prefer std.heap.smp_allocator in ReleaseFast.
// smp_allocator is unavailable when compiled with -fsingle-threaded.
const alloc = if (builtin.single_threaded) std.heap.page_allocator else std.heap.smp_allocator;
var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = alloc });
defer pool.deinit();
const cpu_count = @max(1, std.Thread.getCpuCount() catch 1);
const task_count = @min(cpu_count * 4, xs.len);
const chunk_len = (xs.len + task_count - 1) / task_count;
var partials = try alloc.alloc(f32, task_count);
defer alloc.free(partials);
var wg: std.Thread.WaitGroup = .{};
for (0..task_count) |t| {
const start = t * chunk_len;
const end = @min(xs.len, start + chunk_len);
pool.spawnWg(&wg, sumChunk, .{ xs[start..end], &partials[t] });
}
// Let the calling thread help execute queued work.
pool.waitAndWork(&wg);
var total: f32 = 0;
for (partials) |p| total += p;
return total;
}
Per-thread scratch (no allocator contention)
- Initialize the pool with
.track_ids = true. - Use
pool.spawnWgId(&wg, func, args);funcreceivesid: usizefirst. - Keep
scratch[id]aligned tostd.atomic.cache_lineto prevent false sharing.
const std = @import("std");
const Scratch = struct {
_: void align(std.atomic.cache_line) = {},
tmp: [4096]u8 = undefined,
};
fn work(id: usize, input: []const u8, scratch: []Scratch) void {
// Stable per-thread slot; no locks, no false sharing.
const buf = scratch[id].tmp[0..];
_ = buf;
_ = input;
}
pub fn runParallel(input: []const u8, allocator: std.mem.Allocator) !void {
var pool: std.Thread.Pool = undefined;
try pool.init(.{ .allocator = allocator, .track_ids = true });
defer pool.deinit();
const scratch = try allocator.alloc(Scratch, pool.getIdCount());
defer allocator.free(scratch);
var wg: std.Thread.WaitGroup = .{};
pool.spawnWgId(&wg, work, .{ input, scratch });
pool.waitAndWork(&wg);
}
Comptime meta-programming (Zig 0.15.2)
Principles:
- Use comptime for specialization and validation; measure compile time like runtime.
- Prefer data over codegen; generate code only when it unlocks optimization.
- Make illegal states unrepresentable with
@compileErrorat the boundary.
Core tools:
- Type reflection:
@typeInfo,@Type,@TypeOf,@typeName. - Namespaces/fields:
@hasDecl,@field,@FieldType,std.meta.fields,std.meta.declarations. - Layout + ABI:
@sizeOf,@alignOf,@bitSizeOf,@offsetOf,@fieldParentPtr. - Controlled unrolling:
inline for,inline while,comptime if. - Diagnostics:
@compileError,@compileLog. - Cost control:
@setEvalBranchQuota(local, justified),--time-report.
Common patterns:
- Traits: assert required decls/methods at compile time.
- Field-wise derivations: generate
eql/hash/format/serializeby iterating fields. - Static tables: small
std.StaticStringMap.initComptime; large enums preferinline forscans (seestd.meta.stringToEnum). - Kernel factories:
fn Kernel(comptime lanes: usize, comptime unroll: usize) type { ... }+std.simd.suggestVectorLength.
Unfair toolbox (stdlib-proven):
std.meta.eql: deep-ish equality for containers (pointers are not followed).std.meta.hasUniqueRepresentation: gate “memcmp-style” fast paths.std.meta.FieldEnum/std.meta.DeclEnum: turn fields/decls into enums for ergonomic switches.std.meta.Tag/std.meta.activeTag: read tags of tagged unions.std.meta.fields/std.meta.declarations: one-liners for reflection without raw@typeInfoplumbing.
Trait check template
const std = @import("std");
fn assertHasRead(comptime T: type) void {
if (!std.meta.hasMethod(T, "read")) {
@compileError(@typeName(T) ++ " must implement read()");
}
}
Field-wise derivation template (struct)
const std = @import("std");
fn eqlStruct(a: anytype, b: @TypeOf(a)) bool {
const T = @TypeOf(a);
if (@typeInfo(T) != .@"struct") @compileError("eqlStruct expects a struct");
inline for (std.meta.fields(T)) |f| {
if (!std.meta.eql(@field(a, f.name), @field(b, f.name))) return false;
}
return true;
}
Layout assertions (ABI lock)
comptime {
const Header = extern struct {
magic: u32,
version: u16,
flags: u16,
len: u32,
_pad: u32,
};
if (@sizeOf(Header) != 16) @compileError("Header ABI: size");
if (@alignOf(Header) != 4) @compileError("Header ABI: align");
if (@offsetOf(Header, "magic") != 0) @compileError("Header ABI: magic offset");
if (@offsetOf(Header, "len") != 8) @compileError("Header ABI: len offset");
}
Union visitor (inline switch)
fn visit(u: anytype) void {
switch (u) {
inline else => |payload, tag| {
_ = payload;
_ = tag; // comptime-known tag
},
}
}
Type-shape dispatcher (derive-anything)
Use this to write one derivation pipeline that supports structs/unions/enums/pointers/arrays/etc.
Keep the return type uniform (R) so call sites stay simple.
Implementation + tests: codex/skills/zig/references/type_switch.zig.
Validate: zig test codex/skills/zig/references/type_switch.zig
@Type builder (surgical)
Reach for @Type when you truly need to manufacture a new type from an input type.
This is sharp: prefer std.meta.* when it can express the same intent.
Example: build a “patch” type where every runtime field is ?T defaulting to null.
Implementation + tests: codex/skills/zig/references/partial_type.zig.
Validate: zig test codex/skills/zig/references/partial_type.zig
Derive pipeline (walk + policies; truly unfair)
One traversal emits semantic events; policies decide what to do (hash, format, serialize, stats). Traversal owns ordering + budgets; policies own semantics.
Implementation + tests: codex/skills/zig/references/derive_walk_policy.zig.
Validate: zig test codex/skills/zig/references/derive_walk_policy.zig
Note: formatting helpers take a writer pointer (e.g. &w) so state is preserved.
Fast path when representation is unique
const std = @import("std");
fn eqlFast(a: anytype, b: @TypeOf(a)) bool {
const T = @TypeOf(a);
if (comptime std.meta.hasUniqueRepresentation(T)) {
return std.mem.eql(u8, std.mem.asBytes(&a), std.mem.asBytes(&b));
}
return std.meta.eql(a, b);
}
Compile-time cost guardrails
- Avoid combinatorial specialization: keep the knob surface small and explicit.
- Avoid huge comptime maps for large domains; prefer
inline forscans. - If you must raise the branch quota, do it in the smallest loop that needs it.
- Prefer
std.meta.*helpers over handwritten@typeInfoplumbing (less code, fewer bugs).
Comptime example
const std = @import("std");
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
test "comptime parameter" {
const x = max(u32, 3, 5);
try std.testing.expect(x == 5);
}
Comptime for performance
Patterns:
- Specialize on lane count and unroll factors (
comptime lanes,comptime unroll). - Generate lookup tables at comptime (e.g., classification maps, shuffle indices).
- Prefer
comptime iffor CPU/arch dispatch (builtin.cpu.arch) when you need different kernels.
Comptime specialization example (SIMD dot product)
const std = @import("std");
fn Dot(comptime lanes: usize) type {
return struct {
pub fn dot(a: []const f32, b: []const f32) f32 {
const V = @Vector(lanes, f32);
var i: usize = 0;
var acc: V = @splat(0);
while (i + lanes <= a.len) : (i += lanes) {
const av: V = a[i..][0..lanes].*;
const bv: V = b[i..][0..lanes].*;
acc += av * bv;
}
var total: f32 = @reduce(.Add, acc);
while (i < a.len) : (i += 1) total += a[i] * b[i];
return total;
}
};
}
pub fn dotAuto(a: []const f32, b: []const f32) f32 {
const lanes = std.simd.suggestVectorLength(f32) orelse 1;
return Dot(lanes).dot(a, b);
}
Build essentials (build.zig)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
if (b.args) |args| run_cmd.addArgs(args);
}
Package management (build.zig.zon)
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.@"some-package" = .{
.url = "https://github.com/user/package/archive/main.tar.gz",
.hash = "1220abcdef...",
},
},
.paths = .{ "build.zig", "build.zig.zon", "src" },
}
Memory / allocators (performance-first)
Rules of thumb:
- Debugging correctness/leaks:
std.testing.allocatorin tests, orstd.heap.DebugAllocatorin apps. - Throughput + multithreading (ReleaseFast):
std.heap.smp_allocator(singleton, designed for MT + ReleaseFast). - Short-lived “build a result then throw away”:
std.heap.ArenaAllocatoron top of a fast backing allocator. - Scratch buffers:
std.heap.FixedBufferAllocatororstd.heap.stackFallback(N, fallback). - Fixed-size objects:
std.heap.MemoryPool/std.heap.MemoryPoolAligned.
Debug allocator (leak checking)
const std = @import("std");
pub fn main() !void {
var dbg = std.heap.DebugAllocator(.{}){};
defer _ = dbg.deinit();
const allocator = dbg.allocator();
const bytes = try allocator.alloc(u8, 100);
defer allocator.free(bytes);
}
Smp allocator + arena reset (hot loop friendly)
const std = @import("std");
const builtin = @import("builtin");
pub fn buildManyThings() !void {
const backing = if (builtin.single_threaded) std.heap.page_allocator else std.heap.smp_allocator;
var arena = std.heap.ArenaAllocator.init(backing);
defer arena.deinit();
const a = arena.allocator();
var i: usize = 0;
while (i < 1000) : (i += 1) {
_ = arena.reset(.retain_capacity);
_ = try a.alloc(u8, 4096);
}
}
Inspecting codegen / benchmarking
- Emit assembly:
zig build-exe -O ReleaseFast -mcpu=native -femit-asm src/main.zig - Emit optimized LLVM IR:
zig build-exe -O ReleaseFast -mcpu=native -femit-llvm-ir src/main.zig(LLVM extensions) - Track compile time:
--time-report - Prevent DCE in benches:
std.mem.doNotOptimizeAway(x) - Time loops:
std.time.Timer,std.time.nanoTimestamp
Benchmark contract for parser work
- Benchmark only the parse/iterate loop; exclude file download/decompression and setup noise.
- Keep buffer size fixed in reports (for example 64 KiB) so runs are comparable.
- Report wall time and at least one hardware counter set (branch misses, cache misses) plus RSS.
- Capture machine + OS + compiler version + dataset mix in the benchmark notes.
Zero-copy parsing playbook
Principles:
- Treat input as immutable bytes; parse into views, not copies.
- Make ownership explicit (borrowed vs owned).
- Store spans/offsets into a stable base buffer.
- Never return slices into temporary buffers.
Iterator lifetime + streaming buffer contract (CSV-class)
- Prefer field iterators for hot paths (one field per
next()), then build record views on top. - Document lifetime explicitly: returned field slices are valid until the next
next()call unless they reference caller-owned stable backing storage. - For streaming input, slide partial tokens to the front before refill (
@memmove), then continue. - If a token exceeds buffer capacity, fail fast with an explicit error (
error.FieldTooLong) instead of truncating or reallocating in the hot loop.
fn refillKeepingTail(buf: []u8, head: *usize, tail: *usize, reader: anytype) !usize {
if (head.* > 0 and head.* < tail.*) {
const rem = tail.* - head.*;
@memmove(buf[0..rem], buf[head.*..tail.*]);
head.* = 0;
tail.* = rem;
} else if (head.* == tail.*) {
head.* = 0;
tail.* = 0;
}
if (tail.* == buf.len) return error.FieldTooLong;
const n = try reader.read(buf[tail.*..]);
tail.* += n;
return n;
}
Quoted-field finite-state path (robust CSV)
- Split early into unquoted and quoted paths; keep unquoted path branch-light.
- In quoted mode, treat
""as escaped quote, then require delimiter/newline/end-of-input after the closing quote. - Track
needs_unescapeduring scan and only unescape when necessary. - Normalize CRLF with integer booleans in hot loops to avoid extra branches.
const has_lf: usize = @intFromBool(end > start and buf[end - 1] == '\n');
const has_cr: usize = @intFromBool(end > start + has_lf and buf[end - 1 - has_lf] == '\r');
const trimmed_end = end - has_lf - has_cr;
Borrowed/owned token (copy-on-write escape hatch)
const std = @import("std");
pub const ByteView = union(enum) {
borrowed: []const u8,
owned: []u8,
pub fn slice(self: ByteView) []const u8 {
return switch (self) {
.borrowed => |s| s,
.owned => |s| s,
};
}
pub fn toOwned(self: ByteView, allocator: std.mem.Allocator) ![]u8 {
return switch (self) {
.owned => |s| s,
.borrowed => |s| try allocator.dupe(u8, s),
};
}
pub fn deinit(self: *ByteView, allocator: std.mem.Allocator) void {
if (self.* == .owned) allocator.free(self.owned);
self.* = .{ .borrowed = &.{} };
}
};
POSIX mmap (stable base buffer)
const std = @import("std");
pub const MappedFile = struct {
data: []const u8,
owns: bool,
pub fn open(path: []const u8) !MappedFile {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const size = (try file.stat()).size;
const map = try std.posix.mmap(
null,
size,
std.posix.PROT.READ,
.{ .TYPE = .PRIVATE },
file.handle,
0,
);
return .{ .data = map, .owns = true };
}
pub fn close(self: *MappedFile) void {
if (self.owns) std.posix.munmap(self.data);
self.* = .{ .data = &.{}, .owns = false };
}
};
Span-based parsing (offsets, not copies)
const Span = struct {
base: []const u8,
start: usize,
len: usize,
pub fn slice(self: Span) []const u8 {
return self.base[self.start..][0..self.len];
}
};
Testing
- Run correctness tests in Debug or ReleaseSafe; run perf checks in ReleaseFast.
- Leak detection: use
std.testing.allocatoranddeferfrees. - Prefer differential tests (reference vs optimized) and metamorphic invariants (roundtrip, monotonicity).
- Allocation counting: wrap an allocator and assert zero allocations for a “zero-copy” path.
- OOM injection: run under
std.testing.FailingAllocator. - Exhaustive OOM:
std.testing.checkAllAllocationFailures.
Fuzz testing (required)
Built-in fuzzer (default, Zig 0.15.2)
Use std.testing.fuzz in a test block and run:
zig build test --fuzz (optionally -Doptimize=ReleaseSafe).
Consult zig build test --help for version-specific --fuzz flags.
macOS caveat (Zig 0.15.2)
Try zig build test --fuzz on macOS first. If it crashes during startup
(InvalidElfMagic observed), skip local fuzzing for that run, keep
std.testing.fuzz tests in-tree, and run the fuzz step on Linux/CI or via an
external harness; use zig test locally for smoke coverage.
Fuzz target rules (make it fuzzer-friendly)
- Deterministic: no timers, threads, or internal RNG (the fuzzer is the RNG).
- Total: accept any input bytes; never read out of bounds; no UB-by-assumption.
- Bounded: cap pathological work (length limits, recursion depth, max allocations).
- Isolated: no global mutable state (or fully reset per call).
- Assert properties, not vibes: reference equivalence, roundtrips, monotonicity, invariants.
Differential fuzzing (recommended)
Make your fuzz target assert equivalence between a small reference implementation and the optimized kernel. This is the fastest route to algorithmic correctness.
Compile-checked template: codex/skills/zig/references/fuzz_differential.zig.
Template:
const std = @import("std");
fn refCountOnes(bytes: []const u8) u64 {
var n: u64 = 0;
for (bytes) |b| n += @popCount(b);
return n;
}
fn fastCountOnes(bytes: []const u8) u64 {
// Replace with the optimized version (SIMD/threads/etc).
return refCountOnes(bytes);
}
fn fuzzTarget(_: void, input: []const u8) !void {
const ref = refCountOnes(input);
const got = fastCountOnes(input);
try std.testing.expectEqual(ref, got);
}
test "fuzz target" {
try std.testing.fuzz({}, fuzzTarget, .{});
}
Allocation-failure fuzzing (mandatory for allocators)
std.testing.checkAllAllocationFailures exhaustively injects error.OutOfMemory across
all allocations in a test function. The test function must take an allocator as its first
argument, return !void, and reset shared state each run.
const std = @import("std");
fn parseWithAlloc(alloc: std.mem.Allocator, bytes: []const u8) !void {
_ = alloc;
_ = bytes;
}
test "allocation failure fuzz" {
const input = "seed";
try std.testing.checkAllAllocationFailures(
std.testing.allocator,
parseWithAlloc,
.{input},
);
}
Allocator pressure tricks (recommended)
- Cap per-allocation size relative to input length to surface pathological allocations.
- Wrap with
std.testing.FailingAllocatorto validateerrdeferand cleanup paths. - Persist interesting inputs under
testdata/fuzz/and promote crashes to regression tests.
Corpus + regression workflow (required)
- When fuzz finds a crash/mismatch, save the input under
testdata/fuzz/<target>/. - Add a deterministic regression test using
@embedFile.
test "regression: fuzz crash" {
const input = @embedFile("testdata/fuzz/parser/crash-<id>.bin");
try std.testing.expectEqual(refCountOnes(input), fastCountOnes(input));
}
Fast in-tree randomized fuzz (smoke; complements --fuzz)
Use randomized inputs inside test blocks for cheap, always-on coverage.
const std = @import("std");
fn parse(bytes: []const u8) !void {
_ = bytes;
}
test "fuzz parse" {
var prng = std.rand.DefaultPrng.init(0x9e3779b97f4a7c15);
const rng = prng.random();
var buf: [4096]u8 = undefined;
var i: usize = 0;
while (i < 10_000) : (i += 1) {
const len = rng.intRangeAtMost(usize, 0, buf.len);
const input = buf[0..len];
rng.bytes(input);
_ = parse(input) catch {};
}
}
External harnesses (optional)
If you need AFL++/libFuzzer infrastructure (shared corpora, distributed fuzzing, custom instrumentation), export a C ABI entrypoint and drive it from an external harness. Example outline:
- Export a stable entrypoint:
export fn fuzz_target(ptr: [*]const u8, len: usize) void. - Build a static library with
zig build-lib. - Link it from an external harness (AFL++ via
cargo-afl) and run with a seed corpus.
C interop
const c = @cImport({
@cInclude("stdio.h");
});
pub fn main() void {
_ = c.printf("Hello from C!\n");
}
Pitfalls
- Multithreading: false sharing, oversubscription, shared allocator contention.
- SIMD: misaligned loads on some targets, reading past the end, non-associative FP reductions.
- SIMD bitmasks: when you
@bitCast@Vector(N, bool)to an int mask, bit 0 is lane/index 0. std.heap.GeneralPurposeAllocatoris deprecated (alias ofDebugAllocator); keep for existing code, prefer explicit allocator choices for new code.- Make ownership explicit; always free heap allocations.
- Avoid returning slices backed by stack memory.
[*c]Tis nullable;[*]Tis non-null.- Use
zig fetch --saveto populatebuild.zig.zonhashes.
Activation cues
- “zig” / “ziglang” / “.zig”
- “build.zig” / “build.zig.zon”
- “zig build” / “zig test”
- “comptime” / “allocator” / “@typeInfo” / “@compileError”
- “SIMD” / “@Vector” / “std.simd”
- “thread” / “std.Thread” / “Thread.Pool” / “WaitGroup”