Skip to content

Zig 0.16 I/O Changes

Zig 0.16 changes how I/O code is written in the standard library.

Zig 0.16 changes how I/O code is written in the standard library.

Older Zig examples often use this form:

const stdout = std.io.getStdOut();

try stdout.writer().print(
    "hello, {s}\n",
    .{"zig"},
);

In Zig 0.16, much of this code moves through the newer std.Io API. The names are shorter, and the model is more explicit about readers, writers, and buffers.

A simple program still has the same purpose:

const std = @import("std");

pub fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("hello, {s}\n", .{"zig"});
    try stdout.flush();
}

The program creates a buffer:

var stdout_buffer: [1024]u8 = undefined;

It then creates a writer for standard output:

var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);

The writer has an interface:

const stdout = &stdout_writer.interface;

The call to print writes formatted bytes into the writer:

try stdout.print("hello, {s}\n", .{"zig"});

The final call is required:

try stdout.flush();

A buffered writer may hold bytes in memory. flush sends them to the operating system.

Input follows the same shape.

const std = @import("std");

pub fn main() !void {
    var stdin_buffer: [1024]u8 = undefined;
    var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer);
    const stdin = &stdin_reader.interface;

    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    var buffer: [256]u8 = undefined;

    while (true) {
        const n = try stdin.readSliceShort(&buffer);
        if (n == 0) break;

        try stdout.writeAll(buffer[0..n]);
    }

    try stdout.flush();
}

This is still the same filter program. It reads from standard input and writes to standard output.

The main difference is that the buffering is now visible in the program. You decide where the buffers live, how large they are, and when output is flushed.

This is a good fit for Zig’s style. Allocation, buffering, and failure are not hidden.

A file writer has the same shape:

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().createFile("out.txt", .{});
    defer file.close();

    var buffer: [4096]u8 = undefined;
    var writer = file.writer(&buffer);
    const out = &writer.interface;

    try out.print("x = {d}\n", .{42});
    try out.flush();
}

A file reader is similar:

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("out.txt", .{});
    defer file.close();

    var buffer: [4096]u8 = undefined;
    var reader = file.reader(&buffer);
    const in = &reader.interface;

    var data: [128]u8 = undefined;
    const n = try in.readSliceShort(&data);

    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.writeAll(data[0..n]);
    try stdout.flush();
}

When reading old Zig code, the translation is usually mechanical:

Older styleZig 0.16 style
std.io.getStdOut()std.fs.File.stdout()
file.writer()file.writer(&buffer)
file.reader()file.reader(&buffer)
writer.print(...)writer.interface.print(...)
implicit buffer helpersexplicit buffers
sometimes no flush visibleexplicit flush() for buffered output

The larger rule is simple: treat I/O as an interface over bytes, backed by explicit storage.

Exercise 13-25. Rewrite an older std.io.getStdOut().writer().print example in Zig 0.16 style.

Exercise 13-26. Write a program that reads standard input and writes it to standard output using std.fs.File.stdin() and std.fs.File.stdout().

Exercise 13-27. Change the output buffer size and confirm that the program still works.

Exercise 13-28. Write to a file and omit flush. Then add flush and compare the result.