zig-skills/references/zig-0.16-release-notes.md

24 KiB

Zig 0.16.0 Release Notes Migration Reference

Primary source: https://ziglang.org/download/0.16.0/release-notes.html

Use this file as the first stop when upgrading or reviewing Zig 0.16 code. It is organized in the same broad order as the official release notes and converts the release-note material into practical coding guidance.

Version Posture

Zig 0.16.0 is not a small stdlib polish release. It changes ownership of I/O, synchronization, file system access, process spawning, time, randomness, containers, C translation, and several language edge cases.

General rule for new Zig 0.16 code:

  • APIs that can block, touch the OS, use entropy, query time, spawn work, or otherwise introduce nondeterminism should accept or store a std.Io.
  • Application entry points should get io from Juicy Main (std.process.Init) when possible.
  • Tests should prefer std.testing.io.
  • A local std.Io.Threaded.init_single_threaded is acceptable only as a temporary adapter at a boundary that cannot yet receive an io.

Target Support

The release notes broaden native CI coverage and add/remove several targets. The important migration guidance is:

  • Do not assume an untested target is broken just because old Zig versions were rough there; 0.16 improves stack traces and weakly-ordered architecture behavior.
  • Minimum OS versions changed. Current notable minimums include Linux 5.10, macOS 13, Windows 10, FreeBSD 14.0, NetBSD 10.1, OpenBSD 7.8, and DragonFly BSD 6.0.
  • If a product's target matrix depends on older OS versions, check the official support table before promising support.
  • More targets have usable stack traces, which makes debug/unwind behavior more valuable but also means verbose crash/log paths may become more expensive if left in hot paths.

Language Changes

switch

0.16 extends valid switch prong expressions and fixes several switch edge cases.

Practical notes:

  • Packed structs/unions can appear as switch prong items and compare by backing integer.
  • Decl literals and result-typed expressions such as @enumFromInt are more broadly usable in prongs.
  • Union tag captures are allowed for all prongs, not just inline prongs.
  • Switch prong captures cannot all be discarded.
  • Error switches have stricter and more consistent unreachable-else handling.

@cImport Moves Toward the Build System

@cImport is deprecated as the long-term C translation API. The official upgrade path is:

  1. Put includes in a real header.
  2. Add a build-system b.addTranslateC(...) step.
  3. Import translate_c.createModule() into the Zig module graph.

For ABI-sensitive bindings:

  • Prefer build-system translation for new C bindings.
  • Keep translated bindings stable and check generated ABI-sensitive structs with comptime size/alignment/offset assertions.
  • If generated C output differs across translation approaches, treat it as a bug-risk investigation, not cosmetic churn.

@Type Removed

@Type is replaced by specific type-constructing builtins:

  • @EnumLiteral
  • @Int
  • @Tuple
  • @Pointer
  • @Fn
  • @Struct
  • @Union
  • @Enum

Migration rule:

  • Use the narrow builtin that matches the type you are constructing.
  • Keep @typeInfo for reflection.
  • Replace old @Type(.{ .@"struct" = ... }) helpers with @Struct(...), and similarly for unions/enums/pointers/functions.

Numeric and Vector Changes

0.16 allows small integer types to coerce to floats in more cases, but it also tightens vector and array representation rules.

Practical notes:

  • Runtime vector indexes are forbidden. Use scalar extraction patterns, compile-time indexes, or restructure the vector operation.
  • Vectors and arrays no longer support in-memory coercion. Make conversions explicit with loads, stores, or element-wise construction.
  • Unary float builtins forward result type. Code such as const x: f64 = @sqrt(@floatFromInt(n)); now works as intended.
  • @floor, @ceil, @round, and @trunc perform result-typed floating operations. Use @intFromFloat for finite, in-range float-to-integer conversion.

Returning Local Addresses

The compiler now diagnoses trivially returning the address of an expired local variable.

Review rule:

  • If a function returns a pointer, confirm the pointee outlives the function.
  • Prefer caller-provided output buffers, allocator-owned results, or stable owner structs.

Packed and Extern Type Tightening

