1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
//! TinyIo: a tiny, blocking `std.Io` implementation optimized for
//! binary size.
//!
//! Compared to `std.Io.Threaded`, the binary cost is roughly ~100KB to
//! ~200KB (macOS vs Linux) smaller, and the runtime cost is ~300KB (256KB
//! of TLS plus the ~20KB threaded structure) smaller. On Windows the
//! DLL is ~370KB smaller (ReleaseFast) because Threaded's Windows logic
//! has Winsock, AFD and process creation.
//!
//! Utilizing the built-in Zig `std.Io.Threaded` is easy but due to its
//! vtable architecture, the linker can't prune uncalled functions, meaning
//! you have to pay for all the code for every op. Plus, at runtime, Threaded
//! requires 256KB of thread-local storage, plus the structure itself is
//! very large.
//!
//! TinyIo implements exactly the operations we need as plain blocking
//! syscalls. It doesn't support cancelation. It doesn't support concurrency
//! operations, but all our APIs are direct syscalls so it supports the
//! concurrency of calls that those syscalls support (which is usually
//! safe on POSIX systems).
//!
//! The platform arms live in `tinyio/`: `posix.zig` is direct syscalls
//! cribbed from Zig's std.posix, and `windows.zig` is direct ntdll and
//! kernel32 calls modeled on the Windows arms of `std.Io.Threaded`, so
//! the library never links Threaded's vtable there either. Shared tests
//! are in `tinyio/test.zig`.
const TinyIo = @This();
const std = @import("std");
const builtin = @import("builtin");
const Io = std.Io;
/// True if this platform has a real TinyIo implementation. On
/// unsupported platforms `io()` still works but every operation fails
/// like `std.Io.failing`.
pub const supported: bool = switch (builtin.os.tag) {
.wasi, .freestanding, .other, .uefi => false,
else => true,
};
/// Initialize a TinyIo. TinyIo is stateless (zero-sized), so this exists
/// purely to mirror the `std.Io.Threaded` initialization and usage shape,
/// making the two easy to swap.
pub const init: TinyIo = .{};
/// Returns the `std.Io` interface for this implementation.
pub fn io(self: TinyIo) Io {
_ = self;
return .{
.userdata = null,
.vtable = &vtable,
};
}
const vtable: Io.VTable = if (!supported) std.Io.failing.vtable.* else .{
.crashHandler = Io.noCrashHandler,
// `noAsync` runs the task synchronously and returns null, which means
// `await`/`cancel` can never be called. `concurrent` reports that
// concurrency is unavailable, which callers must handle anyway.
.async = Io.noAsync,
.concurrent = Io.failingConcurrent,
.await = Io.unreachableAwait,
.cancel = Io.unreachableCancel,
.groupAsync = Io.noGroupAsync,
.groupConcurrent = Io.failingGroupConcurrent,
.groupAwait = Io.unreachableGroupAwait,
.groupCancel = Io.unreachableGroupCancel,
// Cancelation is never requested (there is no async), but these are
// benign no-ops instead of `unreachable` since generic std code may
// toggle cancel protection around operations.
.recancel = recancel,
.swapCancelProtection = swapCancelProtection,
.checkCancel = checkCancel,
// Real futexes: `std.Io.Mutex` (used by kitty graphics storage) parks
// on these when contended.
.futexWait = futexWait,
.futexWaitUncancelable = futexWaitUncancelable,
.futexWake = futexWake,
// `operate` carries the streaming file reads used by `File.Reader`.
.operate = operate,
.batchAwaitAsync = Io.unreachableBatchAwaitAsync,
.batchAwaitConcurrent = Io.unreachableBatchAwaitConcurrent,
.batchCancel = Io.unreachableBatchCancel,
.dirCreateDir = Io.failingDirCreateDir,
.dirCreateDirPath = Io.failingDirCreateDirPath,
.dirCreateDirPathOpen = Io.failingDirCreateDirPathOpen,
.dirOpenDir = Io.failingDirOpenDir,
.dirStat = Io.failingDirStat,
.dirStatFile = Io.failingDirStatFile,
.dirAccess = Io.failingDirAccess,
.dirCreateFile = Io.failingDirCreateFile,
.dirCreateFileAtomic = Io.failingDirCreateFileAtomic,
.dirOpenFile = impl.dirOpenFile,
.dirClose = impl.dirClose,
.dirRead = Io.noDirRead,
.dirRealPath = Io.failingDirRealPath,
.dirRealPathFile = impl.dirRealPathFile,
.dirDeleteFile = impl.dirDeleteFile,
.dirDeleteDir = Io.failingDirDeleteDir,
.dirRename = Io.failingDirRename,
.dirRenamePreserve = Io.failingDirRenamePreserve,
.dirSymLink = Io.failingDirSymLink,
.dirReadLink = Io.failingDirReadLink,
.dirSetOwner = Io.failingDirSetOwner,
.dirSetFileOwner = Io.failingDirSetFileOwner,
.dirSetPermissions = Io.failingDirSetPermissions,
.dirSetFilePermissions = Io.failingDirSetFilePermissions,
.dirSetTimestamps = Io.noDirSetTimestamps,
.dirHardLink = Io.failingDirHardLink,
.fileStat = impl.fileStat,
.fileLength = impl.fileLength,
.fileClose = impl.fileClose,
.fileWritePositional = Io.failingFileWritePositional,
.fileWriteFileStreaming = Io.noFileWriteFileStreaming,
.fileWriteFilePositional = Io.noFileWriteFilePositional,
.fileReadPositional = impl.fileReadPositional,
.fileSeekBy = impl.fileSeekBy,
.fileSeekTo = impl.fileSeekTo,
.fileSync = Io.failingFileSync,
.fileIsTty = Io.unreachableFileIsTty,
.fileEnableAnsiEscapeCodes = Io.unreachableFileEnableAnsiEscapeCodes,
.fileSupportsAnsiEscapeCodes = Io.unreachableFileSupportsAnsiEscapeCodes,
.fileSetLength = Io.failingFileSetLength,
.fileSetOwner = Io.failingFileSetOwner,
.fileSetPermissions = Io.failingFileSetPermissions,
.fileSetTimestamps = Io.noFileSetTimestamps,
.fileLock = Io.failingFileLock,
.fileTryLock = Io.failingFileTryLock,
.fileUnlock = Io.unreachableFileUnlock,
.fileDowngradeLock = Io.failingFileDowngradeLock,
.fileRealPath = impl.fileRealPath,
.fileHardLink = Io.failingFileHardLink,
.fileMemoryMapCreate = Io.failingFileMemoryMapCreate,
.fileMemoryMapDestroy = Io.unreachableFileMemoryMapDestroy,
.fileMemoryMapSetLength = Io.unreachableFileMemoryMapSetLength,
.fileMemoryMapRead = Io.unreachableFileMemoryMapRead,
.fileMemoryMapWrite = Io.unreachableFileMemoryMapWrite,
.processExecutableOpen = Io.failingProcessExecutableOpen,
.processExecutablePath = Io.failingProcessExecutablePath,
.lockStderr = Io.unreachableLockStderr,
.tryLockStderr = Io.noTryLockStderr,
.unlockStderr = Io.unreachableUnlockStderr,
.processCurrentPath = Io.failingProcessCurrentPath,
.processSetCurrentDir = Io.failingProcessSetCurrentDir,
.processSetCurrentPath = Io.failingProcessSetCurrentPath,
.processReplace = Io.failingProcessReplace,
.processReplacePath = Io.failingProcessReplacePath,
.processSpawn = Io.failingProcessSpawn,
.processSpawnPath = Io.failingProcessSpawnPath,
.childWait = Io.unreachableChildWait,
.childKill = Io.unreachableChildKill,
.progressParentFile = Io.failingProgressParentFile,
.random = Io.noRandom,
.randomSecure = randomSecure,
.now = Io.noNow,
.clockResolution = Io.failingClockResolution,
.sleep = Io.noSleep,
.netListenIp = Io.failingNetListenIp,
.netAccept = Io.failingNetAccept,
.netBindIp = Io.failingNetBindIp,
.netConnectIp = Io.failingNetConnectIp,
.netListenUnix = Io.failingNetListenUnix,
.netConnectUnix = Io.failingNetConnectUnix,
.netSocketCreatePair = Io.failingNetSocketCreatePair,
.netSend = Io.failingNetSend,
.netRead = Io.failingNetRead,
.netWrite = Io.failingNetWrite,
.netWriteFile = Io.failingNetWriteFile,
.netClose = Io.unreachableNetClose,
.netShutdown = Io.failingNetShutdown,
.netInterfaceNameResolve = Io.failingNetInterfaceNameResolve,
.netInterfaceName = Io.unreachableNetInterfaceName,
.netLookup = Io.failingNetLookup,
};
/// The platform arm behind the vtable. Each arm exports the same set of
/// operations; only the selected one is ever analyzed, and `supported`
/// gates the vtable so unsupported targets never resolve either.
const impl = switch (builtin.os.tag) {
.windows => @import("tinyio/windows.zig"),
else => @import("tinyio/posix.zig"),
};
fn recancel(_: ?*anyopaque) void {}
fn swapCancelProtection(
_: ?*anyopaque,
new: Io.CancelProtection,
) Io.CancelProtection {
// Stateless: there is no async, so cancelation can never be requested
// and the protection state is meaningless. Callers save this return
// value only to restore it via another call to this function.
_ = new;
return .blocked;
}
fn checkCancel(_: ?*anyopaque) Io.Cancelable!void {}
fn randomSecure(_: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void {
if (buffer.len == 0) return;
return impl.randomSecure(buffer);
}
fn operate(
userdata: ?*anyopaque,
operation: Io.Operation,
) Io.Cancelable!Io.Operation.Result {
switch (operation) {
// Streaming file reads are how `File.Reader` consumes files that
// don't support positional reads (and the fallback path in
// general).
.file_read_streaming => |o| return .{
.file_read_streaming = impl.fileReadStreaming(o.file, o.data),
},
// Everything else (streaming writes, ioctls, socket receives) is
// unused by the terminal; fail like `Io.failing` does.
else => return Io.failingOperate(userdata, operation),
}
}
/// Convert an `Io.Timeout` to relative nanoseconds without consulting a
/// clock. Deadlines can't be resolved without `now` support; treat them
/// as a short poll, which is valid because futex waits are allowed to
/// wake spuriously (callers must re-check their condition and retry).
fn timeoutToNs(timeout: Io.Timeout) ?u64 {
return switch (timeout) {
.none => null,
.duration => |d| @intCast(@max(0, d.raw.toNanoseconds())),
.deadline => 10 * std.time.ns_per_ms,
};
}
fn futexWait(
userdata: ?*anyopaque,
ptr: *const u32,
expected: u32,
timeout: Io.Timeout,
) Io.Cancelable!void {
_ = userdata;
impl.futexWaitInner(ptr, expected, timeoutToNs(timeout));
}
fn futexWaitUncancelable(userdata: ?*anyopaque, ptr: *const u32, expected: u32) void {
_ = userdata;
impl.futexWaitInner(ptr, expected, null);
}
fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
return impl.futexWake(userdata, ptr, max_waiters);
}
test {
_ = @import("tinyio/test.zig");
}
|