Skip to content

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 CPython asyncio event loop built on zio.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, if zio.Runtime's isn't the one you want.
  • zio.Runtime: the full runtime. It schedules zio.coro coroutines across executor threads, drives their I/O through zio.ev, and adds structured concurrency (task groups), cancellation, synchronization primitives, and the std.Io implementation. 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_uring with automatic epoll fallback), 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, loongarch64 and powerpc64 architectures
  • 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 getaddrinfo in 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.log and std.debug.print via custom debug_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.