zig-skills/SKILL.md

14 KiB

name description
zig Up-to-date Zig programming language patterns for version 0.16.0. Use when writing, reviewing, or debugging Zig code, working with build.zig/build.zig.zon, translating C headers, or using comptime metaprogramming. Critical for avoiding outdated patterns from training data, especially std.Io ownership, Juicy Main, std.Io.Dir/File/net/process APIs, Io synchronization primitives, removed @Type, @cImport migration, unmanaged containers, and removed std.Thread.Pool.

Zig Language Reference (v0.16.0)

Zig evolves rapidly. Training data contains outdated patterns that cause compilation errors or subtle runtime bugs. This skill documents modern Zig 0.16.0 patterns and points to deeper references when needed.

Always load Zig 0.16 Release Notes Migration Reference for upgrade work, stdlib API questions, C translation changes, I/O changes, or code review involving old Zig examples. It is grounded in the official Zig 0.16.0 release notes: https://ziglang.org/download/0.16.0/release-notes.html

Critical: I/O Is an Interface

Zig 0.16.0 routes many new blocking, OS-facing, and nondeterministic APIs through a std.Io instance. For new APIs, treat entropy, time, networking, process, and file operations as std.Io responsibilities. Existing stdlib APIs are not uniformly parameterized this way, so check each installed signature.

Preferred app entry:

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;

    const args = try init.minimal.args.toSlice(init.arena.allocator());
    _ = .{ gpa, io, args };
}

Preferred test I/O:

const io = std.testing.io;

Temporary adapter only when a boundary cannot yet receive io:

var threaded: std.Io.Threaded = .init_single_threaded;
const io = threaded.io();

General rule:

  • Accept io: std.Io where the API performs or later owns I/O.
  • Store io on long-lived systems, allocators, registries, timers, queues, and clients that need it after initialization.
  • Do not create local fallback Io.Threaded instances deep in library code unless explicitly approved as a transition shim.
  • When converting existing code, prefer threading io through signatures over global state.

Critical: File, Net, Process, Time, Random

Files

Use std.Io.Dir and std.Io.File; old std.fs.File/std.fs.Dir patterns are obsolete.

const cwd = std.Io.Dir.cwd();
const file = try cwd.openFile(io, "data.txt", .{});
defer file.close(io);

var buf: [4096]u8 = undefined;
var reader = file.reader(io, &buf);
const bytes = try reader.interface.allocRemaining(gpa, .limited(1024 * 1024));
defer gpa.free(bytes);
const file = try std.Io.Dir.cwd().createFile(io, "out.txt", .{});
defer file.close(io);

var buf: [4096]u8 = undefined;
var writer = file.writer(io, &buf);
try writer.interface.print("hello {s}\n", .{"world"});
try writer.interface.flush();

Convenience:

const bytes = try std.Io.Dir.cwd().readFileAlloc(io, "data.txt", gpa, .limited(max_size));
defer gpa.free(bytes);

try std.Io.Dir.cwd().writeFile(io, .{
    .sub_path = "out.txt",
    .data = "hello\n",
});

Reader and Writer

std.io is replaced by std.Io. GenericReader, AnyReader, GenericWriter, AnyWriter, and FixedBufferStream are gone.

var r: std.Io.Reader = .fixed("a\nb\n");
while (try r.takeDelimiter('\n')) |line| {
    _ = line;
}

var out_buf: [256]u8 = undefined;
var w: std.Io.Writer = .fixed(&out_buf);
try w.print("value={d}", .{42});
const written = w.buffered();

Custom format methods use the writer-only signature:

pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
    try writer.print("{s}:{d}", .{ self.name, self.value });
}

Use {f} to invoke format methods.

Process

Use Juicy Main for args/env and std.process.run / std.process.spawn for child processes.

const result = try std.process.run(gpa, io, .{
    .argv = &.{ "git", "status", "--short" },
    .stdout_limit = .limited(64 * 1024),
    .stderr_limit = .limited(64 * 1024),
});
defer gpa.free(result.stdout);
defer gpa.free(result.stderr);
var child = try std.process.spawn(io, .{
    .argv = &.{ "tool", "--flag" },
    .stdout = .pipe,
    .stderr = .pipe,
});
defer child.kill(io);
const term = try child.wait(io);
_ = term;

Current directory:

const cwd = try std.process.currentPathAlloc(io, gpa);
defer gpa.free(cwd);