0.16 makes packed/extern layout more explicit:

  • Packed union fields must have an unambiguous backing bit size.
  • Pointers are forbidden in packed structs and packed unions; store an integer address only when that is actually the ABI.
  • Packed unions may specify explicit backing integers.
  • Enum and packed types in extern contexts need explicit backing types.

Binding rule:

  • For ABI-sensitive C bindings, preserve or add comptime checks for size, alignment, and field offsets.
  • Do not accept layout churn in translated bindings without checking the C header contract.

Type Resolution

0.16 reworks type resolution. Some dependency loops disappear, while other previously accepted self-dependent constructs are now rejected with clearer diagnostics.

Practical notes:

  • Do not assume every new dependency-loop diagnostic is a false positive.
  • Prefer moving self-referential size/alignment queries behind explicit helper types or runtime fields.
  • Lazy field analysis means more namespace-like types can exist without forcing full field resolution.
  • Pointers to comptime-only types are no longer themselves comptime-only, but dereferencing them at runtime is still invalid.
  • Explicitly aligned pointer types are distinct from naturally aligned pointer types, even if they coerce easily.
  • Zero-bit tuple fields are no longer implicitly marked comptime in type info.

Standard Library

Top-Level Additions, Removals, and Renames

Important removals/renames:

  • SegmentedList removed.
  • std.meta.declList removed.
  • std.Io.GenericWriter, std.Io.AnyWriter, std.Io.null_writer, and std.Io.CountingReader removed.
  • std.Thread.Mutex.Recursive removed.
  • std.fmt.format is replaced by std.Io.Writer.print.
  • std.fmt.Formatter is renamed to std.fmt.Alt.
  • std.fmt.FormatOptions is renamed to std.fmt.Options.
  • std.fmt.bufPrintZ remains as a deprecated zero-sentinel wrapper; use std.fmt.bufPrintSentinel for new code.
  • std.DynLib removed Windows support; use platform APIs (LoadLibraryExW, GetProcAddress) directly or through a local abstraction.
  • BitSet and EnumSet use decl literals instead of initEmpty / initFull.

Important error changes:

  • error.RenameAcrossMountPoints and error.NotSameFileSystem become error.CrossDevice.
  • error.SharingViolation becomes error.FileBusy.
  • error.EnvironmentVariableNotFound becomes error.EnvironmentVariableMissing.
  • std.Io.Dir.rename returns error.DirNotEmpty rather than error.PathAlreadyExists for non-empty destination directories.

I/O as an Interface

The core 0.16 direction is to route new blocking, OS-facing, and nondeterministic APIs through an std.Io instance. Entropy, time, networking, process, and file operations generally follow this design, but existing stdlib APIs are not uniformly parameterized by std.Io; verify the installed signature before applying the rule mechanically.

Main patterns:

const std = @import("std");

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

Temporary adapter when no owner can pass io yet:

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

Testing:

const io = std.testing.io;

I/O implementations mentioned by the release notes:

  • std.Io.Threaded: feature-complete threaded implementation and closest behavior to 0.15 blocking APIs.
  • std.Io.Evented: experimental M:N/user-space stack switching implementation.
  • std.Io.Uring, std.Io.Kqueue, std.Io.Dispatch: early or platform-specific implementations.
  • std.Io.failing: no-operation/failing implementation for unsupported contexts and tests.

Future, Group, Cancelation, and Batch

New task-level and operation-level APIs live under std.Io.

Guidance:

  • io.async(...) creates a future for function-level task independence.
  • std.Io.Group manages many tasks and can await or cancel them together.
  • std.Io.Batch is lower-level and operation-oriented; use it when you need efficient independence among I/O operations rather than arbitrary Zig functions.
  • Propagate error.Canceled unless the code that requested cancelation is the code handling it.
  • If you handle error.Canceled locally and continue, use io.recancel() when the cancelation should remain active.
  • Prefer errdefer group.cancel(io) after spawning grouped work so task resources are released on all exits.

Synchronization Primitives

Synchronization that can block must migrate to std.Io equivalents so it cooperates with the chosen I/O backend.

Map:

  • 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
  • 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.once removed

Use cancelable locks when cancelation should be honored:

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

Use uncancelable locks only for short critical sections or cleanup paths:

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

Lock-free atomics do not need std.Io.

