Zig — Reference

Source: https://ziglang.org/documentation/master/

Zig

  • Created: announced 2016-02-08 by Andrew Kelley (Wikipedia).
  • Latest stable: Zig 0.16.0, released 2026-04-13 (ziglang.org/download). Zig is pre-1.0; breaking changes still happen between releases.
  • Owner: Zig Software Foundation (501(c)(3) non-profit, since 2020). License: MIT.
  • Paradigms: imperative, procedural, low-level systems; some functional flavor; metaprogramming via comptime.
  • Typing: static, strong, nominal; type inference where unambiguous; types are first-class comptime values.
  • Memory: manual; no GC, no hidden allocations, no destructors. Allocators are explicit, passed-in values. Optionals (?T) and error unions (E!T) handle absence/failure.
  • Compilation: AOT to native via self-hosted compiler (LLVM backend, with WIP self-hosted x86_64/aarch64 backends). zig cc is a drop-in C/C++ cross-compiler.
  • Primary domains: systems programming, embedded, game engines, replacing C, cross-compilation toolchain, WebAssembly. Bun runtime is the most visible Zig user.
  • Official docs: https://ziglang.org/documentation/master/

At a glance

Zig is a small, explicit, low-level language designed by people who got tired of C’s footguns and C++‘s complexity but didn’t want a garbage collector. The language has no preprocessor, no macros, no operator overloading, no exceptions, no hidden control flow, and no hidden allocations. The metaprogramming story is comptime — running normal Zig code at compile time on types as values. The build system, package manager, C compiler, and cross-compiler are all the same zig binary; you can target any supported triple from any host without installing anything else.

Getting started

Install: download tarball from https://ziglang.org/download/ (no system install needed — just put on PATH). Package managers: brew install zig, pacman -S zig, winget install zig.zig. Zig versions are specific: pin via your project’s build.zig.zon (minimum_zig_version).

Version manager: zigup (community, recommended) or zvm. Many projects pin a nightly via build.zig.zon.

Hello world (file hello.zig):

const std = @import("std");
 
pub fn main() !void {
    try std.io.getStdOut().writer().print("Hello, world!\n", .{});
}

Run: zig run hello.zig. Build executable: zig build-exe hello.zig.

Project layout:

myproj/
  build.zig            # build script (Zig code)
  build.zig.zon        # manifest: name, version, deps, paths
  src/
    main.zig
    root.zig
  zig-out/             # build outputs (gitignored)
  .zig-cache/          # incremental cache (gitignored)

Initialize with zig init (creates exe + lib scaffold).

Build / package tool: zig build runs build.zig. There is no separate package manager — build.zig.zon lists dependencies (URL + hash); zig build fetches and verifies them. Per-project cache in .zig-cache/, global cache in ~/.cache/zig/.

REPL: there is no official REPL. zig run file.zig is the script-style replacement.

Basics

Types and literals:

  • Integers: i8/i16/i32/i64/i128, u8/u16/...u128, plus arbitrary widths i7, u23, u1 (a bit). usize/isize are pointer-sized.
  • Floats: f16, f32, f64, f80, f128.
  • bool, void, noreturn, anyopaque (C’s void *).
  • comptime_int, comptime_float — unbounded literal types only valid at compile time.
  • Composites: arrays [N]T (length is part of the type), slices []T (pointer + length), struct, enum, union, error, ?T (optional), E!T (error union).
  • Pointers: *T (single), [*]T (many, no length), [*:0]T (sentinel-terminated, e.g. C strings), *const T.
  • Strings: string literals are *const [N:0]u8 — sentinel-terminated u8 arrays.

Variables/scoping: const x = 1; (immutable, default), var x: i32 = 1; (mutable). Block-scoped. const and var at file scope are globals. All declarations require a value or undefined — uninitialized is var x: i32 = undefined;, which is a contract that you’ll write before reading.

Control flow: if, else, while, for, switch, defer, errdefer, unreachable, break, continue, return. for (slice) |item, i| iterates with optional index. switch is exhaustive on enums and produces a value.

Functions:

fn add(a: i32, b: i32) i32 {
    return a + b;
}
 
fn divide(a: i32, b: i32) !i32 {              // !T means "error union"
    if (b == 0) return error.DivByZero;
    return @divTrunc(a, b);
}
 
pub fn main() !void {
    const r = try divide(10, 2);              // try = unwrap or propagate err
    _ = r;
}

No closures (functions can’t capture runtime state — but comptime data is accessible). Anonymous functions exist; they cannot capture.

Strings: not a special type — they are []const u8 (UTF-8 bytes, by convention). No automatic UTF-8 handling in the stdlib’s basic indexing; std.unicode provides utilities. Concatenation is allocation: std.mem.concat(allocator, u8, &.{ "a", "b" }).

Collections: std.ArrayList(T), std.AutoHashMap(K, V), std.StringHashMap(V), std.MultiArrayList(T) (struct-of-arrays), std.SegmentedList(T, prealloc), std.PriorityQueue(T, Ctx, lessThan). All take an allocator at construction.

Intermediate

