zig/lib/std/Io/EventLoop.zig
2025-07-20 10:38:38 -07:00

749 lines
28 KiB
Zig

const std = @import("../std.zig");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Io = std.Io;
const EventLoop = @This();
const Alignment = std.mem.Alignment;
const IoUring = std.os.linux.IoUring;
/// Must be a thread-safe allocator.
gpa: Allocator,
mutex: std.Thread.Mutex,
queue: std.DoublyLinkedList,
/// Atomic copy of queue.len
queue_len: u32,
free: std.DoublyLinkedList,
main_fiber: Fiber,
idle_count: usize,
threads: std.ArrayListUnmanaged(Thread),
exiting: bool,
threadlocal var thread_index: u32 = undefined;
/// Empirically saw >128KB being used by the self-hosted backend to panic.
const idle_stack_size = 256 * 1024;
const io_uring_entries = 64;
const Thread = struct {
thread: std.Thread,
idle_context: Context,
current_context: *Context,
io_uring: IoUring,
fn currentFiber(thread: *Thread) *Fiber {
return @fieldParentPtr("context", thread.current_context);
}
};
const Fiber = struct {
context: Context,
awaiter: ?*Fiber,
queue_node: std.DoublyLinkedList.Node,
result_align: Alignment,
const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber)));
const max_result_align: Alignment = .@"16";
const max_result_size = max_result_align.forward(64);
/// This includes any stack realignments that need to happen, and also the
/// initial frame return address slot and argument frame, depending on target.
const min_stack_size = 4 * 1024 * 1024;
const max_context_align: Alignment = .@"16";
const max_context_size = max_context_align.forward(1024);
const allocation_size = std.mem.alignForward(
usize,
std.mem.alignForward(
usize,
max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size,
@max(@alignOf(AsyncClosure), max_context_align.toByteUnits()),
) + @sizeOf(AsyncClosure) + max_context_size,
std.heap.page_size_max,
);
fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber {
return if (free_node: {
el.mutex.lock();
defer el.mutex.unlock();
break :free_node el.free.pop();
}) |free_node|
@alignCast(@fieldParentPtr("queue_node", free_node))
else
@ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), allocation_size));
}
fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 {
return @as([*]align(@alignOf(Fiber)) u8, @ptrCast(f))[0..allocation_size];
}
fn allocatedEnd(f: *Fiber) [*]u8 {
const allocated_slice = f.allocatedSlice();
return allocated_slice[allocated_slice.len..].ptr;
}
fn resultPointer(f: *Fiber) [*]u8 {
return @ptrFromInt(f.result_align.forward(@intFromPtr(f) + @sizeOf(Fiber)));
}
};
pub fn io(el: *EventLoop) Io {
return .{
.userdata = el,
.vtable = &.{
.@"async" = @"async",
.@"await" = @"await",
.createFile = createFile,
.openFile = openFile,
.closeFile = closeFile,
.read = read,
.write = write,
},
};
}
pub fn init(el: *EventLoop, gpa: Allocator) !void {
const threads_size = @max(std.Thread.getCpuCount() catch 1, 1) * @sizeOf(Thread);
const idle_stack_end_offset = std.mem.alignForward(usize, threads_size + idle_stack_size, std.heap.page_size_max);
const allocated_slice = try gpa.alignedAlloc(u8, @alignOf(Thread), idle_stack_end_offset);
errdefer gpa.free(allocated_slice);
el.* = .{
.gpa = gpa,
.mutex = .{},
.queue = .{},
.queue_len = 0,
.free = .{},
.main_fiber = undefined,
.idle_count = 0,
.threads = .initBuffer(@ptrCast(allocated_slice[0..threads_size])),
.exiting = false,
};
thread_index = 0;
const main_thread = el.threads.addOneAssumeCapacity();
main_thread.io_uring = try IoUring.init(io_uring_entries, 0);
const idle_stack_end: [*]usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr));
(idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)};
main_thread.idle_context = .{
.rsp = @intFromPtr(idle_stack_end - 1),
.rbp = 0,
.rip = @intFromPtr(&mainIdleEntry),
};
std.log.debug("created main idle {*}", .{&main_thread.idle_context});
std.log.debug("created main {*}", .{&el.main_fiber});
main_thread.current_context = &el.main_fiber.context;
}
pub fn deinit(el: *EventLoop) void {
assert(el.queue.len == 0); // pending async
el.yield(null, .exit);
while (el.free.pop()) |free_node| {
const free_fiber: *Fiber = @alignCast(@fieldParentPtr("queue_node", free_node));
el.gpa.free(free_fiber.allocatedSlice());
}
const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.capacity * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max);
const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.items.ptr));
for (el.threads.items[1..]) |*thread| thread.thread.join();
el.gpa.free(allocated_ptr[0..idle_stack_end_offset]);
el.* = undefined;
}
fn yield(el: *EventLoop, optional_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void {
const thread: *Thread = &el.threads.items[thread_index];
const ready_context: *Context = ready_context: {
const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: {
el.mutex.lock();
defer el.mutex.unlock();
const expected_queue_len = std.math.lossyCast(u32, el.queue.len);
const ready_node = el.queue.pop();
_ = @cmpxchgStrong(u32, &el.queue_len, expected_queue_len, std.math.lossyCast(u32, el.queue.len), .monotonic, .monotonic);
break :ready_node ready_node;
}) |ready_node|
@alignCast(@fieldParentPtr("queue_node", ready_node))
else
break :ready_context &thread.idle_context;
break :ready_context &ready_fiber.context;
};
const message: SwitchMessage = .{
.contexts = .{
.prev = thread.current_context,
.ready = ready_context,
},
.pending_task = pending_task,
};
std.log.debug("switching from {*} to {*}", .{ message.contexts.prev, message.contexts.ready });
contextSwitch(&message).handle(el);
}
fn schedule(el: *EventLoop, fiber: *Fiber) void {
std.log.debug("scheduling {*}", .{fiber});
if (idle_count: {
el.mutex.lock();
defer el.mutex.unlock();
const expected_queue_len = std.math.lossyCast(u32, el.queue.len);
el.queue.append(&fiber.queue_node);
_ = @cmpxchgStrong(u32, &el.queue_len, expected_queue_len, std.math.lossyCast(u32, el.queue.len), .monotonic, .monotonic);
break :idle_count el.idle_count;
} > 0) {
_ = std.os.linux.futex2_wake(&el.queue_len, std.math.maxInt(u32), 1, std.os.linux.FUTEX2.SIZE_U32 | std.os.linux.FUTEX2.PRIVATE); // TODO: io_uring
return;
}
if (el.threads.items.len == el.threads.capacity) return;
const thread = el.threads.addOneAssumeCapacity();
thread.thread = std.Thread.spawn(.{
.stack_size = idle_stack_size,
.allocator = el.gpa,
}, threadEntry, .{ el, el.threads.items.len - 1 }) catch {
el.threads.items.len -= 1;
return;
};
}
fn recycle(el: *EventLoop, fiber: *Fiber) void {
std.log.debug("recyling {*}", .{fiber});
@memset(fiber.allocatedSlice(), undefined);
el.mutex.lock();
defer el.mutex.unlock();
el.free.append(&fiber.queue_node);
}
fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn {
message.handle(el);
el.idle();
el.yield(&el.main_fiber, .nothing);
unreachable; // switched to dead fiber
}
fn threadEntry(el: *EventLoop, index: usize) void {
thread_index = @intCast(index);
const thread: *Thread = &el.threads.items[index];
std.log.debug("created thread idle {*}", .{&thread.idle_context});
thread.io_uring = IoUring.init(io_uring_entries, 0) catch |err| {
std.log.warn("exiting worker thread during init due to io_uring init failure: {s}", .{@errorName(err)});
return;
};
thread.current_context = &thread.idle_context;
el.idle();
}
const CompletionKey = enum(u64) {
queue_len_futex_wait = 1,
_,
};
fn idle(el: *EventLoop) void {
const thread: *Thread = &el.threads.items[thread_index];
const iou = &thread.io_uring;
var cqes_buffer: [io_uring_entries]std.os.linux.io_uring_cqe = undefined;
var queue_len_futex_is_scheduled: bool = false;
while (true) {
el.yield(null, .nothing);
if (@atomicLoad(bool, &el.exiting, .acquire)) return;
if (!queue_len_futex_is_scheduled) {
const sqe = getSqe(&thread.io_uring);
sqe.prep_rw(.FUTEX_WAIT, std.os.linux.FUTEX2.SIZE_U32 | std.os.linux.FUTEX2.PRIVATE, @intFromPtr(&el.queue_len), 0, 0);
sqe.addr3 = std.math.maxInt(u32);
sqe.user_data = @intFromEnum(CompletionKey.queue_len_futex_wait);
queue_len_futex_is_scheduled = true;
}
_ = iou.submit_and_wait(1) catch |err| switch (err) {
error.SignalInterrupt => std.log.debug("submit_and_wait: SignalInterrupt", .{}),
else => @panic(@errorName(err)),
};
for (cqes_buffer[0 .. iou.copy_cqes(&cqes_buffer, 1) catch |err| switch (err) {
error.SignalInterrupt => cqes_len: {
std.log.debug("copy_cqes: SignalInterrupt", .{});
break :cqes_len 0;
},
else => @panic(@errorName(err)),
}]) |cqe| switch (@as(CompletionKey, @enumFromInt(cqe.user_data))) {
.queue_len_futex_wait => {
switch (errno(cqe.res)) {
.SUCCESS, .AGAIN => {},
.INVAL => unreachable,
else => |err| {
std.posix.unexpectedErrno(err) catch {};
@panic("unexpected");
},
}
std.log.debug("{*} woken up with queue size of {d}", .{
&thread.idle_context,
@atomicLoad(u32, &el.queue_len, .unordered),
});
queue_len_futex_is_scheduled = false;
},
_ => {
const fiber: *Fiber = @ptrFromInt(cqe.user_data);
const res: *i32 = @ptrCast(@alignCast(fiber.resultPointer()));
res.* = cqe.res;
el.schedule(fiber);
},
};
}
}
const SwitchMessage = struct {
contexts: extern struct {
prev: *Context,
ready: *Context,
},
pending_task: PendingTask,
const PendingTask = union(enum) {
nothing,
register_awaiter: *?*Fiber,
exit,
};
fn handle(message: *const SwitchMessage, el: *EventLoop) void {
const thread: *Thread = &el.threads.items[thread_index];
thread.current_context = message.contexts.ready;
switch (message.pending_task) {
.nothing => {},
.register_awaiter => |awaiter| {
const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev));
if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber);
},
.exit => {
@atomicStore(bool, &el.exiting, true, .unordered);
@atomicStore(u32, &el.queue_len, std.math.maxInt(u32), .release);
_ = std.os.linux.futex2_wake(&el.queue_len, std.math.maxInt(u32), std.math.maxInt(i32), std.os.linux.FUTEX2.SIZE_U32 | std.os.linux.FUTEX2.PRIVATE); // TODO: use io_uring
},
}
}
};
const Context = switch (builtin.cpu.arch) {
.x86_64 => extern struct {
rsp: u64,
rbp: u64,
rip: u64,
},
else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
};
inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage {
return @fieldParentPtr("contexts", switch (builtin.cpu.arch) {
.x86_64 => asm volatile (
\\ movq 0(%%rsi), %%rax
\\ movq 8(%%rsi), %%rcx
\\ leaq 0f(%%rip), %%rdx
\\ movq %%rsp, 0(%%rax)
\\ movq %%rbp, 8(%%rax)
\\ movq %%rdx, 16(%%rax)
\\ movq 0(%%rcx), %%rsp
\\ movq 8(%%rcx), %%rbp
\\ jmpq *16(%%rcx)
\\0:
: [received_message] "={rsi}" (-> *const @FieldType(SwitchMessage, "contexts")),
: [message_to_send] "{rsi}" (&message.contexts),
: "rax", "rcx", "rdx", "rbx", "rdi", //
"r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", //
"mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7", //
"zmm0", "zmm1", "zmm2", "zmm3", "zmm4", "zmm5", "zmm6", "zmm7", //
"zmm8", "zmm9", "zmm10", "zmm11", "zmm12", "zmm13", "zmm14", "zmm15", //
"zmm16", "zmm17", "zmm18", "zmm19", "zmm20", "zmm21", "zmm22", "zmm23", //
"zmm24", "zmm25", "zmm26", "zmm27", "zmm28", "zmm29", "zmm30", "zmm31", //
"fpsr", "fpcr", "mxcsr", "rflags", "dirflag", "memory"
),
else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
});
}
fn mainIdleEntry() callconv(.naked) void {
switch (builtin.cpu.arch) {
.x86_64 => asm volatile (
\\ movq (%%rsp), %%rdi
\\ jmp %[mainIdle:P]
:
: [mainIdle] "X" (&mainIdle),
),
else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
}
}
fn fiberEntry() callconv(.naked) void {
switch (builtin.cpu.arch) {
.x86_64 => asm volatile (
\\ leaq 8(%%rsp), %%rdi
\\ jmp %[AsyncClosure_call:P]
:
: [AsyncClosure_call] "X" (&AsyncClosure.call),
),
else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
}
}
pub fn @"async"(
userdata: ?*anyopaque,
result: []u8,
result_alignment: Alignment,
context: []const u8,
context_alignment: Alignment,
start: *const fn (context: *const anyopaque, result: *anyopaque) void,
) ?*std.Io.AnyFuture {
assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO
assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO
assert(result.len <= Fiber.max_result_size); // TODO
assert(context.len <= Fiber.max_context_size); // TODO
const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
const fiber = Fiber.allocate(event_loop) catch {
start(context.ptr, result.ptr);
return null;
};
std.log.debug("allocated {*}", .{fiber});
const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward(
@intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size,
) - @sizeOf(AsyncClosure));
fiber.* = .{
.context = switch (builtin.cpu.arch) {
.x86_64 => .{
.rsp = @intFromPtr(closure) - @sizeOf(usize),
.rbp = 0,
.rip = @intFromPtr(&fiberEntry),
},
else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)),
},
.awaiter = null,
.queue_node = undefined,
.result_align = result_alignment,
};
closure.* = .{
.event_loop = event_loop,
.fiber = fiber,
.start = start,
};
@memcpy(closure.contextPointer(), context);
event_loop.schedule(fiber);
return @ptrCast(fiber);
}
const AsyncClosure = struct {
event_loop: *EventLoop,
fiber: *Fiber,
start: *const fn (context: *const anyopaque, result: *anyopaque) void,
fn contextPointer(closure: *AsyncClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 {
return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(AsyncClosure));
}
fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(AsyncClosure))) noreturn {
message.handle(closure.event_loop);
std.log.debug("{*} performing async", .{closure.fiber});
closure.start(closure.contextPointer(), closure.fiber.resultPointer());
const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel);
closure.event_loop.yield(awaiter, .nothing);
unreachable; // switched to dead fiber
}
};
pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void {
const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
const future_fiber: *Fiber = @alignCast(@ptrCast(any_future));
if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, .{ .register_awaiter = &future_fiber.awaiter });
@memcpy(result, future_fiber.resultPointer());
event_loop.recycle(future_fiber);
}
pub fn cancel(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void {
const event_loop: *EventLoop = @alignCast(@ptrCast(userdata));
const future_fiber: *Fiber = @alignCast(@ptrCast(any_future));
// TODO set a flag that makes all IO operations for this fiber return error.Canceled
if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, .{ .register_awaiter = &future_fiber.awaiter });
@memcpy(result, future_fiber.resultPointer());
event_loop.recycle(future_fiber);
}
pub fn createFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File.OpenError!std.fs.File {
const el: *EventLoop = @ptrCast(@alignCast(userdata));
const posix = std.posix;
const sub_path_c = try posix.toPosixPath(sub_path);
var os_flags: posix.O = .{
.ACCMODE = if (flags.read) .RDWR else .WRONLY,
.CREAT = true,
.TRUNC = flags.truncate,
.EXCL = flags.exclusive,
};
if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true;
if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true;
// Use the O locking flags if the os supports them to acquire the lock
// atomically. Note that the NONBLOCK flag is removed after the openat()
// call is successful.
const has_flock_open_flags = @hasField(posix.O, "EXLOCK");
if (has_flock_open_flags) switch (flags.lock) {
.none => {},
.shared => {
os_flags.SHLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
.exclusive => {
os_flags.EXLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
};
const have_flock = @TypeOf(posix.system.flock) != void;
if (have_flock and !has_flock_open_flags and flags.lock != .none) {
@panic("TODO");
}
if (has_flock_open_flags and flags.lock_nonblocking) {
@panic("TODO");
}
const thread: *Thread = &el.threads.items[thread_index];
const iou = &thread.io_uring;
const sqe = getSqe(iou);
const fiber = thread.currentFiber();
sqe.prep_openat(dir.fd, &sub_path_c, os_flags, flags.mode);
sqe.user_data = @intFromPtr(fiber);
el.yield(null, .nothing);
const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)]));
const rc = result.*;
switch (errno(rc)) {
.SUCCESS => return .{ .handle = rc },
.INTR => @panic("TODO is this reachable?"),
.CANCELED => @panic("TODO figure out how this error code fits into things"),
.FAULT => unreachable,
.INVAL => return error.BadPathName,
.BADF => unreachable,
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.OPNOTSUPP => return error.FileLocksNotSupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
else => |err| return posix.unexpectedErrno(err),
}
}
pub fn openFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File {
const el: *EventLoop = @ptrCast(@alignCast(userdata));
const posix = std.posix;
const sub_path_c = try posix.toPosixPath(sub_path);
var os_flags: posix.O = .{
.ACCMODE = switch (flags.mode) {
.read_only => .RDONLY,
.write_only => .WRONLY,
.read_write => .RDWR,
},
};
if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true;
if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true;
if (@hasField(posix.O, "NOCTTY")) os_flags.NOCTTY = !flags.allow_ctty;
// Use the O locking flags if the os supports them to acquire the lock
// atomically.
const has_flock_open_flags = @hasField(posix.O, "EXLOCK");
if (has_flock_open_flags) {
// Note that the NONBLOCK flag is removed after the openat() call
// is successful.
switch (flags.lock) {
.none => {},
.shared => {
os_flags.SHLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
.exclusive => {
os_flags.EXLOCK = true;
os_flags.NONBLOCK = flags.lock_nonblocking;
},
}
}
const have_flock = @TypeOf(posix.system.flock) != void;
if (have_flock and !has_flock_open_flags and flags.lock != .none) {
@panic("TODO");
}
if (has_flock_open_flags and flags.lock_nonblocking) {
@panic("TODO");
}
const thread: *Thread = &el.threads.items[thread_index];
const iou = &thread.io_uring;
const sqe = getSqe(iou);
const fiber = thread.currentFiber();
sqe.prep_openat(dir.fd, &sub_path_c, os_flags, 0);
sqe.user_data = @intFromPtr(fiber);
el.yield(null, .nothing);
const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)]));
const rc = result.*;
switch (errno(rc)) {
.SUCCESS => return .{ .handle = rc },
.INTR => @panic("TODO is this reachable?"),
.CANCELED => @panic("TODO figure out how this error code fits into things"),
.FAULT => unreachable,
.INVAL => return error.BadPathName,
.BADF => unreachable,
.ACCES => return error.AccessDenied,
.FBIG => return error.FileTooBig,
.OVERFLOW => return error.FileTooBig,
.ISDIR => return error.IsDir,
.LOOP => return error.SymLinkLoop,
.MFILE => return error.ProcessFdQuotaExceeded,
.NAMETOOLONG => return error.NameTooLong,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.NoDevice,
.NOENT => return error.FileNotFound,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.NOTDIR => return error.NotDir,
.PERM => return error.PermissionDenied,
.EXIST => return error.PathAlreadyExists,
.BUSY => return error.DeviceBusy,
.OPNOTSUPP => return error.FileLocksNotSupported,
.AGAIN => return error.WouldBlock,
.TXTBSY => return error.FileBusy,
.NXIO => return error.NoDevice,
else => |err| return posix.unexpectedErrno(err),
}
}
fn errno(signed: i32) std.posix.E {
const int = if (signed > -4096 and signed < 0) -signed else 0;
return @enumFromInt(int);
}
fn getSqe(iou: *IoUring) *std.os.linux.io_uring_sqe {
return iou.get_sqe() catch @panic("TODO: handle submission queue full");
}
pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void {
const el: *EventLoop = @ptrCast(@alignCast(userdata));
const posix = std.posix;
const thread: *Thread = &el.threads.items[thread_index];
const iou = &thread.io_uring;
const sqe = getSqe(iou);
const fiber = thread.currentFiber();
sqe.prep_close(file.handle);
sqe.user_data = @intFromPtr(fiber);
el.yield(null, .nothing);
const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)]));
const rc = result.*;
switch (errno(rc)) {
.SUCCESS => return,
.INTR => @panic("TODO is this reachable?"),
.CANCELED => @panic("TODO figure out how this error code fits into things"),
.BADF => unreachable, // Always a race condition.
else => return,
}
}
pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) std.fs.File.ReadError!usize {
const el: *EventLoop = @ptrCast(@alignCast(userdata));
const posix = std.posix;
const thread: *Thread = &el.threads.items[thread_index];
const iou = &thread.io_uring;
const sqe = getSqe(iou);
const fiber = thread.currentFiber();
sqe.prep_read(file.handle, buffer, std.math.maxInt(u64));
sqe.user_data = @intFromPtr(fiber);
el.yield(null, .nothing);
const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)]));
const rc = result.*;
switch (errno(rc)) {
.SUCCESS => return @as(u32, @bitCast(rc)),
.INTR => @panic("TODO is this reachable?"),
.CANCELED => @panic("TODO figure out how this error code fits into things"),
.INVAL => unreachable,
.FAULT => unreachable,
.NOENT => return error.ProcessNotFound,
.AGAIN => return error.WouldBlock,
.BADF => return error.NotOpenForReading, // Can be a race condition.
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOBUFS => return error.SystemResources,
.NOMEM => return error.SystemResources,
.NOTCONN => return error.SocketNotConnected,
.CONNRESET => return error.ConnectionResetByPeer,
.TIMEDOUT => return error.ConnectionTimedOut,
else => |err| return posix.unexpectedErrno(err),
}
}
pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) std.fs.File.WriteError!usize {
const el: *EventLoop = @ptrCast(@alignCast(userdata));
const posix = std.posix;
const thread: *Thread = &el.threads.items[thread_index];
const iou = &thread.io_uring;
const sqe = getSqe(iou);
const fiber = thread.currentFiber();
sqe.prep_write(file.handle, buffer, std.math.maxInt(u64));
sqe.user_data = @intFromPtr(fiber);
el.yield(null, .nothing);
const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)]));
const rc = result.*;
switch (errno(rc)) {
.SUCCESS => return @as(u32, @bitCast(rc)),
.INTR => @panic("TODO is this reachable?"),
.CANCELED => @panic("TODO figure out how this error code fits into things"),
.INVAL => return error.InvalidArgument,
.FAULT => unreachable,
.NOENT => return error.ProcessNotFound,
.AGAIN => return error.WouldBlock,
.BADF => return error.NotOpenForWriting, // can be a race condition.
.DESTADDRREQ => unreachable, // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.ACCES => return error.AccessDenied,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.CONNRESET => return error.ConnectionResetByPeer,
.BUSY => return error.DeviceBusy,
.NXIO => return error.NoDevice,
.MSGSIZE => return error.MessageTooBig,
else => |err| return posix.unexpectedErrno(err),
}
}