Skip to content

Instantly share code, notes, and snippets.

@alogic0
Created July 13, 2026 16:31
Show Gist options
  • Select an option

  • Save alogic0/abfee3e29515a3c7b57eca2475f2c0f1 to your computer and use it in GitHub Desktop.

Select an option

Save alogic0/abfee3e29515a3c7b57eca2475f2c0f1 to your computer and use it in GitHub Desktop.
std.Io.Writer explanation

The "Writergate" overhaul in Zig 0.15/0.16 completely re-architected how the standard library handles I/O. In Zig 0.16, the old std.io.Writer (which relied heavily on duck-typing with anytype) has been removed and replaced by the strict, explicit interface type under std.Io.Writer.

This change mirrors Zig's Allocator model. To perform any I/O, you now pass an explicit io: std.Io context down through your functions.


1. Core Architecture: What Is It?

Instead of using duck-typing (anytype), std.Io.Writer is a concrete type that acts as an interface using a Virtual Method Table (vtable).

Crucially, buffering is now baked directly into the Writer interface rather than being a separate wrapper like the old std.io.BufferedWriter. A Writer owns an internal buffer, processing small writes locally in the hot path. When that buffer fills up, it flushes via its vtable.

The heart of the new interface is the drain function on the vtable:

fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize

Instead of a simple byte-by-byte or single-slice write, it accepts a slice of slices ([]const []const u8) to naturally support vectored / scatter-gather I/O at the operating system level.


2. The .interface Pattern

When you initialize a writer from a concrete source (like a File), you don't get a std.Io.Writer directly. Instead, you get a concrete implementation type (e.g., std.Io.File.Writer), which contains an internal .interface field of type std.Io.Writer.

When a function expects a generic writer, you pass a pointer to that .interface field.

Hello World Example in Zig 0.16

Here is how you write to stdout using a formatted print with the new buffered Writer interface:

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    // 1. All I/O requires an 'io' context from the environment
    const io = init.io; 

    // 2. Allocate a userspace buffer for the writer
    var buffer: [4096]u8 = undefined;
    
    // 3. Initialize the concrete file writer bound to our io instance and buffer
    var stdout_impl = std.Io.File.stdout().writer(io, &buffer);
    
    // 4. Extract a pointer to the generic std.Io.Writer interface
    const stdout = &stdout_impl.interface;

    // 5. Use standard writer methods like print
    try stdout.print("Hello, {s}!\n", .{"Zig 0.16"});
    
    // 6. Explicitly flush remaining bytes when finished
    try stdout.flush();
}

3. Key Differences: Old vs. New

Feature Old Way (Pre-0.15/0.16) New Way (0.16)
Type Signature Handled implicitly via anytype Explicitly typed as *std.Io.Writer
I/O Context Implicit global state (std.io.getStdOut()) Explicitly passed via init.io context
Buffering Composited via std.io.BufferedWriter Baked directly into the Writer itself
String Formatting std.fmt.format(writer, ...) Methods live directly on the Writer (e.g., writer.print)
In-Memory Writing std.io.fixedBufferStream std.Io.Writer.fixed(buf)

4. In-Memory Slices

If you just want to format string data into a stack or heap allocation without touching a file or network descriptor, std.io.fixedBufferStream is gone. Instead, use the built-in fixed-buffer utility on the Writer:

var buf: [256]u8 = undefined;
var f_writer = std.Io.Writer.fixed(&buf);
const writer = &f_writer.interface;

try writer.print("Status code: {d}", .{200});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment