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
iofrom Juicy Main (std.process.Init) when possible. - Tests should prefer
std.testing.io. - A local
std.Io.Threaded.init_single_threadedis acceptable only as a temporary adapter at a boundary that cannot yet receive anio.
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
@enumFromIntare 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:
- Put includes in a real header.
- Add a build-system
b.addTranslateC(...)step. - 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
@typeInfofor 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@truncperform result-typed floating operations. Use@intFromFloatfor 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:
SegmentedListremoved.std.meta.declListremoved.std.Io.GenericWriter,std.Io.AnyWriter,std.Io.null_writer, andstd.Io.CountingReaderremoved.std.Thread.Mutex.Recursiveremoved.std.fmt.formatis replaced bystd.Io.Writer.print.std.fmt.Formatteris renamed tostd.fmt.Alt.std.fmt.FormatOptionsis renamed tostd.fmt.Options.std.fmt.bufPrintZremains as a deprecated zero-sentinel wrapper; usestd.fmt.bufPrintSentinelfor new code.std.DynLibremoved Windows support; use platform APIs (LoadLibraryExW,GetProcAddress) directly or through a local abstraction.BitSetandEnumSetuse decl literals instead ofinitEmpty/initFull.
Important error changes:
error.RenameAcrossMountPointsanderror.NotSameFileSystembecomeerror.CrossDevice.error.SharingViolationbecomeserror.FileBusy.error.EnvironmentVariableNotFoundbecomeserror.EnvironmentVariableMissing.std.Io.Dir.renamereturnserror.DirNotEmptyrather thanerror.PathAlreadyExistsfor 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.Groupmanages many tasks and can await or cancel them together.std.Io.Batchis lower-level and operation-oriented; use it when you need efficient independence among I/O operations rather than arbitrary Zig functions.- Propagate
error.Canceledunless the code that requested cancelation is the code handling it. - If you handle
error.Canceledlocally and continue, useio.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.Eventstd.Thread.WaitGroup->std.Io.Groupstd.Thread.Futexoperations ->io.futexWait,io.futexWaitTimeout,io.futexWaitUncancelable, andio.futexWakestd.Thread.Mutex->std.Io.Mutexstd.Thread.Condition->std.Io.Conditionstd.Thread.Semaphore->std.Io.Semaphorestd.Thread.RwLock->std.Io.RwLockstd.onceremoved
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.Timestampclock-selection or conversion rules. - Store
ioon timing systems that need repeated timestamps instead of constructing local fallbackIo.Threadedinstances.
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.copyFileAbsoluteand other absolute helpers move tostd.Io.Dir.*Absolute- Many
ZandWpath-specific helpers were removed; use the cross-platform[]u8path 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_mapfromstd.process.Initat app boundaries. error.EnvironmentVariableNotFoundis nowerror.EnvironmentVariableMissing.- Current directory is now
std.process.currentPath(io, buffer)orstd.process.currentPathAlloc(io, allocator). - WASI preopens moved to
std.process.Preopensand 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.GenericReaderadapter 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.findstd.mem.findLaststd.mem.findScalarstd.mem.findScalarLaststd.mem.findAnystd.mem.findNonestd.mem.cutstd.mem.cutLaststd.mem.cutScalarstd.mem.cutPrefixstd.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, andrelativePosixare 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, andStringArrayHashMapremoved.AutoArrayHashMapUnmanaged->std.array_hash_map.AutoStringArrayHashMapUnmanaged->std.array_hash_map.StringArrayHashMapUnmanaged->std.array_hash_map.CustomPriorityQueueandPriorityDequeueno longer store allocators.
PriorityQueue and PriorityDequeue
Priority containers now prefer .empty and push/pop terminology.
Common migrations:
init->.emptyorinitContextadd->pushaddSlice->pushSliceaddUnchecked->pushUncheckedremove/removeOrNull->popremoveIndex->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.Readerstd.Io.AnyReader->std.Io.Readerstd.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.addTempFilesb.addMutateFilesb.tmpPathstd.Build.Step.WriteFilein 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:
- Prefer
pub fn main(init: std.process.Init) !voidfor applications and tools. - Pass
init.iodown through setup rather than constructing localIo.Threadedinstances. - Use
std.testing.ioin tests. - Add
io: std.Iofields to long-lived systems, allocators, registries, timers, and queues that must perform blocking work later. - Use
std.Io.Mutex/Condition/Semaphorewhen the lock can block. UselockUncancelable(io)only for short critical sections where cancellation must not interrupt cleanup or queue integrity. - Migrate file operations to
std.Io.Dir/std.Io.File. - Keep platform-specific DynamicLib behavior behind a local abstraction on Windows.
- Preserve translated-C ABI asserts and expand them when translation output changes.
- Replace old
std.crypto.randomusage withio.randomorstd.Random.IoSource. - Replace old process helpers with
std.process.run(gpa, io, ...)orstd.process.spawn(io, ...). - Replace old
std.Thread.Poolassumptions with an application scheduler or carefully designedstd.Io.Groupusage. - Keep migration notes updated when the codebase adopts application-specific policy decisions.