Entropy and Random

Entropy moved under std.Io.

Patterns:

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

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

Use io.randomSecure(...) when fresh OS-backed cryptographic entropy is required and failures should be reported.

Time

The old wall-clock/monotonic split is now routed through std.Io time types for clock operations that may depend on the runtime.

Migration requires an explicit clock choice. Use std.Io.Timestamp.now(io, .real) for wall-clock timestamps and .boot or .awake for elapsed-time measurement; Timestamp is not a one-for-one timer replacement.

Application preference:

  • Put common timestamp reads behind a shared application helper so callsites do not reinvent Io.Timestamp clock-selection or conversion rules.
  • Store io on timing systems that need repeated timestamps instead of constructing local fallback Io.Threaded instances.

File System

Most file system APIs moved from std.fs handles to std.Io.Dir and std.Io.File.

Basic patterns:

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 out = try std.Io.Dir.cwd().createFile(io, "out.txt", .{});
defer out.close(io);

var buf: [4096]u8 = undefined;
var writer = out.writer(io, &buf);
try writer.interface.print("value={d}\n", .{42});
try writer.interface.flush();

Convenience methods:

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

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

Common migrations:

  • file.close() -> file.close(io)
  • std.fs.cwd() -> std.Io.Dir.cwd()
  • std.fs.File.stdout() -> std.Io.File.stdout()
  • std.fs.Dir.readFileAlloc(...) -> std.Io.Dir.readFileAlloc(io, ..., .limited(max))
  • std.fs.File.readToEndAlloc(...) -> file reader + reader.interface.allocRemaining(...)
  • fs.copyFileAbsolute and other absolute helpers move to std.Io.Dir.*Absolute
  • Many Z and W path-specific helpers were removed; use the cross-platform []u8 path APIs.

Networking and HTTP

Networking moved under std.Io.net, and higher-level clients hold an io.

Patterns:

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

The release notes call out that DNS, parallel connection attempts, and cancelation now work through the chosen Io implementation. Do not bypass this with direct platform sockets in new generic code.

Process, Args, Env, and Preopens

Process APIs now use std.Io, and Juicy Main makes args/env/preopens non-global.

Patterns:

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const gpa = init.gpa;
    const args = try init.minimal.args.toSlice(init.arena.allocator());
    _ = args;
    _ = gpa;
    _ = io;
}

Run a child and capture output:

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);

Spawn a child:

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;

Environment guidance:

  • Prefer init.environ_map from std.process.Init at app boundaries.
  • error.EnvironmentVariableNotFound is now error.EnvironmentVariableMissing.
  • Current directory is now std.process.currentPath(io, buffer) or std.process.currentPathAlloc(io, allocator).
  • WASI preopens moved to std.process.Preopens and are available through Juicy Main.

File.MemoryMap

Memory-mapped file APIs moved under std.Io.File.MemoryMap. Treat maps as file-backed I/O resources and keep their lifetime explicit.

posix and os.windows Removals

Several low-level stdlib wrappers were removed as part of moving blocking/nondeterministic operations behind std.Io. Prefer the std.Io abstraction for portable code. Drop down to std.posix or std.os.windows only for explicit platform-specific code.

Allocators

heap.ArenaAllocator's allocator interface is thread-safe when its child allocator is thread-safe. The stdlib does not promise unconditional lock-free behavior.

heap.ThreadSafe allocator was removed. If shared allocation still needs protection, choose an allocator that is already thread-safe or place synchronization at the owner of the shared data.

Compression

Compression/decompression APIs continue moving toward std.Io.Reader and std.Io.Writer. LZMA, LZMA2, and XZ are specifically called out as updated. Deflate compression was added and decompression simplified.

Review rule:

  • Do not reintroduce old std.io.GenericReader adapter patterns for compressors.

Debug Information

Debug information was reworked and improved for several kinds of types. Expect better type names and better error-set runtime names, but also keep debug/unwind cost in mind for hot logging paths.

Inter-Process Progress on Windows

Progress reporting now has better Windows inter-process support. Prefer std build/progress APIs over custom ad hoc pipes for build step progress where practical.

Windows Networking and NtDll

