zig-skills/references/std-io.md

7.7 KiB

std.Io - I/O API Reference (Zig 0.16.0)

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

Zig 0.16 makes I/O an explicit interface. All file, network, process, time, entropy, cancelable synchronization, and task APIs that can block or interact with the outside world need a std.Io.

Ownership Rule

Prefer this flow:

  1. Application entry receives init: std.process.Init.
  2. It extracts const io = init.io.
  3. It passes io through setup.
  4. Long-lived systems that need I/O store io.
  5. Tests use std.testing.io.
pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;
    _ = .{ gpa, io };
}

Temporary adapter only at a boundary:

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

Implementations

  • std.Io.Threaded: threaded, feature-complete, closest to old blocking behavior.
  • std.Io.Evented: experimental evented/M:N implementation.
  • std.Io.Uring, std.Io.Kqueue, std.Io.Dispatch: platform/proof-of-concept backends.
  • std.Io.failing: backend that supports no operations.

Writer

std.Io.Writer is non-generic. It contains its buffer and vtable.

Fixed Buffer Writer

var buffer: [256]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buffer);

try writer.print("name={s} value={d}", .{ "answer", 42 });
const bytes = writer.buffered();

File Writer

const file = try std.Io.Dir.cwd().createFile(io, "out.txt", .{});
defer file.close(io);

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

try writer.writeAll("hello\n");
try writer.print("value={d}\n", .{42});
try writer.flush();

flush is still required for buffered file/socket writers.

Standard Output

var buffer: [4096]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(io, &buffer);
const stdout = &stdout_writer.interface;

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

Allocating Writer

var aw: std.Io.Writer.Allocating = .init(allocator);
defer aw.deinit();

try aw.writer.print("hello {s}", .{"world"});
const owned = try aw.toOwnedSlice();
defer allocator.free(owned);

Allocating has an alignment field in 0.16. Prefer .init, .initAligned, or .initCapacity.

Discarding Writer

Use this when old code used counting/discarding writer patterns.

var buffer: [256]u8 = undefined;
var discarding: std.Io.Writer.Discarding = .init(&buffer);
try discarding.writer.print("ignored {d}", .{123});
const count = discarding.fullCount();

Reader

std.Io.Reader is non-generic and buffer-aware.

Fixed Reader

var reader: std.Io.Reader = .fixed("alpha\nbeta\n");

while (try reader.takeDelimiter('\n')) |line| {
    _ = line;
}

takeDelimiter returns !?[]u8: null means EOF with no buffered bytes remaining. A final unterminated line is returned as data before a later call yields null.

File Reader

const file = try std.Io.Dir.cwd().openFile(io, "data.txt", .{});
defer file.close(io);

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

while (try reader.takeDelimiter('\n')) |line| {
    _ = line;
}

Read Remaining

const bytes = try reader.allocRemaining(allocator, .limited(1024 * 1024));
defer allocator.free(bytes);

Binary Reads

const value = try reader.takeInt(u32, .little);
const header = try reader.takeStruct(Header, .little);
const leb = try reader.takeLeb128(u64);

takeStruct reads an extern/packed memory representation through the reader buffer; use a layout with a defined byte representation, ensure the reader can supply the full size, and do not treat a native-layout struct as a portable wire format.

File Integration

Use std.Io.Dir and std.Io.File.

const cwd = std.Io.Dir.cwd();

const contents = try cwd.readFileAlloc(io, "data.txt", allocator, .limited(1024 * 1024));
defer allocator.free(contents);

try cwd.writeFile(io, .{
    .sub_path = "out.txt",
    .data = contents,
});

close, stat, setTimestamps, readers, writers, and most operations now take io.

Networking Integration

Use std.Io.net for sockets/streams and pass io to high-level clients.

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

Process Integration

Use std.process.run and std.process.spawn.

const result = try std.process.run(allocator, io, .{
    .argv = &.{ "zig", "version" },
    .stdout_limit = .limited(16 * 1024),
    .stderr_limit = .limited(16 * 1024),
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);

Entropy

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

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

Use io.randomSecure for fresh secure entropy with error reporting.

Time

Timestamp reads now require an explicit clock choice:

const now = std.Io.Timestamp.now(io, .real);
const elapsed_mark = std.Io.Timestamp.now(io, .awake);

Timestamp is not a one-for-one replacement for every old Instant/Timer behavior; choose .real versus an appropriate monotonic clock such as .awake, and build elapsed-time helpers around that choice.

Use a shared application helper when common timestamp reads require consistent clock selection or conversion semantics.

Tasks and Cancelation

std.Io provides function-level and operation-level concurrency:

  • io.async(...)
  • std.Io.Group
  • std.Io.Select(U) where U is the tagged union of possible results
  • std.Io.Batch
  • std.Io.Queue(Elem), initialized with caller-provided typed element storage

Cancelation guidance:

  • Propagate error.Canceled by default.
  • Only the code that requested cancelation should ignore it.
  • If handling it locally but continuing, use io.recancel() when cancelation should stay active.
  • Use errdefer group.cancel(io) after creating grouped tasks.
var group: std.Io.Group = .init;
errdefer group.cancel(io);

group.async(io, worker, .{ io, arg });
try group.await(io);

Sync Primitives

Blocking sync moved to std.Io equivalents:

Old New
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.Thread.ResetEvent std.Io.Event
std.Thread.WaitGroup Conceptually std.Io.Group; submit tasks, then await or cancel the group
std.Thread.Futex io.futexWait* / io.futexWake; waits have cancelable and uncancelable forms
try mutex.lock(io);
defer mutex.unlock(io);
mutex.lockUncancelable(io);
defer mutex.unlock(io);

Removed Reader/Writer Names

Use these replacements:

Old New
std.io std.Io
std.Io.GenericReader std.Io.Reader
std.Io.AnyReader std.Io.Reader
std.Io.GenericWriter std.Io.Writer
std.Io.AnyWriter std.Io.Writer
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

Review Checklist

  • Does this API need an io: std.Io parameter?
  • Is io obtained at the application/test boundary rather than constructed deep inside?
  • Are all file/socket/process/time/random operations routed through std.Io?
  • Are blocking locks using std.Io primitives?
  • Are buffered writers flushed?
  • Are cancelation errors propagated unless this code requested cancelation?
  • Are temporary Io.Threaded.init_single_threaded adapters isolated and documented?