Networking and HTTP

Use std.Io.net and pass io to high-level clients.

var client: std.http.Client = .{
    .allocator = gpa,
    .io = io,
};
defer client.deinit();

Time

0.16 routes runtime-dependent time through std.Io. Choose an explicit std.Io.Clock and pass io; for example, use std.Io.Timestamp.now(io, .real) for wall time and .boot or .awake for elapsed-time measurements. std.Io.Timestamp is not a one-for-one timer replacement: preserve the old callsite's clock semantics during migration.

Applications may centralize timestamp reads behind a shared helper so clock selection and timestamp semantics remain consistent.

Random and Entropy

Do not use std.crypto.random directly in new 0.16 code.

var seed: [32]u8 = undefined;
io.random(&seed);

const source: std.Random.IoSource = .{ .io = io };
const rng = source.interface();

Critical: I/O Synchronization

Blocking synchronization moved from std.Thread to std.Io so it cooperates with the runtime backend and cancelation.

Migration map:

  • std.Thread.Mutex -> std.Io.Mutex
  • std.Thread.Condition -> std.Io.Condition
  • std.Thread.Semaphore -> std.Io.Semaphore
  • std.Thread.RwLock -> std.Io.RwLock
  • std.Thread.ResetEvent -> std.Io.Event
  • std.Thread.WaitGroup -> std.Io.Group
  • std.Thread.Futex operations -> io.futexWait, io.futexWaitTimeout, io.futexWaitUncancelable, and io.futexWake on a suitably aligned four-byte value

Cancelable lock:

try mutex.lock(io);
defer mutex.unlock(io);

Uncancelable lock for short critical sections or cleanup:

mutex.lockUncancelable(io);
defer mutex.unlock(io);

Do not replace these with custom spin loops. Use std primitives unless a measured, documented special case requires otherwise.

Critical: Language and Builtins

Removed or Deprecated

  • @Type removed. Use @Int, @Struct, @Union, @Enum, @Pointer, @Fn, @Tuple, or @EnumLiteral. Declare opaque types with opaque {}.
  • @cImport is deprecated long-term. Prefer b.addTranslateC(...) and import translate_c.createModule().
  • async/await keywords remain removed; use std.Io task APIs, an application scheduler, or explicit threads.
  • usingnamespace is removed; explicitly re-export names.
  • @fence is removed; use stronger atomic orderings or RMW operations.
  • @intFromFloat converts a finite, in-range float to an integer. @trunc truncates a floating value while retaining a floating result type.

Packed and Extern Layout

  • Packed structs/unions cannot contain pointers.
  • Packed unions need unambiguous bit sizes and may use explicit backing integers.
  • Extern enum/packed types need explicit backing types.
  • ABI-sensitive bindings must keep comptime size/alignment/offset assertions.

Vectors and Arrays

  • Runtime vector indexes are forbidden.
  • Vectors and arrays no longer support in-memory coercion; convert explicitly.

Safer Pointer Returns

The compiler rejects trivially returning the address of an expired local. When returning pointers, prove ownership and lifetime explicitly.

Critical: Containers and Allocators

Use .empty for empty unmanaged containers and pass allocators to methods.

var list: std.ArrayList(u32) = .empty;
defer list.deinit(gpa);
try list.append(gpa, 42);

0.16 container changes:

  • std.ArrayHashMap, std.AutoArrayHashMap, and std.StringArrayHashMap removed.
  • Use std.array_hash_map.Custom, std.array_hash_map.Auto, and std.array_hash_map.String.
  • std.PriorityQueue and std.PriorityDequeue no longer store allocators.
  • Priority queues use push/pop terminology and .empty initialization.
  • std.SegmentedList removed.
  • std.heap.ThreadSafe removed.
  • std.heap.ArenaAllocator's allocator interface is thread-safe when its child allocator is thread-safe. Do not assume a universal lock-free guarantee.

Critical: Build System

The 0.15 root-module build style remains the baseline:

const exe = b.addExecutable(.{
    .name = "app",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    }),
});

Use module-level APIs:

exe.root_module.addImport("helper", helper_mod);
exe.root_module.linkSystemLibrary("SDL3", .{});
exe.root_module.addCSourceFiles(.{ .files = &.{"src/foo.c"} });