Networking no longer requires ws2_32.dll in the same way old code did, and more Windows implementation moved toward NtDll. Prefer std Io.net when writing portable networking code.

mem cut/find APIs

The mem API emphasizes find* naming and adds cut* helpers.

Common names in 0.16:

  • std.mem.find
  • std.mem.findLast
  • std.mem.findScalar
  • std.mem.findScalarLast
  • std.mem.findAny
  • std.mem.findNone
  • std.mem.cut
  • std.mem.cutLast
  • std.mem.cutScalar
  • std.mem.cutPrefix
  • std.mem.cutSuffix

Avoid adding new indexOf / lastIndexOf style calls in 0.16 code.

Directory Walking and Paths

std.Io.Dir.walkSelectively was added for recursive walks where the walker decides which directories to enter.

std.fs.path behavior changed for Windows path handling:

  • UNC, rooted, and drive-relative paths are handled more consistently.
  • relative, relativeWindows, and relativePosix are now pure and require the current directory path and sometimes an environment map as input.

Pattern:

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

const relative = try std.fs.path.relative(gpa, cwd_path, init.environ_map, from, to);
defer gpa.free(relative);

File.Stat

Access time is optional:

const stat = try file.stat(io);
const atime = stat.atime orelse return error.FileAccessTimeUnavailable;

Timestamp-setting APIs also use structured options rather than positional atime/mtime values.

Atomic and Temporary Files

Atomic temporary file handling now uses std.Io.File.Atomic and the Io entropy path. Do not hand-roll random temp names when std already provides an atomic file helper.

Current Directory API

Use:

  • std.process.currentPath(io, buffer)
  • std.process.currentPathAlloc(io, allocator)

Avoid old std.process.getCwd* patterns.

Migration to Unmanaged Containers

0.16 continues the container migration toward allocator-free fields and allocator-at-call-site methods.

Important changes:

  • ArrayHashMap, AutoArrayHashMap, and StringArrayHashMap removed.
  • AutoArrayHashMapUnmanaged -> std.array_hash_map.Auto
  • StringArrayHashMapUnmanaged -> std.array_hash_map.String
  • ArrayHashMapUnmanaged -> std.array_hash_map.Custom
  • PriorityQueue and PriorityDequeue no longer store allocators.

PriorityQueue and PriorityDequeue

Priority containers now prefer .empty and push/pop terminology.

Common migrations:

  • init -> .empty or initContext
  • add -> push
  • addSlice -> pushSlice
  • addUnchecked -> pushUnchecked
  • remove / removeOrNull -> pop
  • removeIndex -> popIndex
  • PriorityDequeue min/max variants become popMin / popMax

Thread.Pool Removed

std.Thread.Pool is removed. Migrate simple independent jobs to std.Io.async or std.Io.Group.async when they are actually asynchronous from the caller.

If tasks synchronize with the caller or each other, re-evaluate the design rather than doing a mechanical replacement. Any blocking synchronization used by Io tasks must move from std.Thread.* primitives to std.Io.* primitives.

subsystem APIs

std.builtin.subsystem was removed. std.Target.SubSystem moved to std.zig.Subsystem with field-name updates, while deprecated aliases remain for some build-script compatibility.

Reader/Writer Removals

std.io is now std.Io.

Migration map:

  • std.Io.GenericReader -> std.Io.Reader
  • std.Io.AnyReader -> std.Io.Reader
  • std.io.fixedBufferStream(data).reader() -> var r: std.Io.Reader = .fixed(data)
  • std.io.fixedBufferStream(buffer).writer() -> var w: std.Io.Writer = .fixed(buffer)
  • std.leb.readUleb128 / readIleb128 -> std.Io.Reader.takeLeb128

Duration Formatting

The {D} duration format specifier was removed. Format std.Io.Duration with {f}.

try writer.print("{f}", .{std.Io.Duration{ .nanoseconds = ns }});

fs.getAppDataDir Removed

Application data directory policy is now application-owned. Use app-specific logic or a third-party package such as known-folders if desired.

Io.Writer.Allocating

std.Io.Writer.Allocating now stores an alignment field. Prefer the provided initializers rather than field literals unless the field set is intentional and complete.

Crypto

