This is the compact reference version of the project migration plan.
The important correction from earlier drafts is simple:
For TurboAPI-owned code, do not solve Zig 0.16 by introducing pthread shims. Move runtime code onto native
std.Ioonce, then keep the backend swappable.
The local Zig 0.16 stdlib already gives us the abstraction we want:
std.Io.Threadedfor the blocking runtime we want todaystd.Io.Eventedfor laterUring/Dispatch/Kqueuebackendsstd.Io.Mutex,Condition, andRwLockthat already follow the chosenIostd.Io.netserver and stream types that are already the new networking surface
That includes:
- sockets and accept loops
- lock types
- condition variables
- rwlocks
- randomness
- crypto call sites that now require
std.Io
That includes:
cwdfilesystem helpers- timestamp helpers
- an isolated blocking sleep helper for legacy code that does not already accept
io
That would preserve the wrong abstraction and force another migration later.
Many Build.Step.Compile helpers moved to root_module:
// 0.15
lib.addIncludePath(.{ .cwd_relative = path });
lib.linkSystemLibrary("foo");
lib.addCSourceFile(.{ .file = b.path("shim.c"), .flags = &.{} });
// 0.16
lib.root_module.addIncludePath(.{ .cwd_relative = path });
lib.root_module.linkSystemLibrary("foo", .{});
lib.root_module.addCSourceFile(.{ .file = b.path("shim.c"), .flags = &.{} });If the compiler mentions Build.Step.Compile and addIncludePath, move the call to
root_module rather than trying to outsmart the pointer-deref note.
Pick one application Io at startup and keep it in a runtime module:
const std = @import("std");
pub var threaded: std.Io.Threaded = undefined;
pub var io: std.Io = undefined;
pub fn init(gpa: std.mem.Allocator) void {
threaded = std.Io.Threaded.init(gpa, .{});
io = threaded.io();
}
pub fn deinit() void {
threaded.deinit();
}std.Io.Threaded.global_single_threaded exists, but it is not the right default for a long-lived
server runtime. Own the Io explicitly.
// 0.15
const addr = try std.net.Address.parseIp4(host, port);
var server = try addr.listen(.{ .reuse_address = true });
const conn = try server.accept();
defer conn.stream.close();
// 0.16
const addr = try std.Io.net.IpAddress.parse(host, port);
var server = try addr.listen(runtime.io, .{ .reuse_address = true });
const stream = try server.accept(runtime.io);
defer stream.close(runtime.io);// 0.15
stream.handle
// 0.16
stream.socket.handlevar read_buf: [4096]u8 = undefined;
var write_buf: [4096]u8 = undefined;
var reader = stream.reader(runtime.io, &read_buf);
var writer = stream.writer(runtime.io, &write_buf);Use raw fd operations only when the code genuinely needs them, such as setsockopt or a
dependency boundary that still speaks raw sockets.
std.net.has_unix_sockets
// becomes
std.Io.net.has_unix_socketsstd.posix.close is gone. Use std.c.close(fd). std.posix.read still exists.
// 0.15
mu: std.Thread.Mutex = .{},
cond: std.Thread.Condition = .{},
// 0.16
mu: std.Io.Mutex = .init,
cond: std.Io.Condition = .init,
rw: std.Io.RwLock = .init,Call-site shape:
self.mu.lockUncancelable(runtime.io);
defer self.mu.unlock(runtime.io);
self.cond.waitUncancelable(runtime.io, &self.mu);
self.rw.lockSharedUncancelable(runtime.io);
defer self.rw.unlockShared(runtime.io);If a lock site belongs to TurboAPI's runtime, pass runtime.io and move on. Do not paper over
this with PthreadMutex unless the code is truly outside the runtime boundary and cannot be
changed without disproportionate churn.
If the code already has access to io, prefer staying on std.Io.
If the code is an isolated legacy blocking path, a tiny compat helper is acceptable:
fn threadSleep(ns: u64) void {
const ts = std.c.timespec{
.sec = @intCast(ns / std.time.ns_per_s),
.nsec = @intCast(ns % std.time.ns_per_s),
};
_ = std.c.nanosleep(&ts, null);
}std.fs.cwd() call sites can usually become thin wrappers around std.Io.Dir.cwd():
const std = @import("std");
const runtime = @import("runtime.zig");
pub const fs = struct {
pub fn cwdDeleteTree(path: []const u8) !void {
return std.Io.Dir.cwd().deleteTree(runtime.io, path);
}
pub fn cwdCreateDirPath(path: []const u8) !void {
return std.Io.Dir.cwd().createDirPath(runtime.io, path);
}
pub fn cwdOpenFile(path: []const u8, options: std.Io.Dir.OpenFileOptions) !std.Io.File {
return std.Io.Dir.cwd().openFile(runtime.io, path, options);
}
};This is a good compat layer because it forwards into std.Io instead of hiding it.
std.time.timestamp(), milliTimestamp(), and nanoTimestamp() are removed.
fn milliTimestamp() i64 {
var ts: std.c.timespec = undefined;
_ = std.c.clock_gettime(.REALTIME, &ts);
return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000);
}ts.nsec is signed. Use @divTrunc, not /.
// 0.15
std.crypto.random.bytes(&buf);
// 0.16
runtime.io.random(&buf);// 0.15-style assumption
const kp = std.crypto.sign.Ed25519.KeyPair.generate();
// 0.16
const kp = std.crypto.sign.Ed25519.KeyPair.generate(runtime.io);// 0.15
var buf: [512]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try std.fmt.format(stream.writer(), "{d}", .{value});
const slice = stream.getWritten();
// 0.16
var buf: [512]u8 = undefined;
const slice = try std.fmt.bufPrint(&buf, "{d}", .{value});fn drain(w: *std.Io.Writer, data: []const u8) error{WriteFailed}!usize { ... }var buf: [4096]u8 = undefined;
const held = std.debug.lockStderr(&buf);
defer std.debug.unlockStderr();
try held.file_writer.print("msg: {s}\n", .{text});var list: std.ArrayListUnmanaged(u8) = .{ .items = &.{}, .capacity = 0 };If you define extern "c" fn socket(...), you can no longer declare const socket = ...
inside a local function. Rename the local.
- Do not create
PthreadMutex/PthreadConditionas the new house style. - Do not keep a fake
std.netfacade just to avoid threadingio. - Do not default to raw
read/writewrappers in code that can use nativestd.Io.net. - Do not hide the application runtime behind multiple singleton
Ioinstances.
- Add
runtime.zig. - Add narrow compat helpers for filesystem and timestamps.
- Fix build-system API moves.
- Fix
std.ioremovals and stderr changes. - Fix timestamps, random, and crypto.
- Replace thread primitives with
std.Io.Mutex,Condition, andRwLock. - Move networking to
std.Io.net. - Run
zig builduntil clean. - Run
zig build test.
If that order is respected, the codebase ends the migration on the abstraction Zig 0.16 is actually designed around.