0.16 additions and removals:

  • --prominent-compile-errors removed; use --error-style minimal.
  • New --multiline-errors styles are available.
  • zig build test --test-timeout <duration> can bound test runtime.
  • Build.makeTempPath and RemoveDir step removed.
  • Use b.addTempFiles, b.addMutateFiles, b.tmpPath, and WriteFile temp/mutate modes.
  • Use build-system addTranslateC for new translated C bindings.

Critical: C Translation and ABI Safety

Translate-c is now Aro/translate-c based rather than libclang based. It is intended to be non-breaking, but subtle generated-code changes can happen.

For ABI-sensitive C bindings:

  • Keep translated output under review.
  • Preserve size/alignment/offset comptime asserts.
  • Compare enum values, bit flags, packed layouts, extern structs, calling conventions, and macro translations.
  • Do not dismiss translate-c diffs as cosmetic until ABI checks pass.

Application Integration Guidance

When integrating these patterns into an application:

  • Prefer an explicit std.Io parameter; an application-owned accessor is also reasonable where the architecture already provides one.
  • New file operations require an io parameter at the API boundary.
  • Long-lived allocators/registries/queues that lock or perform I/O should accept io at init and expose it if needed.
  • Prefer a shared application helper for common wall-clock timestamps.
  • Keep dynamic-library behavior behind a platform abstraction, especially on Windows where std.DynLib support changed.

Import Loop Guidance

Do not over-index on dependency loops when importing source files directly.

  • @import("mything.zig") is a direct file import and should be treated as safe.
  • Only consider import-loop risk when importing a module name such as @import("module").
  • A top-level file plus same-named helper directory is normal:
// module.zig
const moduleComponent = @import("module/moduleComponent.zig");

// module/moduleComponent.zig
const module = @import("../module.zig");

Quick Fixes

Symptom Zig 0.16 Fix
missing std.fs.File / std.fs.Dir Use std.Io.File / std.Io.Dir and pass io
file close() takes an argument Use file.close(io) or dir.close(io)
old stdout writer fails std.Io.File.stdout().writer(io, &buf)
old std.process.Child.run fails Use std.process.run(gpa, io, .{ ... })
old Child.init fails Use std.process.spawn(io, .{ ... })
old std.crypto.random fails Use io.random or std.Random.IoSource
old std.Thread.Mutex.lock() fails in I/O code Use std.Io.Mutex and lock(io) / lockUncancelable(io)
std.Thread.Pool missing Use an application scheduler or carefully designed std.Io.Group / io.async
@Type missing Use specific type-constructor builtins
@cImport warning Move translation to b.addTranslateC(...)
std.mem.indexOf not found Use std.mem.find / findScalar / cut helpers
{D} duration format fails Format std.Io.Duration with {f}
std.fmt.Formatter missing Use std.fmt.Alt
std.fmt.format missing Use std.Io.Writer.print
std.fs.path.relative signature changed Pass cwd path and optional environment map
stat.atime type changed Handle optional access time

References

Load these selectively:

  • Zig 0.16 Release Notes Migration Reference - First stop for version-specific upgrade work, organized by official release-note section.
  • std.Io - 0.16 reader/writer, file I/O, Io ownership, sync and task concepts.
  • std.fs / std.Io.Dir - File system migration and common 0.16 patterns.
  • std.process - Juicy Main, args/env, child process APIs, current directory.
  • std.Thread / std.Io sync - Thread spawning plus migration to std.Io synchronization primitives.
  • std.time / std.Io time - Timestamp migration and duration handling.
  • std.Build - Build graph, modules, package deps, 0.16 build-system changes.
  • Built-in Functions - Builtin syntax and 0.16 @Type replacement.
  • Comptime Reference - Type reflection, type construction, lazy type resolution, compile-time patterns.
  • C Interop - C ABI, exports, translated headers, and binding checks.
  • Zig Patterns - Idiomatic patterns. Treat 0.16 release-note reference as authoritative when examples conflict.
  • Code Review - Review checklist. Always apply the 0.16 release-note checks first.

General references remain available for specific modules: std.mem, std.fmt, std.json, std.zon, std.crypto, std.http, std.Io.net, std.compress, std.tar, std.zip, std.testing, std.debug, std.log, std.atomic, std.simd, and other files under references/.

If any reference still contains a historical 0.14/0.15 example, the Zig 0.16 release-notes reference and the top-level rules in this file win.