0.16 adds AES-SIV, AES-GCM-SIV, and the Ascon AEAD/hash constructions. Existing crypto code also needs the entropy migration described above.

Build System

Local Package Overrides

The build system can override packages locally. Use this for development overrides rather than editing dependency cache contents.

Project-Local Package Fetching

Packages can be fetched into a project-local directory. This is useful for reproducible workspaces and local depot workflows.

Unit Test Timeouts

zig build test --test-timeout <duration> can bound tests by real time. Use this for runaway tests, but remember scheduler load can make real-time limits flaky.

Error Formatting

New flags:

  • --error-style verbose|minimal|verbose_clear|minimal_clear
  • --multiline-errors indent|newline|none

--prominent-compile-errors was removed. Use --error-style minimal for the closest behavior.

Temporary Files

Build.makeTempPath and the RemoveDir step are gone. Use:

  • b.addTempFiles
  • b.addMutateFiles
  • b.tmpPath
  • std.Build.Step.WriteFile in temporary/mutate modes

Do not create temporary directories during the configure phase and then mutate them during make.

Compiler

C Translation

Translate-c is now based on Aro/translate-c rather than libclang. The change is intended to be non-breaking, but generated code can differ.

For ABI-sensitive libraries:

  • Regenerate translated C bindings deliberately.
  • Compare struct layouts, enum values, constants, calling conventions, and macro translation.
  • Keep compile-time asserts next to translated bindings.

LLVM Backend

The LLVM backend has experimental incremental compilation support, smaller bitcode output, some compile-time improvements, and better debug info for several type cases.

Type Resolution

Compiler type resolution changed significantly. See the language section above for source-level effects.

Incremental Compilation

Incremental compilation is more usable but still disabled by default. Try zig build -fincremental --watch for local iteration, but do not treat it as a required CI mode yet.

x86 and Other Backends

The x86 backend remains the default for Debug mode and has faster compile times than LLVM, with lower machine-code quality. Other self-hosted backends continue to mature.

Linker

The new ELF linker is available with -fnew-linker or build-script options and is default when using incremental compilation for ELF. It is faster for incremental relinks but not feature-complete, especially around debug information.

Use it for iteration when it works; validate release/QA builds on the intended linker path.

Fuzzer

Fuzz tests now use *std.testing.Smith instead of a raw []const u8 input. Smith generates structured values, bytes, slices, and weighted choices.

Migration rule:

  • Update fuzz entry points to accept *std.testing.Smith.
  • Generate inputs through Smith rather than manually slicing the old byte stream.

Toolchain

0.16 updates major toolchain components including LLVM 21, musl 1.2.5, glibc 2.43, Linux 6.19 headers, macOS 26.4 headers, MinGW-w64, FreeBSD 15.0 libc, WASI libc, and zig libc/zig cc updates.

Practical effects:

  • Cross-compilation behavior can change even when Zig code did not.
  • Re-check C ABI integration and platform feature detection after upgrading.
  • Treat changed C translation output as worthy of investigation, especially for ABI-sensitive or platform bindings.

Application Upgrade Checklist

When upgrading an application to Zig 0.16:

  1. Prefer pub fn main(init: std.process.Init) !void for applications and tools.
  2. Pass init.io down through setup rather than constructing local Io.Threaded instances.
  3. Use std.testing.io in tests.
  4. Add io: std.Io fields to long-lived systems, allocators, registries, timers, and queues that must perform blocking work later.
  5. Use std.Io.Mutex/Condition/Semaphore when the lock can block. Use lockUncancelable(io) only for short critical sections where cancellation must not interrupt cleanup or queue integrity.
  6. Migrate file operations to std.Io.Dir/std.Io.File.
  7. Keep platform-specific DynamicLib behavior behind a local abstraction on Windows.
  8. Preserve translated-C ABI asserts and expand them when translation output changes.
  9. Replace old std.crypto.random usage with io.random or std.Random.IoSource.
  10. Replace old process helpers with std.process.run(gpa, io, ...) or std.process.spawn(io, ...).
  11. Replace old std.Thread.Pool assumptions with an application scheduler or carefully designed std.Io.Group usage.
  12. Keep migration notes updated when the codebase adopts application-specific policy decisions.