Coming from an Older Zig
This guide targets Zig master: the version in the footer is the compiler that actually compiled and ran every snippet here. If you are on a tagged release, some of this code will not build for you.
Rather than maintaining a parallel guide per release, this page gives you the delta: what changed, what it looks like now, and which chapter covers it. That is usually what you actually need.
At a glance
| What | Before | Now |
|---|---|---|
main signature | pub fn main() !void | pub fn main(init: std.process.Init) !void |
| Writers | unbuffered, no flush | buffered, must flush |
| File API | std.fs.File | std.Io.File, and takes an Io |
| Array repeat | "-" ** 5 | @splat (** is gone) |
ArrayList | init(allocator) | .empty, allocator per call |
| Mutex | std.Thread.Mutex | std.Io.Mutex, lock(io) |
Custom format | four parameters, used by {} | one writer parameter, used by {f} |
| Enum reflection | info.fields[i].name | info.field_names[i] |
addExecutable | source file directly | root_module |
main takes an argument
The one that breaks the very first program you write.
// before
pub fn main() !void {
std.debug.print("Hello\n", .{});
}
// now
pub fn main(init: std.process.Init) !void {
try std.Io.File.stdout().writeStreamingAll(init.io, "Hello\n");
}
init.io is an Io instance: the same idea as Allocator, applied to
anything that can block. There is no global stdout; you must be handed the
capability to write.
Writers are buffered (“writergate”, 0.15)
// before: appeared immediately
const stdout = std.io.getStdOut().writer();
try stdout.print("{d}\n", .{42});
// now: nothing is written until you flush
var buf: [1024]u8 = undefined;
var file_writer = std.Io.File.stdout().writerStreaming(io, &buf);
const out = &file_writer.interface;
try out.print("{d}\n", .{42});
try out.flush(); // ← forget this and you get no output at all
The buffer belongs to the interface, which is why you supply it: no hidden
allocation. The missing flush is the single most common symptom of following
an older tutorial.
Filesystem moved onto Io (0.16)
// before
var file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();
// now
var dir = std.Io.Dir.cwd();
const file = try dir.openFile(io, "data.txt", .{});
defer file.close(io);
Uniform rule: anything that touches the disk takes the io instance.
** no longer exists
// before
const dashes = "-" ** 5;
// now
const dashes: [5]u8 = @splat('-');
Worth calling out because the failure is actively misleading: ** no longer
tokenises at all, so the compiler reports “binary operator * has whitespace
on one side, but not the other”, which tells you nothing about what actually
happened. ++ concatenation is unaffected.
→ Arrays
ArrayList and hash maps are unmanaged
// before: the list stored your allocator
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.append('a');
// now: you pass it to anything that can allocate
var list: std.ArrayList(u8) = .empty;
defer list.deinit(gpa);
try list.append(gpa, 'a');
More typing, but the allocator is visible at every call site that can fail. The
same change applies to StringHashMapUnmanaged and AutoHashMapUnmanaged.
Synchronisation moved to Io
// before
var mutex: std.Thread.Mutex = .{};
mutex.lock();
defer mutex.unlock();
// now
var mutex: std.Io.Mutex = .init;
try mutex.lock(io);
defer mutex.unlock(io);
lock can fail only because it can be cancelled. std.Thread.spawn still
exists, but structured concurrency via io.async and Io.Group is the better
default, and works on targets with no threads at all.
→ Threads · Concurrency
Custom format methods
// before
pub fn format(
self: Point,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void { ... }
// now
pub fn format(self: Point, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("({d}, {d})", .{ self.x, self.y });
}
And it is no longer invoked by {}; you must ask for it with {f}.
{t} prints an enum tag name or error name.
Enum reflection
// before
const fields = @typeInfo(Direction).@"enum".fields;
fields[2].name;
// now: parallel arrays
const info = @typeInfo(Direction).@"enum";
info.field_names[2];
info.field_values[2];
This one is recent: it landed between 0.17.0-dev.644 and dev.1441, and was
the only break across all 53 snippets in this guide when the compiler was
updated across roughly 800 commits.
→ Enums
build.zig
// before
const exe = b.addExecutable(.{
.name = "app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// now: build the module first
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
If you are staying on a release
Nothing here is a reason to upgrade in a hurry. A tagged release is the right choice for most work; master is what this guide tracks because a guide that lags is a guide that quietly lies.
Use this page as a translation layer: read the chapter for the concept, then
map the syntax back. The ideas (explicit allocators, errors as values,
exhaustive switches, defer, comptime) have not changed at all, and those are
the parts worth your attention.