Type system depth: nominal, static. Generics are not a separate feature — they’re functions returning types at comptime:

fn ArrayList(comptime T: type) type {
    return struct {
        items: []T,
        capacity: usize,
        // methods...
    };
}
const IntList = ArrayList(i32);

Tagged unions: union(enum) { int: i32, str: []const u8 } — switch-checked exhaustively.

Modules: each .zig file is a struct (an implicit container). @import("path") returns that struct. There is no import x as y syntax — assign the result: const json = @import("std").json;. Packages defined in build.zig get aliases the build system makes importable.

Error handling: errors are values, not exceptions. An error set is an enum-like type:

const FileError = error{ NotFound, AccessDenied };
fn open(path: []const u8) FileError!File { ... }

E!T is the error union. Operators:

  • try expr — propagate error up.
  • catch — handle: const x = open("f") catch |err| return err;
  • catch unreachable — assert no error (panic in safe modes).
  • errdefer — defer that only runs on the error path.

Concurrency primitives: as of 0.16.0, stackful coroutine async/await was removed in 0.11 and is being redesigned; do not write new async code yet. Today: OS threads via std.Thread.spawn, std.Thread.Mutex, std.atomic, std.Thread.Pool. Single-threaded event loops via std.io.poll. The io_uring and epoll/kqueue story is currently library-level (Tigerbeetle’s io.zig, libxev).

I/O: std.io provides Reader and Writer interfaces (vtable-based abstractions). std.fs for filesystem (cwd().openFile, createFile, readFileAlloc). std.net for sockets. std.process for subprocess + env vars. std.posix for raw syscalls.

Stdlib highlights: std.mem (memory ops, allocators), std.fmt (formatting), std.heap (allocators: GeneralPurposeAllocator, ArenaAllocator, FixedBufferAllocator, c_allocator, page_allocator), std.testing (expect, expectEqual), std.json, std.crypto, std.compress, std.hash, std.simd, std.Build.

Advanced

Memory model: there is no GC. Every allocation is explicit and goes through an Allocator interface (a vtable struct). Common patterns:

  • Arena: std.heap.ArenaAllocator — bulk-free everything at end (great for request-scoped).
  • GeneralPurposeAllocator (GPA): leak-detecting, double-free-detecting; the dev default.
  • FixedBufferAllocator: fail when fixed slab is full; great for hard real-time and embedded.
  • c_allocator: wraps malloc/free.
  • page_allocator: direct mmap/VirtualAlloc.

Allocator is passed in to every function that allocates. This makes resource discipline auditable from the call site.

Concurrency deep dive: as of 0.16, the recommended approach for parallelism is OS threads (std.Thread, std.Thread.Pool). For I/O concurrency, libraries like libxev provide a cross-platform event loop. Stackless async is being reworked (the Andrew Kelley July 2024 post outlines Io as an interface — see ziglang.org blog).

FFI: best-in-class.

  • @cImport({ @cInclude("stdio.h"); }) literally parses C headers and exposes them as Zig declarations. (Translation via translate-c, the same engine as zig translate-c.)
  • Calling C: just call the resulting function. ABIs match extern fn.
  • Exporting to C: export fn my_fn(x: c_int) c_int { ... } — visible as my_fn symbol.
  • Linking C source: b.addCSourceFiles(...) in build.zig.
  • zig cc is a C compiler (clang frontend, Zig’s improved cross-compilation): zig cc -target aarch64-linux-musl hello.c.

Reflection: @TypeOf(x), @typeInfo(T) — full structural introspection at comptime (returns a tagged union describing fields, decls, params, etc.). Build generic JSON serializers, pretty-printers, ORMs entirely at compile time.

Performance tools:

  • zig build -Doptimize=ReleaseFast (max perf) / ReleaseSafe (kept safety checks) / ReleaseSmall / Debug (default).
  • --summary all prints build steps + cache hit info.
  • perf (Linux), Instruments (macOS), VTune.
  • @setRuntimeSafety(false) disables UB checks per scope.
  • Inline assembly: asm volatile (...) with named operands.
  • @prefetch, @branchHint(.likely), @setFloatMode(.optimized).
  • Built-in benchmarking: std.time.Timer.

God mode

comptime is the single big idea. Mark a parameter or block comptime and Zig evaluates it during compilation:

fn factorial(comptime n: u32) u32 {
    var r: u32 = 1;
    inline for (1..n + 1) |i| r *= @intCast(i);
    return r;
}
const six = factorial(3);                 // computed at compile time
const tbl: [factorial(5) + 1]u32 = ...;   // type uses comptime result

Use comptime for:

  • Generics: functions that take comptime T: type and build a struct around it.
  • Conditional compilation: if (builtin.os.tag == .windows) ....
  • Compile-time tables: SIMD lookup tables, regex compilation, JSON schema validation.
  • DSLs: parse a format string at compile time to a typed parser.
  • inline for / inline while — fully unroll, allowing different types per iteration.
  • @compileError("message") to fail compilation on bad input.

@import / @cImport: @import("std"), @import("./util.zig"). @cImport is a block of C preprocessor directives whose result is a Zig namespace.

