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:
- Application entry receives
init: std.process.Init. - It extracts
const io = init.io. - It passes
iothrough setup. - Long-lived systems that need I/O store
io. - 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.Groupstd.Io.Select(U)whereUis the tagged union of possible resultsstd.Io.Batchstd.Io.Queue(Elem), initialized with caller-provided typed element storage
Cancelation guidance:
- Propagate
error.Canceledby 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.Ioparameter? - Is
ioobtained 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.Ioprimitives? - Are buffered writers flushed?
- Are cancelation errors propagated unless this code requested cancelation?
- Are temporary
Io.Threaded.init_single_threadedadapters isolated and documented?