| name | zig-guide |
| description | Zig language guardrails, patterns, and best practices for AI-assisted development.
Use when working with Zig files (.zig), build.zig, or when the user mentions Zig.
Provides comptime patterns, allocator conventions, C interop guidelines,
and testing standards specific to this project's coding standards.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"language","language":"zig","extensions":".zig"} |
Zig Guide
Applies to: Zig 0.13+, Systems Programming, CLIs, Embedded, Game Engines, C/C++ Replacement
Core Principles
- Explicit Over Implicit: No hidden control flow, no hidden allocations, no operator overloading, no implicit conversions
- Allocator Passing: Every function that allocates receives an
std.mem.Allocator as a parameter; never use a global allocator
- Errors Are Values: Use error unions (
!) with try, catch, and errdefer; never silently discard errors
- Comptime Over Runtime: Move computation to compile time with
comptime; use it for generics, validation, and code generation
- Safety With Escape Hatches: Keep runtime safety enabled by default; disable only in measured hot paths with a justifying comment
Guardrails
Code Style
- Run
zig fmt before every commit (non-negotiable)
camelCase functions/variables, PascalCase types/structs/enums, SCREAMING_SNAKE_CASE module constants
- Prefer
snake_case for file names (my_module.zig)
- Discard unused values explicitly:
_ = value; (no _ prefix naming)
- Prefer
const over var everywhere; mutability requires justification
- All public declarations (
pub fn, pub const) must have a doc comment (///)
Memory and Allocators
- Every function that heap-allocates must accept an
std.mem.Allocator parameter
- Always pair
alloc/free with defer or errdefer at the call site
- Use
std.heap.ArenaAllocator for batch allocations freed together
- Use
std.heap.GeneralPurposeAllocator in debug builds (detects leaks, double-free)
- Never store an allocator in a struct unless the struct owns long-lived memory
- Prefer stack allocation (
var buf: [256]u8 = undefined;) for small, bounded buffers
- Slices (
[]T, []const T) over raw pointers ([*]T) whenever possible
Error Handling
- Return error unions (
!T) for all fallible operations
- Use
try to propagate; use catch only where you can handle or log meaningfully
- Use
errdefer to clean up resources on error paths
- Define domain-specific error sets; avoid
anyerror except at top-level boundaries
- Never ignore errors:
_ = fallibleFn(); is forbidden outside tests
const ConfigError = error{ FileNotFound, InvalidFormat, MissingField };
fn loadConfig(allocator: std.mem.Allocator, path: []const u8) ConfigError!Config {
const file = std.fs.cwd().openFile(path, .{}) catch return error.FileNotFound;
defer file.close();
// parse and return ...
}
Comptime
- Use
comptime parameters for generics instead of runtime polymorphism
- Use
@typeInfo and @Type for type introspection at compile time
- Use
@compileError to produce clear messages for invalid type combinations
- Prefer
inline for only when the loop count is comptime-known and small
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
fn ensureUnsigned(comptime T: type) void {
if (@typeInfo(T) != .int or @typeInfo(T).int.signedness == .signed)
@compileError("expected unsigned integer type");
}
Safety
- Keep runtime safety enabled in debug and test builds
- Validate all external inputs (file I/O, network, FFI boundaries)
- Use
std.math.add / std.math.mul for checked arithmetic; sentinel slices ([:0]const u8) for C strings
- Avoid
@ptrCast and @intFromPtr unless interfacing with C; justify each use
Key Patterns
Optionals
fn findUser(users: []const User, id: u64) ?*const User {
for (users) |*user| {
if (user.id == id) return user;
}
return null;
}
const user = findUser(users, 42) orelse return error.UserNotFound;
if (findUser(users, 42)) |u| {
std.debug.print("Found: {s}\n", .{u.name});
}
Defer and Errdefer
fn createConnection(allocator: std.mem.Allocator, host: []const u8) !*Connection {
const conn = try allocator.create(Connection);
errdefer allocator.destroy(conn); // only runs on error return
conn.* = .{ .socket = try std.net.tcpConnectToHost(allocator, host, 8080), .allocator = allocator };
errdefer conn.socket.close();
try conn.performHandshake();
return conn;
}
Slices vs Arrays
const fixed: [4]u8 = .{ 1, 2, 3, 4 }; // fixed-size array (stack)
const c_str: [:0]const u8 = "hello"; // sentinel-terminated (C compat)
fn sum(values: []const u32) u64 { // slice (preferred for params)
var total: u64 = 0;
for (values) |v| total += v;
return total;
}
Packed and Extern Structs
const StatusRegister = packed struct {
carry: u1, zero: u1, irq_disable: u1, decimal: u1,
brk: u1, _reserved: u1, overflow: u1, negative: u1,
};
const CFileHeader = extern struct { magic: u32, version: u16, flags: u16, size: u64 };
Testing
const testing = @import("std").testing;
test "add returns correct sum" {
try testing.expectEqual(@as(i32, 5), add(2, 3));
}
test "string list detects leaks via testing allocator" {
var list = StringList.init(testing.allocator); // leak detection built-in
defer list.deinit();
try list.add("hello");
try testing.expectEqual(@as(usize, 1), list.items.items.len);
}
test "readFileContents returns error for missing file" {
try testing.expectError(error.FileNotFound, readFileContents(testing.allocator, "/nonexistent"));
}
Testing Standards
- Test names describe behavior:
test "user creation fails with empty name"
- Unit tests live in
test blocks inside the same .zig file
- Integration tests go in
tests/ directory
- Use
std.testing.allocator -- it detects memory leaks automatically
- Key assertions:
expectEqual, expectError, expectEqualStrings, expectEqualSlices
- Coverage target: >80% for library code, >60% for application code
Tooling
zig build
zig build test
zig build run
zig fmt src/
zig test src/main.zig
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
See patterns.md for a build.zig template.
C Interop
- Use
@cImport / @cInclude for C headers (not manual extern declarations)
- Use
[*c]T only at FFI boundary; convert to []T or ?*T immediately
- Use
std.mem.span to convert [*:0]const u8 (C string) to Zig slice
- Always call
exe.linkLibC() in build.zig when using libc
- Validate all values received from C before using in safe Zig code
References
For full implementation examples, see:
- references/patterns.md -- Arena allocator, GPA leak detection, comptime generics, comptime interface validation, C library wrapper (SQLite), sentinel string conversion, tagged union state machines, errdefer chains, build.zig template
External References