Vector types: @Vector(4, f32) is a SIMD vector; arithmetic operators apply lanewise. @reduce, @shuffle, @select for cross-lane ops.

Inline assembly: full LLVM-style inline asm with typed input/output operand bindings.

build.zig is Zig: no DSL, no YAML. The build system is a Zig program that builds a graph of Steps. Cross-compile any project for any target by changing one line:

const target = b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl });

Cross-compilation everywhere: zig build -Dtarget=x86_64-windows-gnu works from a Mac or Linux box without any toolchain install. Same for zig cc -target .... This is unique among general-purpose toolchains.

Allocators as a first-class concept: every library that allocates takes an Allocator parameter. There is no global heap. This makes test-time allocators trivial (std.testing.allocator detects leaks) and embedded use possible (pass a FixedBufferAllocator).

Async/await transform (legacy / pre-0.11; being redesigned): the compiler used to perform a stackless coroutine transform — turning an async function into a state machine struct with a frame() method. The new design under discussion exposes I/O as an interface (std.Io) so async-ness is a property of the I/O implementation, not the function color.

Zig as a C compiler: zig cc and zig c++ use Zig’s bundled clang frontend with libcs (musl, glibc multi-version, MSVCRT, mingw, wasi-libc, macos-libc) bundled. Famously used as a portable, easily cross-targetable build of clang.

Idioms & style

  • Naming: lowerCamelCase for functions and variables; TitleCase for types; SCREAMING_SNAKE_CASE rarely (often just TitleCase for constants too); file names snake_case.zig.
  • Formatter: zig fmt is built into the toolchain; no config, no opinions to argue.
  • Linter: no separate linter — the compiler’s safety modes and comptime checking cover most ground. zlint (community) exists.
  • Idiomatic patterns:
    • try early, errdefer for cleanup on error path.
    • Pass allocator first param to constructor; keep it on the struct.
    • Prefer slices []T over many-pointers [*]T.
    • Use tagged unions + exhaustive switch for variants.
    • Use comptime to monomorphize, not runtime polymorphism.
    • Single-file modules; export via top-level pub decls.
  • Expert review focus: allocator correctness (leaks, double-free, use-after-free in Debug/ReleaseSafe), error set granularity (avoid anyerror), undefined writes before reads, integer overflow (+%/-% are wrap operators), pointer aliasing in noalias parameters, comptime explosion (build-time cost), C ABI struct layout when using extern.

Ecosystem

  • HTTP / web: std.http (built-in client + server), zap (mongoose-based), httpz.
  • Game / graphics: zig-gamedev (mach-glfw, zmath, zaudio), Mach engine, raylib bindings, sokol bindings.
  • Database: pg.zig (Postgres), zqlite (SQLite wrapper), zigzag (Redis).
  • Parsing / serialization: std.json, zig-yaml, std.compress.gzip, tres (TOML).
  • CLI: zig-cli, zig-clap.
  • Testing: std.testing is built-in (test "name" { ... } blocks; run with zig test file.zig or zig build test). No third-party framework needed; expect, expectEqual, expectError, expectEqualSlices.
  • Docs: zig build-exe -femit-docs generates static HTML from doc comments (//! for module, /// for decl). Style: https://ziglang.org/documentation/master/std/.
  • Notable users: Bun (JS runtime, Zig is the implementation language), TigerBeetle (financial DB), Uber (zig cc as their cross-compile toolchain at scale), Roc lang compiler, Mach engine, Ghostty terminal.

Gotchas

  • Pre-1.0: APIs change between minor versions. Always pin the compiler version in build.zig.zon.
  • async/await is currently absent (removed 0.11, being redesigned). Don’t write async-style code yet.
  • undefined is a contract you make: writing it is fine, reading it is UB. In Debug/ReleaseSafe, reads are filled with 0xAA to make bugs loud.
  • Integer overflow in +/-/* is UB in ReleaseFast, panics in Debug/ReleaseSafe. Use +% (wrap), +| (saturate), @addWithOverflow.
  • Slice lifetimes: a slice of a stack array becomes invalid when the stack frame returns. Borrow-checker doesn’t exist; you must reason yourself.
  • @cImport of two files with the same C header doesn’t dedupe types — combine all headers into one @cImport block.
  • Allocator forgetting: failing to defer alloc.free(x) is a leak the GPA will report on shutdown — but only if you initialize the GPA correctly with .deinit().
  • Pointer aliasing under noalias: violating the contract is UB.
  • comptime recursion limits can cripple large compile-time computations; tune @setEvalBranchQuota.
  • String == comparison doesn’t work: []const u8 is a slice; use std.mem.eql(u8, a, b).
  • No method syntax for free functions: obj.method(arg) only works if method is declared inside the type’s struct (or extension via @field).
  • Build cache footguns: editing build.zig doesn’t always invalidate downstream. rm -rf .zig-cache zig-out if things get weird.
  • Zig stdlib is unstable: nightly often breaks std.io.Writer, std.Build.Step, etc. Track release notes.

Citations