ZIO - Async I/O framework for Zig
ZIO is an asynchronous runtime for Zig, in the same spirit as Go's runtime or Tokio: it schedules lightweight coroutines onto a pool of OS threads, and gives you blocking-looking network, file, and process I/O that's actually backed by non-blocking, event-driven OS APIs under the hood. On top of that, it's a full implementation of the standard library's std.Io interface, so any Zig 0.16+ code written against std.Io runs on zio unmodified.
Architecture
zio is built in layers, and each layer is usable on its own:
zio.ev: a cross-platform, callback-based event loop (io_uring/epoll/kqueue/iocp/poll), in the same space as libuv or libxev. You can use this independently for async I/O without coroutines, or to embed zio's I/O into an existing callback-driven loop. For example, see blazio, a CPythonasyncioevent loop built onzio.ev.zio.coro: stackful coroutine primitives (context switching, growable stacks, manual scheduling), with no I/O or scheduler attached. This is what you'd build a different kind of scheduler on top of, ifzio.Runtime's isn't the one you want.zio.Runtime: the full runtime. It scheduleszio.corocoroutines across executor threads, drives their I/O throughzio.ev, and adds structured concurrency (task groups), cancellation, synchronization primitives, and thestd.Ioimplementation. Most programs use this directly and never touch the layers below it.
A runtime can run single-threaded, or multi-threaded in one of two modes. With work-stealing (the default), idle executors steal work from busy ones. This keeps every thread busy when runnable work is spread unevenly to begin with. With pinned scheduling, a task stays on whichever executor it was spawned on for its entire life, with no migration and no cross-executor synchronization on the scheduling path, at the cost of no rebalancing if load is uneven.
Features
- Support for Linux (
io_uringwith automaticepollfallback), Windows (iocp), macOS/FreeBSD/NetBSD/OpenBSD (kqueue), and many other systems (poll) - User-mode coroutine context switching for
x86_64,x86,aarch64,arm,thumb,riscv32,riscv64,loongarch64andpowerpc64architectures - Growable stacks for the coroutines implemented by auto-extending virtual memory reservations
- Single-threaded or multi-threaded coroutine scheduler, with or without work-stealing
- Fully asynchronous network I/O on all systems. Supports TCP, UDP, Unix sockets, raw IP sockets, etc.
- Fully asynchronous file I/O on Linux, partially asynchronous (read/write) on Windows. Using blocking syscalls in a thread pool on other systems.
- Fully asynchronous DNS resolver on Linux, Windows and macOS. Using
getaddrinfoin a thread pool on other systems. - Synchronization primitives, including more advanced ones, like channels
- Fast and safe cancellation support for all operations
- Full timeout support, both for individual I/O operations and for arbitrary user code, with proper cleanup on either a timeout or an external cancel
- Structured concurrency using task groups
- Waiting on a mix of different operations at once, tasks, channels, timeouts, and anything else implementing the wait protocol
- Integration with
std.logandstd.debug.printvia customdebug_io, so logging and printing don't block the event loop
Ecosystem
The following libraries use ZIO for networking and concurrency:
Quick Example
Basic TCP echo server:
const std = @import("std");
const zio = @import("zio");
pub const std_options_debug_io = zio.debug_io;
fn handleClient(stream: zio.net.Stream) !void {
defer stream.close();
defer stream.shutdown(.both) catch |err| {
std.log.err("Failed to shutdown client connection: {}", .{err});
};
std.log.info("Client connected from {f}", .{stream.socket.address});
var read_buffer: [1024]u8 = undefined;
var reader = stream.reader(&read_buffer);
var write_buffer: [1024]u8 = undefined;
var writer = stream.writer(&write_buffer);
while (true) {
// Read a line from the client
const line = reader.interface.takeDelimiterInclusive('\n') catch |err| switch (err) {
error.EndOfStream => break,
error.ReadFailed => |e| return reader.err orelse e,
else => |e| return e,
};
std.log.info("Received: {s}", .{line});
// Delay the response a little bit
try zio.sleep(.fromMilliseconds(1000));
// Echo the line back
try writer.interface.writeAll(line);
try writer.interface.flush();
}
std.log.info("Client disconnected", .{});
}
pub fn main(init: std.process.Init) !void {
const rt = try zio.Runtime.init(init.gpa, .{});
defer rt.deinit();
const addr = try zio.net.IpAddress.parseIp4("127.0.0.1", 8080);
const server = try addr.listen(.{});
defer server.close();
std.log.info("TCP echo server listening on {f}", .{server.socket.address});
std.log.info("Press Ctrl+C to stop the server", .{});
var group: zio.Group = .init;
defer group.cancel();
while (true) {
const stream = try server.accept(.{});
errdefer stream.close();
try group.spawn(handleClient, .{stream});
}
}
See the Tutorial to get started, or check out the examples in the repository.
Installation
See the Getting Started guide for installation instructions.
License
This project is licensed under the MIT license.