about things notes.zzstoatzz.io
notes
README.md

std.Io #

0.16 overhauls I/O with the std.Io interface. everything that can block (filesystem, networking, timers, concurrency) moves through this interface.

sources #

the interface #

like Allocator for memory, Io is passed to functions that do I/O:

fn fetchData(io: std.Io, allocator: Allocator) ![]u8 {
    // io provides networking, timers, concurrency, etc.
}

this decouples code from execution model. from the devlog (Oct 2025):

Regardless of whether Io is implemented via threads, or via an event loop, this code behaves optimally. The code also works when using single-threaded, blocking Io even though the operations happen sequentially.

same code, three execution models. write once, swap backend at init.

what Io provides #

  • file system, networking, processes
  • time and sleeping
  • randomness
  • async, await, concurrent, and cancel
  • concurrent queues (Io.Queue)
  • wait groups and select (Io.Group, Io.Select)
  • mutexes, futexes, events, and conditions (Io.Mutex, Io.Condition, Io.Event)
  • memory mapped files

timers and retry loops #

Retry loops should sleep through the same std.Io value that performed the network request:

try io.sleep(std.Io.Duration.fromMilliseconds(delay_ms), .awake);

This makes the delay cancellation-aware and keeps the code backend-agnostic. Use .awake for monotonic elapsed retry delays; use .real only when the deadline is tied to wall-clock time. If a protocol exposes an absolute Unix timestamp, convert it deliberately instead of mixing wall-clock and elapsed duration logic.

sleep returns error.Canceled. catch {} on it turns a cancellation into an immediate re-send; propagate it with try so the loop unwinds instead.

std.Io also provides randomness. Retry jitter can use std.Random.IoSource so the policy stays on the caller's I/O backend instead of reaching for a separate global source:

const source: std.Random.IoSource = .{ .io = io };
const delay = half + source.interface().uintAtMost(u32, half); // equal jitter

A Retry-After header is seconds from now, so compare it on the wall clock: Io.Clock.real.now(io).toSeconds().

backends #

  • Io.Threaded — thread-based, always available, production default
  • Io.Evented — fiber-based, experimental:
    • linux: Io.Uring (io_uring)
    • macOS/iOS: Io.Dispatch (GCD)
    • BSD: Io.Kqueue
    • unsupported platforms: void
    • uses userspace stack switching (fibers/green threads)
    • currently experimental — known bugs (see patterns.md)
  • WASM: fiber-based backends can't work (no stack switching). stackless coroutines planned as future compiler feature.

backend selection:

const Backend = if (Io.Evented != void) Io.Evented else Io.Threaded;

init differs by backend:

// Threaded
backend = Io.Threaded.init(allocator, .{});

// Evented
try Backend.init(&backend, allocator, .{});

both expose .io() → std.Io. all downstream code uses the same Io value.

no function coloring (mostly) #

async and await are library functions, not keywords. no viral async/await infection.

fn foo(io: std.Io) !void { ... }

// called normally
try foo(io);

// or asynchronously
const future = io.async(foo, .{io});

caveat: coloring shifted from keyword-based to parameter-based. if a pure computation function later needs I/O, it must accept an Io parameter, which propagates to callers. the advantage: code stays agnostic to the execution model.

files in this folder #

  • concurrency.md — async vs concurrent, Future, Group, Select, Queue. read before building any timeout/watchdog on io.async: it degrades to an inline call past async_limit (= cpus − 1, so always inline on a 1-vCPU box), which silently disables deadlines built by racing a future.
  • synchronization.md — Mutex, Condition, CancelProtection, cancellation model
  • patterns.md — backend selection, InitOptions, debug_io, long-lived tasks, timedWait workarounds
  • fibers.md — building stackful fibers: the ReleaseSafe clobber bugs, guard-page stacks, keeping blocking work off fibers