Deep dive
Decision table
| Job | Pick | Why |
|---|---|---|
| New low-level component, small expert team | Zig | Explicit, simple, fast compiles, easy C interop |
| Gradually modernizing a C codebase | Zig build system + C sources, new files in Zig | Same ABI, no rewrite |
| Cross-compiling C for many targets / glibc versions | zig cc -target x86_64-linux-gnu.2.28 | Bundled headers and libc stubs |
| Static Rust binaries for odd targets | cargo zigbuild | Zig as linker |
| Network-facing parser with untrusted input | Rust | Compile-time memory safety |
| Large team, long-lived product | Rust, C++ or Go | Stability, hiring, libraries |
Memory and performance model
- Allocators are values passed explicitly:
page_allocator,ArenaAllocator(free everything at once),FixedBufferAllocator(stack memory), a debug allocator (leak and double-free detection),c_allocatorfor malloc. defer/errdeferfor cleanup on scope exit / error exit.- Slices (
[]T) carry length; pointer types distinguish single-item*T, many-item[*]T, and sentinel-terminated[:0]u8. - Integer overflow is a checked panic in Debug/ReleaseSafe; wrapping (
+%) and saturating (+|) operators are explicit. - Performance equals C with LLVM;
@Vectorfor portable SIMD; packed structs and explicit alignment for layouts.
Interop
- C libraries:
@cImport(@cInclude("zlib.h"))and link the library inbuild.zig(linkSystemLibrary); no bindings generator needed. - Exporting to other languages:
export fnwith C ABI types; build a static or shared library frombuild.zig. - Generics are plain comptime functions returning types, so there is no separate template or macro language to learn.
Concurrency model
OS threads (std.Thread), mutexes, atomics with explicit orderings, thread pool in std. No async/await in the current language (removed in 0.11); the std I/O interface is being redesigned around an injectable Io implementation. For high-performance I/O, projects use io_uring/kqueue directly (TigerBeetle, Bun).
Code
Written for Zig 0.14/0.15 (zig test ring.zig, zig build-exe ring.zig -O ReleaseSafe).
const std = @import("std");
// comptime generics: a function that returns a type.
fn Ring(comptime T: type, comptime cap: usize) type {
return struct {
buf: [cap]T = undefined,
head: usize = 0,
len: usize = 0,
const Self = @This();
pub fn push(self: *Self, x: T) error{Full}!void {
if (self.len == cap) return error.Full;
self.buf[(self.head + self.len) % cap] = x;
self.len += 1;
}
pub fn pop(self: *Self) ?T {
if (self.len == 0) return null;
const x = self.buf[self.head];
self.head = (self.head + 1) % cap;
self.len -= 1;
return x;
}
};
}
// Allocation is explicit and can fail; the caller chooses the allocator.
fn sumSquares(allocator: std.mem.Allocator, n: usize) !u64 {
const xs = try allocator.alloc(u64, n);
defer allocator.free(xs);
for (xs, 0..) |*x, i| {
const v: u64 = @intCast(i);
x.* = v * v; // overflow would panic in safe modes
}
var total: u64 = 0;
for (xs) |x| total += x;
return total;
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // frees every arena allocation at once
const a = arena.allocator();
var r: Ring(u32, 4) = .{};
try r.push(7);
if (r.pop()) |v| std.debug.print("popped {d}\n", .{v});
std.debug.print("sum = {d}\n", .{try sumSquares(a, 1000)});
}
test "ring reports full" {
var r: Ring(u8, 2) = .{};
try r.push(1);
try r.push(2);
try std.testing.expectError(error.Full, r.push(3));
try std.testing.expectEqual(@as(?u8, 1), r.pop());
}
test "sumSquares frees what it allocates" {
// testing.allocator fails the test on leaks or double frees
try std.testing.expectEqual(@as(u64, 285), try sumSquares(std.testing.allocator, 10));
}
Efficiency
- Cost: no GC, no hidden allocation, and all Zig code lives in one compilation unit optimized together (Zig overview), so the cost model is C's on LLVM. Safety checks (bounds, overflow) exist in Debug and ReleaseSafe and cost a branch each; ReleaseFast and ReleaseSmall turn them into unchecked illegal behavior (Zig language reference). Bound: memory access and allocator choice.
- Levers:
FixedBufferAllocatororArenaAllocator— no per-objectfree, no syscalls once the buffer exists (order of) — request-scoped or fixed-size workReleaseSafeby default,ReleaseFastonly on measured hot code — keeps bounds and overflow checks where they are cheap enough (Zig language reference) — services; per-scope via@setRuntimeSafety@Vector— portable SIMD exposed in the language (Zig overview) — numeric and parsing loopsReleaseSmall— smaller binaries (Zig language reference) — embedded and WASM targetscomptime— computation and specialization at compile time, no runtime cost (Zig language reference) — generics, lookup tables
- Naive → standard → state of the art:
- general-purpose allocator per object, Debug build · checks and leak detection on, slow
- arena per request, ReleaseSafe · near-C speed with checks
- fixed buffers,
@Vector, ReleaseFast on measured hot paths · C speed or better, no checks there
Build and deploy
build.zigis Zig code: declare executables, libraries, C sources, tests, cross targets.zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafeproduces a static binary from any host.- Package manager:
build.zig.zonwith URL + content hash; ecosystem is small, so expect to wrap C libraries. - Ship ReleaseSafe by default; use ReleaseFast only for measured hot code (checks can also be toggled per scope with
@setRuntimeSafety). - Pin the exact Zig version per project (e.g., with a toolchain manager); upgrade deliberately.
Pitfalls
- Upgrading Zig without budgeting for breaking changes in std (I/O, containers, allocator APIs changed in 0.14-0.15).
- Returning slices that point into a freed arena or stack buffer: not caught at compile time.
- ReleaseFast everywhere: turns overflows and out-of-bounds into silent UB like C.
- Forgetting
errdeferfor partially initialized resources on error paths. - Assuming Rust-like safety from "safety checks": they catch bounds and overflow, not lifetimes or data races.
- Thin documentation for std; read the std source, it's the reference.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Zig compiler + toolchain | Zig | ziglang/zig | brew install zig | — | — | — | — | zig build, zig cc, cross-compiler; repo moved from GitHub to Codeberg |
| ZLS | Zig | zigtools/zls | brew install zls | — | — | — | — | language server |
| TigerBeetle | Zig | tigerbeetle/tigerbeetle | download from the README (curl -Lo tigerbeetle.zip https://linux.tigerbeetle.com) | — | — | — | — | large real Zig codebase (financial database) |
| Ghostty | Zig | ghostty-org/ghostty | see ghostty.org | — | — | — | — | terminal emulator; libghostty for C and Zig |