From 21b731677224deeb425d971e0754bac6c0daee0e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 24 Mar 2025 18:43:53 -0700 Subject: [PATCH 001/244] introduce std.Io interface which is planned to have all I/O operations in the interface, but for now has only async and await. --- lib/std/Io.zig | 69 +++++++++++++++++++++++++++++++++++++++++ lib/std/Thread/Pool.zig | 23 ++------------ 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 18600c2140..72a6174060 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -553,3 +553,72 @@ test { _ = tty; _ = @import("Io/test.zig"); } + +const Io = @This(); + +userdata: ?*anyopaque, +vtable: *const VTable, + +pub const VTable = struct { + /// If it returns `null` it means `result` has been already populated and + /// `await` will be a no-op. + async: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// The pointer of this slice is an "eager" result value. + /// The length is the size in bytes of the result type. + eager_result: []u8, + /// Passed to `start`. + context: ?*anyopaque, + start: *const fn (context: ?*anyopaque, result: *anyopaque) void, + ) ?*AnyFuture, + + /// This function is only called when `async` returns a non-null value. + await: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// The same value that was returned from `async`. + any_future: *AnyFuture, + /// Points to a buffer where the result is written. + /// The length is equal to size in bytes of result type. + result: []u8, + ) void, +}; + +pub const AnyFuture = opaque {}; + +pub fn Future(Result: type) type { + return struct { + any_future: ?*AnyFuture, + result: Result, + + pub fn await(f: *@This(), io: Io) Result { + const any_future = f.any_future orelse return f.result; + io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1])); + f.any_future = null; + return f.result; + } + }; +} + +/// `s` is a struct instance that contains a function like this: +/// ``` +/// struct { +/// pub fn start(s: S) Result { ... } +/// } +/// ``` +/// where `Result` is any type. +pub fn async(io: Io, s: anytype) Future(@typeInfo(@TypeOf(@TypeOf(s).start)).@"fn".return_type.?) { + const S = @TypeOf(s); + const Result = @typeInfo(@TypeOf(S.start)).@"fn".return_type.?; + const TypeErased = struct { + fn start(context: ?*anyopaque, result: *anyopaque) void { + const context_casted: *const S = @alignCast(@ptrCast(context)); + const result_casted: *Result = @ptrCast(@alignCast(result)); + result_casted.* = S.start(context_casted.*); + } + }; + var future: Future(Result) = undefined; + future.any_future = io.vtable.async(io.userdata, @ptrCast((&future.result)[0..1]), @constCast(&s), TypeErased.start); + return future; +} diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index e836665d70..86e8e87056 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -7,6 +7,7 @@ mutex: std.Thread.Mutex = .{}, cond: std.Thread.Condition = .{}, run_queue: std.SinglyLinkedList = .{}, is_running: bool = true, +/// Must be a thread-safe allocator. allocator: std.mem.Allocator, threads: if (builtin.single_threaded) [0]std.Thread else []std.Thread, ids: if (builtin.single_threaded) struct { @@ -16,12 +17,12 @@ ids: if (builtin.single_threaded) struct { } } else std.AutoArrayHashMapUnmanaged(std.Thread.Id, void), -const Runnable = struct { +pub const Runnable = struct { runFn: RunProto, node: std.SinglyLinkedList.Node = .{}, }; -const RunProto = *const fn (*Runnable, id: ?usize) void; +pub const RunProto = *const fn (*Runnable, id: ?usize) void; pub const Options = struct { allocator: std.mem.Allocator, @@ -117,12 +118,6 @@ pub fn spawnWg(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); @call(.auto, func, closure.arguments); closure.wait_group.finish(); - - // The thread pool's allocator is protected by the mutex. - const mutex = &closure.pool.mutex; - mutex.lock(); - defer mutex.unlock(); - closure.pool.allocator.destroy(closure); } }; @@ -179,12 +174,6 @@ pub fn spawnWgId(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, ar const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); @call(.auto, func, .{id.?} ++ closure.arguments); closure.wait_group.finish(); - - // The thread pool's allocator is protected by the mutex. - const mutex = &closure.pool.mutex; - mutex.lock(); - defer mutex.unlock(); - closure.pool.allocator.destroy(closure); } }; @@ -228,12 +217,6 @@ pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) !void { fn runFn(runnable: *Runnable, _: ?usize) void { const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); @call(.auto, func, closure.arguments); - - // The thread pool's allocator is protected by the mutex. - const mutex = &closure.pool.mutex; - mutex.lock(); - defer mutex.unlock(); - closure.pool.allocator.destroy(closure); } }; From cb9f9bf58d9cf18c8fc70967c43240b1ea0f9ca1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 24 Mar 2025 18:49:03 -0700 Subject: [PATCH 002/244] make thread pool satisfy async/await interface --- lib/std/Thread/Pool.zig | 62 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 86e8e87056..f1d2a7338f 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -1,7 +1,8 @@ -const std = @import("std"); const builtin = @import("builtin"); -const Pool = @This(); +const std = @import("std"); +const assert = std.debug.assert; const WaitGroup = @import("WaitGroup.zig"); +const Pool = @This(); mutex: std.Thread.Mutex = .{}, cond: std.Thread.Condition = .{}, @@ -307,3 +308,60 @@ pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void { pub fn getIdCount(pool: *Pool) usize { return @intCast(1 + pool.threads.len); } + +const AsyncClosure = struct { + func: *const fn (context: ?*anyopaque, result: *anyopaque) void, + context: ?*anyopaque, + run_node: std.Thread.Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, + reset_event: std.Thread.ResetEvent, + + fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { + const run_node: *std.Thread.Pool.RunQueue.Node = @fieldParentPtr("data", runnable); + const closure: *@This() = @alignCast(@fieldParentPtr("run_node", run_node)); + closure.func(closure.context, closure.resultPointer()); + closure.reset_event.set(); + } + + fn resultPointer(closure: *@This()) [*]u8 { + const base: [*]u8 = @ptrCast(closure); + return base + @sizeOf(@This()); + } +}; + +pub fn @"async"( + userdata: ?*anyopaque, + eager_result: []u8, + context: ?*anyopaque, + start: *const fn (context: ?*anyopaque, result: *anyopaque) void, +) ?*std.Io.AnyFuture { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + pool.mutex.lock(); + + const gpa = pool.allocator; + const n = @sizeOf(AsyncClosure) + eager_result.len; + const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, @alignOf(AsyncClosure), n) catch { + pool.mutex.unlock(); + start(context, eager_result.ptr); + return null; + })); + closure.* = .{ + .func = start, + .context = context, + .reset_event = .{}, + }; + pool.run_queue.prepend(&closure.run_node); + pool.mutex.unlock(); + + pool.cond.signal(); + + return @ptrCast(closure); +} + +pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void { + const thread_pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); + closure.reset_event.wait(); + const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); + @memcpy(result, (base + @sizeOf(AsyncClosure))[0..result.len]); + thread_pool.allocator.free(base[0 .. @sizeOf(AsyncClosure) + result.len]); +} From 4d56267938b83b04cf6f4dd6e0f4328e0cec8375 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 26 Mar 2025 19:24:37 -0700 Subject: [PATCH 003/244] demo: single-threaded green threads implementation --- lib/std/Io.zig | 3 + lib/std/Io/EventLoop.zig | 210 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 lib/std/Io/EventLoop.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 72a6174060..bf3151852f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -556,6 +556,8 @@ test { const Io = @This(); +pub const EventLoop = @import("Io/EventLoop.zig"); + userdata: ?*anyopaque, vtable: *const VTable, @@ -567,6 +569,7 @@ pub const VTable = struct { userdata: ?*anyopaque, /// The pointer of this slice is an "eager" result value. /// The length is the size in bytes of the result type. + /// This pointer's lifetime expires directly after the call to this function. eager_result: []u8, /// Passed to `start`. context: ?*anyopaque, diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig new file mode 100644 index 0000000000..c5a9dd63ff --- /dev/null +++ b/lib/std/Io/EventLoop.zig @@ -0,0 +1,210 @@ +const std = @import("../std.zig"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Io = std.Io; +const EventLoop = @This(); + +gpa: Allocator, +queue: std.DoublyLinkedList(void), +free: std.DoublyLinkedList(void), +main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), + +threadlocal var current_fiber: *Fiber = undefined; + +const max_result_len = 64; +const min_stack_size = 4 * 1024 * 1024; + +const Fiber = struct { + regs: Regs, + awaiter: ?*Fiber, + queue_node: std.DoublyLinkedList(void).Node, + + const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); + + fn resultPointer(f: *Fiber) [*]u8 { + const base: [*]u8 = @ptrCast(f); + return base + @sizeOf(Fiber); + } + + fn stackEndPointer(f: *Fiber) [*]u8 { + const base: [*]u8 = @ptrCast(f); + return base + std.mem.alignForward( + usize, + @sizeOf(Fiber) + max_result_len + min_stack_size, + std.heap.page_size_max, + ); + } +}; + +pub fn init(el: *EventLoop, gpa: Allocator) void { + el.* = .{ + .gpa = gpa, + .queue = .{}, + .free = .{}, + .main_fiber_buffer = undefined, + }; + current_fiber = @ptrCast(&el.main_fiber_buffer); +} + +fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { + assert(result_len <= max_result_len); + const free_node = el.free.pop() orelse { + const n = std.mem.alignForward( + usize, + @sizeOf(Fiber) + max_result_len + min_stack_size, + std.heap.page_size_max, + ); + return @alignCast(@ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), n))); + }; + return @fieldParentPtr("queue_node", free_node); +} + +fn yield(el: *EventLoop, optional_fiber: ?*Fiber) void { + if (optional_fiber) |fiber| { + const old = ¤t_fiber.regs; + current_fiber = fiber; + contextSwitch(old, &fiber.regs); + return; + } + if (el.queue.pop()) |node| { + const fiber: *Fiber = @fieldParentPtr("queue_node", node); + const old = ¤t_fiber.regs; + current_fiber = fiber; + contextSwitch(old, &fiber.regs); + return; + } + @panic("everything is done"); +} + +/// Equivalent to calling `yield` and then giving the fiber back to the event loop. +fn exit(el: *EventLoop, optional_fiber: ?*Fiber) noreturn { + yield(el, optional_fiber); + @panic("TODO recycle the fiber"); +} + +fn schedule(el: *EventLoop, fiber: *Fiber) void { + el.queue.append(&fiber.queue_node); +} + +fn myFiber(el: *EventLoop) *Fiber { + _ = el; + return current_fiber; +} + +const Regs = extern struct { + rsp: usize, + r15: usize, + r14: usize, + r13: usize, + r12: usize, + rbx: usize, + rbp: usize, +}; + +const contextSwitch: *const fn (old: *Regs, new: *Regs) callconv(.c) void = @ptrCast(&contextSwitch_naked); + +noinline fn contextSwitch_naked() callconv(.naked) void { + asm volatile ( + \\movq %%rsp, 0x00(%%rdi) + \\movq %%r15, 0x08(%%rdi) + \\movq %%r14, 0x10(%%rdi) + \\movq %%r13, 0x18(%%rdi) + \\movq %%r12, 0x20(%%rdi) + \\movq %%rbx, 0x28(%%rdi) + \\movq %%rbp, 0x30(%%rdi) + \\ + \\movq 0x00(%%rsi), %%rsp + \\movq 0x08(%%rsi), %%r15 + \\movq 0x10(%%rsi), %%r14 + \\movq 0x18(%%rsi), %%r13 + \\movq 0x20(%%rsi), %%r12 + \\movq 0x28(%%rsi), %%rbx + \\movq 0x30(%%rsi), %%rbp + \\ + \\ret + ); +} + +fn popRet() callconv(.naked) void { + asm volatile ( + \\pop %%rdi + \\ret + ); +} + +pub fn @"async"( + userdata: ?*anyopaque, + eager_result: []u8, + context: ?*anyopaque, + start: *const fn (context: ?*anyopaque, result: *anyopaque) void, +) ?*std.Io.AnyFuture { + const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); + const fiber = event_loop.allocateFiber(eager_result.len) catch { + start(context, eager_result.ptr); + return null; + }; + fiber.awaiter = null; + fiber.queue_node = .{ .data = {} }; + + const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward( + usize, + @intFromPtr(fiber.stackEndPointer() - @sizeOf(AsyncClosure)), + @alignOf(AsyncClosure), + )); + closure.* = .{ + .event_loop = event_loop, + .context = context, + .fiber = fiber, + .start = start, + }; + const stack_end_ptr: [*]align(16) usize = @alignCast(@ptrCast(closure)); + (stack_end_ptr - 1)[0] = 0; + (stack_end_ptr - 2)[0] = @intFromPtr(&AsyncClosure.call); + (stack_end_ptr - 3)[0] = @intFromPtr(closure); + (stack_end_ptr - 4)[0] = @intFromPtr(&popRet); + + fiber.regs = .{ + .rsp = @intFromPtr(stack_end_ptr - 4), + .r15 = 0, + .r14 = 0, + .r13 = 0, + .r12 = 0, + .rbx = 0, + .rbp = 0, + }; + + event_loop.schedule(fiber); + return @ptrCast(fiber); +} + +const AsyncClosure = struct { + _: void align(16) = {}, + event_loop: *EventLoop, + context: ?*anyopaque, + fiber: *EventLoop.Fiber, + start: *const fn (context: ?*anyopaque, result: *anyopaque) void, + + fn call(closure: *AsyncClosure) callconv(.c) void { + std.log.debug("wrap called in async", .{}); + closure.start(closure.context, closure.fiber.resultPointer()); + const awaiter = @atomicRmw(?*EventLoop.Fiber, &closure.fiber.awaiter, .Xchg, EventLoop.Fiber.finished, .seq_cst); + closure.event_loop.exit(awaiter); + } +}; + +pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void { + const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); + const future_fiber: *EventLoop.Fiber = @alignCast(@ptrCast(any_future)); + const result_src = future_fiber.resultPointer()[0..result.len]; + const my_fiber = event_loop.myFiber(); + + const prev = @atomicRmw(?*EventLoop.Fiber, &future_fiber.awaiter, .Xchg, my_fiber, .seq_cst); + if (prev == EventLoop.Fiber.finished) { + @memcpy(result, result_src); + return; + } + event_loop.yield(prev); + // Resumed when the value is available. + std.log.debug("yield returned in await", .{}); + @memcpy(result, result_src); +} From fe6f1efde4b1b192a7e8061ee35c4af4838debae Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 27 Mar 2025 01:49:01 -0400 Subject: [PATCH 004/244] EventLoop: prepare for threading --- lib/std/Io/EventLoop.zig | 113 ++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index c5a9dd63ff..2976f3386a 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -5,6 +5,7 @@ const Io = std.Io; const EventLoop = @This(); gpa: Allocator, +mutex: std.Thread.Mutex, queue: std.DoublyLinkedList(void), free: std.DoublyLinkedList(void), main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), @@ -39,6 +40,7 @@ const Fiber = struct { pub fn init(el: *EventLoop, gpa: Allocator) void { el.* = .{ .gpa = gpa, + .mutex = .{}, .queue = .{}, .free = .{}, .main_fiber_buffer = undefined, @@ -48,7 +50,11 @@ pub fn init(el: *EventLoop, gpa: Allocator) void { fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { assert(result_len <= max_result_len); - const free_node = el.free.pop() orelse { + const free_node = free_node: { + el.mutex.lock(); + defer el.mutex.unlock(); + break :free_node el.free.pop(); + } orelse { const n = std.mem.alignForward( usize, @sizeOf(Fiber) + max_result_len + min_stack_size, @@ -59,36 +65,48 @@ fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { return @fieldParentPtr("queue_node", free_node); } -fn yield(el: *EventLoop, optional_fiber: ?*Fiber) void { - if (optional_fiber) |fiber| { - const old = ¤t_fiber.regs; - current_fiber = fiber; - contextSwitch(old, &fiber.regs); - return; - } - if (el.queue.pop()) |node| { - const fiber: *Fiber = @fieldParentPtr("queue_node", node); - const old = ¤t_fiber.regs; - current_fiber = fiber; - contextSwitch(old, &fiber.regs); - return; - } - @panic("everything is done"); +fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { + const message: SwitchMessage = .{ + .ready_fiber = optional_fiber orelse if (ready_node: { + el.mutex.lock(); + defer el.mutex.unlock(); + break :ready_node el.queue.pop(); + }) |ready_node| + @fieldParentPtr("queue_node", ready_node) + else if (register_awaiter) |_| + @panic("no other fiber to switch to in order to be able to register this fiber as an awaiter") // time to switch to an idle fiber? + else + return, // nothing to do + .register_awaiter = register_awaiter, + }; + std.log.debug("switching from {*} to {*}", .{ current_fiber, message.ready_fiber }); + SwitchMessage.handle(@ptrFromInt(contextSwitch(¤t_fiber.regs, &message.ready_fiber.regs, @intFromPtr(&message))), el); } -/// Equivalent to calling `yield` and then giving the fiber back to the event loop. -fn exit(el: *EventLoop, optional_fiber: ?*Fiber) noreturn { - yield(el, optional_fiber); - @panic("TODO recycle the fiber"); -} +const SwitchMessage = struct { + ready_fiber: *Fiber, + register_awaiter: ?*?*Fiber, + + fn handle(message: *const SwitchMessage, el: *EventLoop) void { + const prev_fiber = current_fiber; + current_fiber = message.ready_fiber; + if (message.register_awaiter) |awaiter| if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); + } +}; fn schedule(el: *EventLoop, fiber: *Fiber) void { + el.mutex.lock(); + defer el.mutex.unlock(); el.queue.append(&fiber.queue_node); } -fn myFiber(el: *EventLoop) *Fiber { - _ = el; - return current_fiber; +fn recycle(el: *EventLoop, fiber: *Fiber) void { + std.log.debug("recyling {*}", .{fiber}); + fiber.awaiter = undefined; + @memset(fiber.resultPointer()[0..max_result_len], undefined); + el.mutex.lock(); + defer el.mutex.unlock(); + el.free.append(&fiber.queue_node); } const Regs = extern struct { @@ -101,7 +119,7 @@ const Regs = extern struct { rbp: usize, }; -const contextSwitch: *const fn (old: *Regs, new: *Regs) callconv(.c) void = @ptrCast(&contextSwitch_naked); +const contextSwitch: *const fn (old: *Regs, new: *Regs, message: usize) callconv(.c) usize = @ptrCast(&contextSwitch_naked); noinline fn contextSwitch_naked() callconv(.naked) void { asm volatile ( @@ -121,6 +139,7 @@ noinline fn contextSwitch_naked() callconv(.naked) void { \\movq 0x28(%%rsi), %%rbx \\movq 0x30(%%rsi), %%rbp \\ + \\movq %%rdx, %%rax \\ret ); } @@ -128,6 +147,7 @@ noinline fn contextSwitch_naked() callconv(.naked) void { fn popRet() callconv(.naked) void { asm volatile ( \\pop %%rdi + \\movq %%rax, %%rsi \\ret ); } @@ -145,6 +165,7 @@ pub fn @"async"( }; fiber.awaiter = null; fiber.queue_node = .{ .data = {} }; + std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward( usize, @@ -157,14 +178,16 @@ pub fn @"async"( .fiber = fiber, .start = start, }; - const stack_end_ptr: [*]align(16) usize = @alignCast(@ptrCast(closure)); - (stack_end_ptr - 1)[0] = 0; - (stack_end_ptr - 2)[0] = @intFromPtr(&AsyncClosure.call); - (stack_end_ptr - 3)[0] = @intFromPtr(closure); - (stack_end_ptr - 4)[0] = @intFromPtr(&popRet); - + const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); + const stack_top = (stack_end - 4)[0..4]; + stack_top.* = .{ + @intFromPtr(&popRet), + @intFromPtr(closure), + @intFromPtr(&AsyncClosure.call), + 0, + }; fiber.regs = .{ - .rsp = @intFromPtr(stack_end_ptr - 4), + .rsp = @intFromPtr(stack_top), .r15 = 0, .r14 = 0, .r13 = 0, @@ -181,30 +204,24 @@ const AsyncClosure = struct { _: void align(16) = {}, event_loop: *EventLoop, context: ?*anyopaque, - fiber: *EventLoop.Fiber, + fiber: *Fiber, start: *const fn (context: ?*anyopaque, result: *anyopaque) void, - fn call(closure: *AsyncClosure) callconv(.c) void { - std.log.debug("wrap called in async", .{}); + fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn { + message.handle(closure.event_loop); + std.log.debug("{*} performing async", .{closure.fiber}); closure.start(closure.context, closure.fiber.resultPointer()); - const awaiter = @atomicRmw(?*EventLoop.Fiber, &closure.fiber.awaiter, .Xchg, EventLoop.Fiber.finished, .seq_cst); - closure.event_loop.exit(awaiter); + const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + closure.event_loop.yield(awaiter, null); + 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: *EventLoop.Fiber = @alignCast(@ptrCast(any_future)); + const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); const result_src = future_fiber.resultPointer()[0..result.len]; - const my_fiber = event_loop.myFiber(); - - const prev = @atomicRmw(?*EventLoop.Fiber, &future_fiber.awaiter, .Xchg, my_fiber, .seq_cst); - if (prev == EventLoop.Fiber.finished) { - @memcpy(result, result_src); - return; - } - event_loop.yield(prev); - // Resumed when the value is available. - std.log.debug("yield returned in await", .{}); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter); @memcpy(result, result_src); + event_loop.recycle(future_fiber); } From 629a20459d315fb201404c0070cabb1917204c0e Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 27 Mar 2025 12:03:35 -0400 Subject: [PATCH 005/244] EventLoop: rewrite context switching --- lib/std/Io/EventLoop.zig | 147 +++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 2976f3386a..1837798296 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -1,4 +1,5 @@ const std = @import("../std.zig"); +const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Io = std.Io; @@ -16,7 +17,7 @@ const max_result_len = 64; const min_stack_size = 4 * 1024 * 1024; const Fiber = struct { - regs: Regs, + context: Context, awaiter: ?*Fiber, queue_node: std.DoublyLinkedList(void).Node, @@ -66,34 +67,28 @@ fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { } fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { + const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: { + el.mutex.lock(); + defer el.mutex.unlock(); + break :ready_node el.queue.pop(); + }) |ready_node| + @fieldParentPtr("queue_node", ready_node) + else if (register_awaiter) |_| // time to switch to an idle fiber? + @panic("no other fiber to switch to in order to be able to register this fiber as an awaiter") + else // nothing to do + return; const message: SwitchMessage = .{ - .ready_fiber = optional_fiber orelse if (ready_node: { - el.mutex.lock(); - defer el.mutex.unlock(); - break :ready_node el.queue.pop(); - }) |ready_node| - @fieldParentPtr("queue_node", ready_node) - else if (register_awaiter) |_| - @panic("no other fiber to switch to in order to be able to register this fiber as an awaiter") // time to switch to an idle fiber? - else - return, // nothing to do + .prev_context = ¤t_fiber.context, + .ready_context = &ready_fiber.context, .register_awaiter = register_awaiter, }; - std.log.debug("switching from {*} to {*}", .{ current_fiber, message.ready_fiber }); - SwitchMessage.handle(@ptrFromInt(contextSwitch(¤t_fiber.regs, &message.ready_fiber.regs, @intFromPtr(&message))), el); + std.log.debug("switching from {*} to {*}", .{ + @as(*Fiber, @fieldParentPtr("context", message.prev_context)), + @as(*Fiber, @fieldParentPtr("context", message.ready_context)), + }); + contextSwitch(&message).handle(el); } -const SwitchMessage = struct { - ready_fiber: *Fiber, - register_awaiter: ?*?*Fiber, - - fn handle(message: *const SwitchMessage, el: *EventLoop) void { - const prev_fiber = current_fiber; - current_fiber = message.ready_fiber; - if (message.register_awaiter) |awaiter| if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); - } -}; - fn schedule(el: *EventLoop, fiber: *Fiber) void { el.mutex.lock(); defer el.mutex.unlock(); @@ -109,47 +104,62 @@ fn recycle(el: *EventLoop, fiber: *Fiber) void { el.free.append(&fiber.queue_node); } -const Regs = extern struct { - rsp: usize, - r15: usize, - r14: usize, - r13: usize, - r12: usize, - rbx: usize, - rbp: usize, +const SwitchMessage = extern struct { + prev_context: *Context, + ready_context: *Context, + register_awaiter: ?*?*Fiber, + + fn handle(message: *const SwitchMessage, el: *EventLoop) void { + const prev_fiber: *Fiber = @fieldParentPtr("context", message.prev_context); + current_fiber = @fieldParentPtr("context", message.ready_context); + if (message.register_awaiter) |awaiter| if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); + } }; -const contextSwitch: *const fn (old: *Regs, new: *Regs, message: usize) callconv(.c) usize = @ptrCast(&contextSwitch_naked); +const Context = extern struct { + rsp: usize, + rbp: usize, + rip: usize, +}; -noinline fn contextSwitch_naked() callconv(.naked) void { - asm volatile ( - \\movq %%rsp, 0x00(%%rdi) - \\movq %%r15, 0x08(%%rdi) - \\movq %%r14, 0x10(%%rdi) - \\movq %%r13, 0x18(%%rdi) - \\movq %%r12, 0x20(%%rdi) - \\movq %%rbx, 0x28(%%rdi) - \\movq %%rbp, 0x30(%%rdi) - \\ - \\movq 0x00(%%rsi), %%rsp - \\movq 0x08(%%rsi), %%r15 - \\movq 0x10(%%rsi), %%r14 - \\movq 0x18(%%rsi), %%r13 - \\movq 0x20(%%rsi), %%r12 - \\movq 0x28(%%rsi), %%rbx - \\movq 0x30(%%rsi), %%rbp - \\ - \\movq %%rdx, %%rax - \\ret - ); +inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { + return 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 SwitchMessage), + : [message_to_send] "{rsi}" (message), + : "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 popRet() callconv(.naked) void { - asm volatile ( - \\pop %%rdi - \\movq %%rax, %%rsi - \\ret - ); +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"( @@ -179,21 +189,10 @@ pub fn @"async"( .start = start, }; const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); - const stack_top = (stack_end - 4)[0..4]; - stack_top.* = .{ - @intFromPtr(&popRet), - @intFromPtr(closure), - @intFromPtr(&AsyncClosure.call), - 0, - }; - fiber.regs = .{ - .rsp = @intFromPtr(stack_top), - .r15 = 0, - .r14 = 0, - .r13 = 0, - .r12 = 0, - .rbx = 0, + fiber.context = .{ + .rsp = @intFromPtr(stack_end - 1), .rbp = 0, + .rip = @intFromPtr(&fiberEntry), }; event_loop.schedule(fiber); From 9d0f44f08a2a302c6e98ed668d92353fba4c0720 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 27 Mar 2025 17:19:53 -0400 Subject: [PATCH 006/244] EventLoop: add threads --- lib/std/Io/EventLoop.zig | 98 ++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 20 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 1837798296..1fda9cb3d1 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -7,15 +7,25 @@ const EventLoop = @This(); gpa: Allocator, mutex: std.Thread.Mutex, +cond: std.Thread.Condition, queue: std.DoublyLinkedList(void), free: std.DoublyLinkedList(void), main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), +exiting: bool, +idle_count: usize, +threads: std.ArrayListUnmanaged(Thread), +threadlocal var current_thread: *Thread = undefined; threadlocal var current_fiber: *Fiber = undefined; const max_result_len = 64; const min_stack_size = 4 * 1024 * 1024; +const Thread = struct { + thread: std.Thread, + idle_fiber: Fiber, +}; + const Fiber = struct { context: Context, awaiter: ?*Fiber, @@ -23,32 +33,58 @@ const Fiber = struct { const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); - fn resultPointer(f: *Fiber) [*]u8 { - const base: [*]u8 = @ptrCast(f); - return base + @sizeOf(Fiber); - } - - fn stackEndPointer(f: *Fiber) [*]u8 { - const base: [*]u8 = @ptrCast(f); - return base + std.mem.alignForward( + fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { + const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); + return base[0..std.mem.alignForward( usize, @sizeOf(Fiber) + max_result_len + min_stack_size, std.heap.page_size_max, - ); + )]; + } + + fn resultSlice(f: *Fiber) []u8 { + const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); + return base[@sizeOf(Fiber)..][0..max_result_len]; + } + + fn stackEndPointer(f: *Fiber) [*]u8 { + const allocated_slice = f.allocatedSlice(); + return allocated_slice[allocated_slice.len..].ptr; } }; -pub fn init(el: *EventLoop, gpa: Allocator) void { +pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { el.* = .{ .gpa = gpa, .mutex = .{}, + .cond = .{}, .queue = .{}, .free = .{}, .main_fiber_buffer = undefined, + .exiting = false, + .idle_count = 0, + .threads = try .initCapacity(gpa, @max(std.Thread.getCpuCount() catch 1, 1)), }; + current_thread = el.threads.addOneAssumeCapacity(); current_fiber = @ptrCast(&el.main_fiber_buffer); } +pub fn deinit(el: *EventLoop) void { + { + el.mutex.lock(); + defer el.mutex.unlock(); + assert(el.queue.len == 0); // pending async + el.exiting = true; + } + el.cond.broadcast(); + while (el.free.pop()) |free_node| { + const free_fiber: *Fiber = @fieldParentPtr("queue_node", free_node); + el.gpa.free(free_fiber.allocatedSlice()); + } + for (el.threads.items[1..]) |*thread| thread.thread.join(); + el.threads.deinit(el.gpa); +} + fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { assert(result_len <= max_result_len); const free_node = free_node: { @@ -73,10 +109,8 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v break :ready_node el.queue.pop(); }) |ready_node| @fieldParentPtr("queue_node", ready_node) - else if (register_awaiter) |_| // time to switch to an idle fiber? - @panic("no other fiber to switch to in order to be able to register this fiber as an awaiter") - else // nothing to do - return; + else + ¤t_thread.idle_fiber; const message: SwitchMessage = .{ .prev_context = ¤t_fiber.context, .ready_context = &ready_fiber.context, @@ -90,20 +124,44 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v } fn schedule(el: *EventLoop, fiber: *Fiber) void { - el.mutex.lock(); - defer el.mutex.unlock(); - el.queue.append(&fiber.queue_node); + signal: { + el.mutex.lock(); + defer el.mutex.unlock(); + el.queue.append(&fiber.queue_node); + if (el.idle_count > 0) break :signal; + if (el.threads.items.len == el.threads.capacity) return; + const thread = el.threads.addOneAssumeCapacity(); + thread.thread = std.Thread.spawn(.{ + .stack_size = min_stack_size, + .allocator = el.gpa, + }, threadEntry, .{ el, thread }) catch return; + } + el.cond.signal(); } fn recycle(el: *EventLoop, fiber: *Fiber) void { std.log.debug("recyling {*}", .{fiber}); fiber.awaiter = undefined; - @memset(fiber.resultPointer()[0..max_result_len], undefined); + @memset(fiber.resultSlice(), undefined); el.mutex.lock(); defer el.mutex.unlock(); el.free.append(&fiber.queue_node); } +fn threadEntry(el: *EventLoop, thread: *Thread) void { + current_thread = thread; + current_fiber = &thread.idle_fiber; + while (true) { + el.yield(null, null); + el.mutex.lock(); + defer el.mutex.unlock(); + if (el.exiting) return; + el.idle_count += 1; + defer el.idle_count -= 1; + el.cond.wait(&el.mutex); + } +} + const SwitchMessage = extern struct { prev_context: *Context, ready_context: *Context, @@ -209,7 +267,7 @@ const AsyncClosure = struct { fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn { message.handle(closure.event_loop); std.log.debug("{*} performing async", .{closure.fiber}); - closure.start(closure.context, closure.fiber.resultPointer()); + closure.start(closure.context, closure.fiber.resultSlice().ptr); const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); closure.event_loop.yield(awaiter, null); unreachable; // switched to dead fiber @@ -219,7 +277,7 @@ const AsyncClosure = struct { 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)); - const result_src = future_fiber.resultPointer()[0..result.len]; + const result_src = future_fiber.resultSlice()[0..result.len]; if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter); @memcpy(result, result_src); event_loop.recycle(future_fiber); From f1dd06b01fc30c54f5605d011b8d33f535857f7a Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Thu, 27 Mar 2025 19:32:26 -0400 Subject: [PATCH 007/244] EventLoop: implement main idle fiber --- lib/std/Io/EventLoop.zig | 145 +++++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 50 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 1fda9cb3d1..7ad43fd6a3 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -11,19 +11,21 @@ cond: std.Thread.Condition, queue: std.DoublyLinkedList(void), free: std.DoublyLinkedList(void), main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), -exiting: bool, +exit_awaiter: ?*Fiber, idle_count: usize, threads: std.ArrayListUnmanaged(Thread), -threadlocal var current_thread: *Thread = undefined; -threadlocal var current_fiber: *Fiber = undefined; +threadlocal var current_idle_context: *Context = undefined; +threadlocal var current_fiber_context: *Context = undefined; const max_result_len = 64; const min_stack_size = 4 * 1024 * 1024; +const idle_stack_size = 32 * 1024; +const stack_align = 16; const Thread = struct { thread: std.Thread, - idle_fiber: Fiber, + idle_context: Context, }; const Fiber = struct { @@ -54,6 +56,11 @@ const Fiber = struct { }; pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { + const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread); + const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); + const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); + const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context), stack_align), idle_stack_end_offset); + errdefer gpa.free(allocated_slice); el.* = .{ .gpa = gpa, .mutex = .{}, @@ -61,28 +68,37 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { .queue = .{}, .free = .{}, .main_fiber_buffer = undefined, - .exiting = false, + .exit_awaiter = null, .idle_count = 0, - .threads = try .initCapacity(gpa, @max(std.Thread.getCpuCount() catch 1, 1)), + .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])), }; - current_thread = el.threads.addOneAssumeCapacity(); - current_fiber = @ptrCast(&el.main_fiber_buffer); + const main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)])); + const idle_stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); + (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; + main_idle_context.* = .{ + .rsp = @intFromPtr(idle_stack_end - 1), + .rbp = 0, + .rip = @intFromPtr(&mainIdleEntry), + }; + std.log.debug("created main idle {*}", .{main_idle_context}); + current_idle_context = main_idle_context; + const current_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); + std.log.debug("created main fiber {*}", .{current_fiber}); + current_fiber_context = ¤t_fiber.context; } pub fn deinit(el: *EventLoop) void { - { - el.mutex.lock(); - defer el.mutex.unlock(); - assert(el.queue.len == 0); // pending async - el.exiting = true; - } - el.cond.broadcast(); + assert(el.queue.len == 0); // pending async + el.yield(null, &el.exit_awaiter); while (el.free.pop()) |free_node| { const free_fiber: *Fiber = @fieldParentPtr("queue_node", free_node); el.gpa.free(free_fiber.allocatedSlice()); } - for (el.threads.items[1..]) |*thread| thread.thread.join(); - el.threads.deinit(el.gpa); + const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context)); + const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); + const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context), stack_align)) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); + for (el.threads.items) |*thread| thread.thread.join(); + el.gpa.free(allocated_ptr[0..idle_stack_end]); } fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { @@ -103,40 +119,44 @@ fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { } fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { - const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: { - el.mutex.lock(); - defer el.mutex.unlock(); - break :ready_node el.queue.pop(); - }) |ready_node| - @fieldParentPtr("queue_node", ready_node) - else - ¤t_thread.idle_fiber; + const ready_context: *Context = ready_context: { + const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: { + el.mutex.lock(); + defer el.mutex.unlock(); + break :ready_node el.queue.pop(); + }) |ready_node| + @fieldParentPtr("queue_node", ready_node) + else + break :ready_context current_idle_context; + break :ready_context &ready_fiber.context; + }; const message: SwitchMessage = .{ - .prev_context = ¤t_fiber.context, - .ready_context = &ready_fiber.context, + .prev_context = current_fiber_context, + .ready_context = ready_context, .register_awaiter = register_awaiter, }; - std.log.debug("switching from {*} to {*}", .{ - @as(*Fiber, @fieldParentPtr("context", message.prev_context)), - @as(*Fiber, @fieldParentPtr("context", message.ready_context)), - }); + std.log.debug("switching from {*} to {*}", .{ message.prev_context, message.ready_context }); contextSwitch(&message).handle(el); } fn schedule(el: *EventLoop, fiber: *Fiber) void { - signal: { - el.mutex.lock(); - defer el.mutex.unlock(); - el.queue.append(&fiber.queue_node); - if (el.idle_count > 0) break :signal; - if (el.threads.items.len == el.threads.capacity) return; - const thread = el.threads.addOneAssumeCapacity(); - thread.thread = std.Thread.spawn(.{ - .stack_size = min_stack_size, - .allocator = el.gpa, - }, threadEntry, .{ el, thread }) catch return; + el.mutex.lock(); + el.queue.append(&fiber.queue_node); + if (el.idle_count > 0) { + el.mutex.unlock(); + el.cond.signal(); + return; } - el.cond.signal(); + defer el.mutex.unlock(); + 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, thread }) catch { + el.threads.items.len -= 1; + return; + }; } fn recycle(el: *EventLoop, fiber: *Fiber) void { @@ -148,14 +168,28 @@ fn recycle(el: *EventLoop, fiber: *Fiber) void { el.free.append(&fiber.queue_node); } +fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn { + message.handle(el); + el.yield(el.idle(), null); + unreachable; // switched to dead fiber +} + fn threadEntry(el: *EventLoop, thread: *Thread) void { - current_thread = thread; - current_fiber = &thread.idle_fiber; + std.log.debug("created thread idle {*}", .{&thread.idle_context}); + current_idle_context = &thread.idle_context; + current_fiber_context = &thread.idle_context; + _ = el.idle(); +} + +fn idle(el: *EventLoop) *Fiber { while (true) { el.yield(null, null); + if (@atomicLoad(?*Fiber, &el.exit_awaiter, .acquire)) |exit_awaiter| { + el.cond.broadcast(); + return exit_awaiter; + } el.mutex.lock(); defer el.mutex.unlock(); - if (el.exiting) return; el.idle_count += 1; defer el.idle_count -= 1; el.cond.wait(&el.mutex); @@ -169,7 +203,7 @@ const SwitchMessage = extern struct { fn handle(message: *const SwitchMessage, el: *EventLoop) void { const prev_fiber: *Fiber = @fieldParentPtr("context", message.prev_context); - current_fiber = @fieldParentPtr("context", message.ready_context); + current_fiber_context = message.ready_context; if (message.register_awaiter) |awaiter| if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); } }; @@ -208,6 +242,18 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { }; } +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 ( @@ -238,7 +284,7 @@ pub fn @"async"( const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward( usize, @intFromPtr(fiber.stackEndPointer() - @sizeOf(AsyncClosure)), - @alignOf(AsyncClosure), + @max(@alignOf(AsyncClosure), stack_align), )); closure.* = .{ .event_loop = event_loop, @@ -246,7 +292,7 @@ pub fn @"async"( .fiber = fiber, .start = start, }; - const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); + const stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(closure)); fiber.context = .{ .rsp = @intFromPtr(stack_end - 1), .rbp = 0, @@ -258,7 +304,6 @@ pub fn @"async"( } const AsyncClosure = struct { - _: void align(16) = {}, event_loop: *EventLoop, context: ?*anyopaque, fiber: *Fiber, From 31ed2d67151b4fcb43e29526bb0ecbcb16cc6db0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 27 Mar 2025 20:53:14 -0700 Subject: [PATCH 008/244] fix context passing in threaded Io impl --- lib/std/Io.zig | 24 +++++++++++------ lib/std/Io/EventLoop.zig | 11 ++++++++ lib/std/Thread/Pool.zig | 58 +++++++++++++++++++++++++++++++--------- 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index bf3151852f..daa4c0275f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -570,10 +570,12 @@ pub const VTable = struct { /// The pointer of this slice is an "eager" result value. /// The length is the size in bytes of the result type. /// This pointer's lifetime expires directly after the call to this function. - eager_result: []u8, - /// Passed to `start`. - context: ?*anyopaque, - start: *const fn (context: ?*anyopaque, result: *anyopaque) void, + result: []u8, + result_alignment: std.mem.Alignment, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*AnyFuture, /// This function is only called when `async` returns a non-null value. @@ -611,17 +613,23 @@ pub fn Future(Result: type) type { /// } /// ``` /// where `Result` is any type. -pub fn async(io: Io, s: anytype) Future(@typeInfo(@TypeOf(@TypeOf(s).start)).@"fn".return_type.?) { - const S = @TypeOf(s); +pub fn async(io: Io, S: type, s: S) Future(@typeInfo(@TypeOf(S.start)).@"fn".return_type.?) { const Result = @typeInfo(@TypeOf(S.start)).@"fn".return_type.?; const TypeErased = struct { - fn start(context: ?*anyopaque, result: *anyopaque) void { + fn start(context: *const anyopaque, result: *anyopaque) void { const context_casted: *const S = @alignCast(@ptrCast(context)); const result_casted: *Result = @ptrCast(@alignCast(result)); result_casted.* = S.start(context_casted.*); } }; var future: Future(Result) = undefined; - future.any_future = io.vtable.async(io.userdata, @ptrCast((&future.result)[0..1]), @constCast(&s), TypeErased.start); + future.any_future = io.vtable.async( + io.userdata, + @ptrCast((&future.result)[0..1]), + .fromByteUnits(@alignOf(Result)), + if (@sizeOf(S) == 0) &.{} else @ptrCast((&s)[0..1]), // work around compiler bug + .fromByteUnits(@alignOf(S)), + TypeErased.start, + ); return future; } diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 7ad43fd6a3..802089cc55 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -55,6 +55,17 @@ const Fiber = struct { } }; +pub fn io(el: *EventLoop) Io { + return .{ + .userdata = el, + .vtable = &.{ + .@"async" = @"async", + .@"await" = @"await", + }, + }; +} + + pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread); const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index f1d2a7338f..e19166993a 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -309,46 +309,80 @@ pub fn getIdCount(pool: *Pool) usize { return @intCast(1 + pool.threads.len); } +pub fn io(pool: *Pool) std.Io { + return .{ + .userdata = pool, + .vtable = &.{ + .@"async" = @"async", + .@"await" = @"await", + }, + }; +} + const AsyncClosure = struct { - func: *const fn (context: ?*anyopaque, result: *anyopaque) void, - context: ?*anyopaque, + func: *const fn (context: *anyopaque, result: *anyopaque) void, run_node: std.Thread.Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, reset_event: std.Thread.ResetEvent, + context_offset: usize, + result_offset: usize, fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { const run_node: *std.Thread.Pool.RunQueue.Node = @fieldParentPtr("data", runnable); - const closure: *@This() = @alignCast(@fieldParentPtr("run_node", run_node)); - closure.func(closure.context, closure.resultPointer()); + const closure: *AsyncClosure = @alignCast(@fieldParentPtr("run_node", run_node)); + closure.func(closure.contextPointer(), closure.resultPointer()); closure.reset_event.set(); } - fn resultPointer(closure: *@This()) [*]u8 { + fn contextOffset(context_alignment: std.mem.Alignment) usize { + return context_alignment.forward(@sizeOf(AsyncClosure)); + } + + fn resultOffset( + context_alignment: std.mem.Alignment, + context_len: usize, + result_alignment: std.mem.Alignment, + ) usize { + return result_alignment.forward(contextOffset(context_alignment) + context_len); + } + + fn resultPointer(closure: *AsyncClosure) [*]u8 { const base: [*]u8 = @ptrCast(closure); - return base + @sizeOf(@This()); + return base + closure.result_offset; + } + + fn contextPointer(closure: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(closure); + return base + closure.context_offset; } }; pub fn @"async"( userdata: ?*anyopaque, - eager_result: []u8, - context: ?*anyopaque, - start: *const fn (context: ?*anyopaque, result: *anyopaque) void, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*std.Io.AnyFuture { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); pool.mutex.lock(); const gpa = pool.allocator; - const n = @sizeOf(AsyncClosure) + eager_result.len; + const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); + const result_offset = result_alignment.forward(context_offset + context.len); + const n = result_offset + result.len; const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, @alignOf(AsyncClosure), n) catch { pool.mutex.unlock(); - start(context, eager_result.ptr); + start(context.ptr, result.ptr); return null; })); closure.* = .{ .func = start, - .context = context, + .context_offset = context_offset, + .result_offset = result_offset, .reset_event = .{}, }; + @memcpy(closure.contextPointer()[0..context.len], context); pool.run_queue.prepend(&closure.run_node); pool.mutex.unlock(); From 4c7c0c41784ac442590dd4838faaecc3344693b9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 27 Mar 2025 23:59:35 -0700 Subject: [PATCH 009/244] update threaded fibers impl to actually storing args sorry, something still not working correctly --- lib/std/Io/EventLoop.zig | 60 ++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 802089cc55..fbab98ff00 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -18,7 +18,11 @@ threads: std.ArrayListUnmanaged(Thread), threadlocal var current_idle_context: *Context = undefined; threadlocal var current_fiber_context: *Context = undefined; +/// Also used for context. const max_result_len = 64; +/// Also used for context. +const max_result_align: std.mem.Alignment = .@"16"; + const min_stack_size = 4 * 1024 * 1024; const idle_stack_size = 32 * 1024; const stack_align = 16; @@ -29,6 +33,8 @@ const Thread = struct { }; const Fiber = struct { + _: void align(max_result_align.toByteUnits()) = {}, + context: Context, awaiter: ?*Fiber, queue_node: std.DoublyLinkedList(void).Node, @@ -39,14 +45,27 @@ const Fiber = struct { const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); return base[0..std.mem.alignForward( usize, - @sizeOf(Fiber) + max_result_len + min_stack_size, + resultOffset() + max_result_len + min_stack_size, std.heap.page_size_max, )]; } + fn argsOffset() usize { + return max_result_align.forward(@sizeOf(Fiber)); + } + + fn resultOffset() usize { + return max_result_align.forward(argsOffset() + max_result_len); + } + + fn argsSlice(f: *Fiber) []u8 { + const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); + return base[argsOffset()..][0..max_result_len]; + } + fn resultSlice(f: *Fiber) []u8 { const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); - return base[@sizeOf(Fiber)..][0..max_result_len]; + return base[resultOffset()..][0..max_result_len]; } fn stackEndPointer(f: *Fiber) [*]u8 { @@ -102,7 +121,7 @@ pub fn deinit(el: *EventLoop) void { assert(el.queue.len == 0); // pending async el.yield(null, &el.exit_awaiter); while (el.free.pop()) |free_node| { - const free_fiber: *Fiber = @fieldParentPtr("queue_node", free_node); + const free_fiber: *Fiber = @alignCast(@fieldParentPtr("queue_node", free_node)); el.gpa.free(free_fiber.allocatedSlice()); } const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context)); @@ -112,8 +131,7 @@ pub fn deinit(el: *EventLoop) void { el.gpa.free(allocated_ptr[0..idle_stack_end]); } -fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { - assert(result_len <= max_result_len); +fn allocateFiber(el: *EventLoop) error{OutOfMemory}!*Fiber { const free_node = free_node: { el.mutex.lock(); defer el.mutex.unlock(); @@ -121,12 +139,12 @@ fn allocateFiber(el: *EventLoop, result_len: usize) error{OutOfMemory}!*Fiber { } orelse { const n = std.mem.alignForward( usize, - @sizeOf(Fiber) + max_result_len + min_stack_size, + Fiber.resultOffset() + max_result_len + min_stack_size, std.heap.page_size_max, ); return @alignCast(@ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), n))); }; - return @fieldParentPtr("queue_node", free_node); + return @alignCast(@fieldParentPtr("queue_node", free_node)); } fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { @@ -136,7 +154,7 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v defer el.mutex.unlock(); break :ready_node el.queue.pop(); }) |ready_node| - @fieldParentPtr("queue_node", ready_node) + @alignCast(@fieldParentPtr("queue_node", ready_node)) else break :ready_context current_idle_context; break :ready_context &ready_fiber.context; @@ -213,7 +231,7 @@ const SwitchMessage = extern struct { register_awaiter: ?*?*Fiber, fn handle(message: *const SwitchMessage, el: *EventLoop) void { - const prev_fiber: *Fiber = @fieldParentPtr("context", message.prev_context); + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); current_fiber_context = message.ready_context; if (message.register_awaiter) |awaiter| if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); } @@ -279,17 +297,25 @@ fn fiberEntry() callconv(.naked) void { pub fn @"async"( userdata: ?*anyopaque, - eager_result: []u8, - context: ?*anyopaque, - start: *const fn (context: ?*anyopaque, result: *anyopaque) void, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*std.Io.AnyFuture { + assert(result_alignment.compare(.lte, max_result_align)); // TODO + assert(context_alignment.compare(.lte, max_result_align)); // TODO + assert(result.len <= max_result_len); // TODO + assert(context.len <= max_result_len); // TODO + const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); - const fiber = event_loop.allocateFiber(eager_result.len) catch { - start(context, eager_result.ptr); + const fiber = event_loop.allocateFiber() catch { + start(context.ptr, result.ptr); return null; }; fiber.awaiter = null; fiber.queue_node = .{ .data = {} }; + @memcpy(fiber.argsSlice()[0..context.len], context); std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward( @@ -299,7 +325,6 @@ pub fn @"async"( )); closure.* = .{ .event_loop = event_loop, - .context = context, .fiber = fiber, .start = start, }; @@ -316,14 +341,13 @@ pub fn @"async"( const AsyncClosure = struct { event_loop: *EventLoop, - context: ?*anyopaque, fiber: *Fiber, - start: *const fn (context: ?*anyopaque, result: *anyopaque) void, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn { message.handle(closure.event_loop); std.log.debug("{*} performing async", .{closure.fiber}); - closure.start(closure.context, closure.fiber.resultSlice().ptr); + closure.start(closure.fiber.argsSlice().ptr, closure.fiber.resultSlice().ptr); const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); closure.event_loop.yield(awaiter, null); unreachable; // switched to dead fiber From 29355ff21c44e2c2f3ba7be019ceb2bdbfb7fc6f Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 28 Mar 2025 09:13:07 -0400 Subject: [PATCH 010/244] EventLoop: fix incorrect alignment panic When the previous fiber did not request to be registered as an awaiter, it may not have actually been a full blown `Fiber`, so only create the `Fiber` pointer when needed. --- lib/std/Io/EventLoop.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index fbab98ff00..065ddfa55f 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -84,7 +84,6 @@ pub fn io(el: *EventLoop) Io { }; } - pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread); const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); @@ -231,9 +230,11 @@ const SwitchMessage = extern struct { register_awaiter: ?*?*Fiber, fn handle(message: *const SwitchMessage, el: *EventLoop) void { - const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); current_fiber_context = message.ready_context; - if (message.register_awaiter) |awaiter| if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); + if (message.register_awaiter) |awaiter| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); + if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); + } } }; From 1493c3b5f3cedff8617380317aba71785ed3f10f Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Fri, 28 Mar 2025 10:05:52 -0400 Subject: [PATCH 011/244] EventLoop: move context after the async closure This avoids needing to store more sizes and alignments. Only the result alignment needs to be stored, because `Fiber` is at a fixed zero offset. --- lib/std/Io/EventLoop.zig | 178 +++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 92 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 065ddfa55f..84ea623954 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -4,28 +4,23 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Io = std.Io; const EventLoop = @This(); +const Alignment = std.mem.Alignment; gpa: Allocator, mutex: std.Thread.Mutex, cond: std.Thread.Condition, queue: std.DoublyLinkedList(void), free: std.DoublyLinkedList(void), -main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), +main_context: Context, exit_awaiter: ?*Fiber, idle_count: usize, threads: std.ArrayListUnmanaged(Thread), threadlocal var current_idle_context: *Context = undefined; -threadlocal var current_fiber_context: *Context = undefined; +threadlocal var current_context: *Context = undefined; -/// Also used for context. -const max_result_len = 64; -/// Also used for context. -const max_result_align: std.mem.Alignment = .@"16"; - -const min_stack_size = 4 * 1024 * 1024; +/// Empirically saw 10KB being used by the self-hosted backend for logging. const idle_stack_size = 32 * 1024; -const stack_align = 16; const Thread = struct { thread: std.Thread, @@ -33,45 +28,53 @@ const Thread = struct { }; const Fiber = struct { - _: void align(max_result_align.toByteUnits()) = {}, - context: Context, awaiter: ?*Fiber, queue_node: std.DoublyLinkedList(void).Node, + result_align: Alignment, const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); - fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { - const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); - return base[0..std.mem.alignForward( + 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, - resultOffset() + max_result_len + min_stack_size, - std.heap.page_size_max, - )]; + 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 argsOffset() usize { - return max_result_align.forward(@sizeOf(Fiber)); + fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { + return @as([*]align(@alignOf(Fiber)) u8, @ptrCast(f))[0..allocation_size]; } - fn resultOffset() usize { - return max_result_align.forward(argsOffset() + max_result_len); - } - - fn argsSlice(f: *Fiber) []u8 { - const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); - return base[argsOffset()..][0..max_result_len]; - } - - fn resultSlice(f: *Fiber) []u8 { - const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); - return base[resultOffset()..][0..max_result_len]; - } - - fn stackEndPointer(f: *Fiber) [*]u8 { + 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 { @@ -88,7 +91,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread); const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); - const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context), stack_align), idle_stack_end_offset); + const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context)), idle_stack_end_offset); errdefer gpa.free(allocated_slice); el.* = .{ .gpa = gpa, @@ -96,13 +99,13 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { .cond = .{}, .queue = .{}, .free = .{}, - .main_fiber_buffer = undefined, + .main_context = undefined, .exit_awaiter = null, .idle_count = 0, .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])), }; const main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)])); - const idle_stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); + const idle_stack_end: [*]align(@max(@alignOf(Thread), @alignOf(Context))) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; main_idle_context.* = .{ .rsp = @intFromPtr(idle_stack_end - 1), @@ -111,9 +114,8 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { }; std.log.debug("created main idle {*}", .{main_idle_context}); current_idle_context = main_idle_context; - const current_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); - std.log.debug("created main fiber {*}", .{current_fiber}); - current_fiber_context = ¤t_fiber.context; + std.log.debug("created main {*}", .{&el.main_context}); + current_context = &el.main_context; } pub fn deinit(el: *EventLoop) void { @@ -125,27 +127,11 @@ pub fn deinit(el: *EventLoop) void { } const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context)); const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); - const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context), stack_align)) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); + const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context))) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); for (el.threads.items) |*thread| thread.thread.join(); el.gpa.free(allocated_ptr[0..idle_stack_end]); } -fn allocateFiber(el: *EventLoop) error{OutOfMemory}!*Fiber { - const free_node = free_node: { - el.mutex.lock(); - defer el.mutex.unlock(); - break :free_node el.free.pop(); - } orelse { - const n = std.mem.alignForward( - usize, - Fiber.resultOffset() + max_result_len + min_stack_size, - std.heap.page_size_max, - ); - return @alignCast(@ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), n))); - }; - return @alignCast(@fieldParentPtr("queue_node", free_node)); -} - fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { const ready_context: *Context = ready_context: { const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: { @@ -159,7 +145,7 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v break :ready_context &ready_fiber.context; }; const message: SwitchMessage = .{ - .prev_context = current_fiber_context, + .prev_context = current_context, .ready_context = ready_context, .register_awaiter = register_awaiter, }; @@ -189,14 +175,13 @@ fn schedule(el: *EventLoop, fiber: *Fiber) void { fn recycle(el: *EventLoop, fiber: *Fiber) void { std.log.debug("recyling {*}", .{fiber}); - fiber.awaiter = undefined; - @memset(fiber.resultSlice(), undefined); + @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(.c) noreturn { +fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { message.handle(el); el.yield(el.idle(), null); unreachable; // switched to dead fiber @@ -205,7 +190,7 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn fn threadEntry(el: *EventLoop, thread: *Thread) void { std.log.debug("created thread idle {*}", .{&thread.idle_context}); current_idle_context = &thread.idle_context; - current_fiber_context = &thread.idle_context; + current_context = &thread.idle_context; _ = el.idle(); } @@ -230,7 +215,7 @@ const SwitchMessage = extern struct { register_awaiter: ?*?*Fiber, fn handle(message: *const SwitchMessage, el: *EventLoop) void { - current_fiber_context = message.ready_context; + current_context = message.ready_context; if (message.register_awaiter) |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); @@ -238,10 +223,13 @@ const SwitchMessage = extern struct { } }; -const Context = extern struct { - rsp: usize, - rbp: usize, - rip: usize, +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 { @@ -299,42 +287,45 @@ fn fiberEntry() callconv(.naked) void { pub fn @"async"( userdata: ?*anyopaque, result: []u8, - result_alignment: std.mem.Alignment, + result_alignment: Alignment, context: []const u8, - context_alignment: std.mem.Alignment, + context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*std.Io.AnyFuture { - assert(result_alignment.compare(.lte, max_result_align)); // TODO - assert(context_alignment.compare(.lte, max_result_align)); // TODO - assert(result.len <= max_result_len); // TODO - assert(context.len <= max_result_len); // TODO + 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 = event_loop.allocateFiber() catch { + const fiber = Fiber.allocate(event_loop) catch { start(context.ptr, result.ptr); return null; }; - fiber.awaiter = null; - fiber.queue_node = .{ .data = {} }; - @memcpy(fiber.argsSlice()[0..context.len], context); std.log.debug("allocated {*}", .{fiber}); - const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward( - usize, - @intFromPtr(fiber.stackEndPointer() - @sizeOf(AsyncClosure)), - @max(@alignOf(AsyncClosure), stack_align), - )); + 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, }; - const stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(closure)); - fiber.context = .{ - .rsp = @intFromPtr(stack_end - 1), - .rbp = 0, - .rip = @intFromPtr(&fiberEntry), - }; + @memcpy(closure.contextPointer(), context); event_loop.schedule(fiber); return @ptrCast(fiber); @@ -345,10 +336,14 @@ const AsyncClosure = struct { fiber: *Fiber, start: *const fn (context: *const anyopaque, result: *anyopaque) void, - fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn { + 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.fiber.argsSlice().ptr, closure.fiber.resultSlice().ptr); + closure.start(closure.contextPointer(), closure.fiber.resultPointer()); const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); closure.event_loop.yield(awaiter, null); unreachable; // switched to dead fiber @@ -358,8 +353,7 @@ const AsyncClosure = struct { 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)); - const result_src = future_fiber.resultSlice()[0..result.len]; if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter); - @memcpy(result, result_src); + @memcpy(result, future_fiber.resultPointer()); event_loop.recycle(future_fiber); } From 08bb7c6c889594f2a1694c5bdf0f4648be4a8d66 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 28 Mar 2025 17:20:35 -0700 Subject: [PATCH 012/244] free freeing wrong amount in thread pool impl --- lib/std/Thread/Pool.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index e19166993a..d2b28492c6 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -396,6 +396,6 @@ pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: [] const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); closure.reset_event.wait(); const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); - @memcpy(result, (base + @sizeOf(AsyncClosure))[0..result.len]); - thread_pool.allocator.free(base[0 .. @sizeOf(AsyncClosure) + result.len]); + @memcpy(result, closure.resultPointer()[0..result.len]); + thread_pool.allocator.free(base[0 .. closure.result_offset + result.len]); } From 66b0f7e92b64538de7f9d9f37a4ef1c25f0f6e5b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 28 Mar 2025 18:23:49 -0700 Subject: [PATCH 013/244] start adding fs functions to std.Io --- lib/std/Io.zig | 44 +++++++++++++++++++++++++++++++++++++++++ lib/std/Thread/Pool.zig | 30 ++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index daa4c0275f..142e3d4dc4 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -555,6 +555,7 @@ test { } const Io = @This(); +const fs = std.fs; pub const EventLoop = @import("Io/EventLoop.zig"); @@ -588,6 +589,12 @@ pub const VTable = struct { /// The length is equal to size in bytes of result type. result: []u8, ) void, + + createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) fs.File.OpenError!fs.File, + openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) fs.File.OpenError!fs.File, + closeFile: *const fn (?*anyopaque, fs.File) void, + read: *const fn (?*anyopaque, file: fs.File, buffer: []u8) fs.File.ReadError!usize, + write: *const fn (?*anyopaque, file: fs.File, buffer: []const u8) fs.File.WriteError!usize, }; pub const AnyFuture = opaque {}; @@ -633,3 +640,40 @@ pub fn async(io: Io, S: type, s: S) Future(@typeInfo(@TypeOf(S.start)).@"fn".ret ); return future; } + +pub fn openFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) fs.File.OpenError!fs.File { + return io.vtable.openFile(io.userdata, dir, sub_path, flags); +} + +pub fn createFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) fs.File.OpenError!fs.File { + return io.vtable.createFile(io.userdata, dir, sub_path, flags); +} + +pub fn closeFile(io: Io, file: fs.File) void { + return io.vtable.closeFile(io.userdata, file); +} + +pub fn read(io: Io, file: fs.File, buffer: []u8) fs.File.ReadError!usize { + return io.vtable.read(io.userdata, file, buffer); +} + +pub fn write(io: Io, file: fs.File, buffer: []const u8) fs.File.WriteError!usize { + return io.vtable.write(io.userdata, file, buffer); +} + +pub fn writeAll(io: Io, file: fs.File, bytes: []const u8) fs.File.WriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try io.write(file, bytes[index..]); + } +} + +pub fn readAll(io: Io, file: fs.File, buffer: []u8) fs.File.ReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try io.read(file, buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; +} diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index d2b28492c6..486d067330 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -315,6 +315,11 @@ pub fn io(pool: *Pool) std.Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .createFile = createFile, + .openFile = openFile, + .closeFile = closeFile, + .read = read, + .write = write, }, }; } @@ -399,3 +404,28 @@ pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: [] @memcpy(result, closure.resultPointer()[0..result.len]); thread_pool.allocator.free(base[0 .. closure.result_offset + result.len]); } + +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 { + _ = userdata; + return dir.createFile(sub_path, flags); +} + +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 { + _ = userdata; + return dir.openFile(sub_path, flags); +} + +pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { + _ = userdata; + return file.close(); +} + +pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) std.fs.File.ReadError!usize { + _ = userdata; + return file.read(buffer); +} + +pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) std.fs.File.WriteError!usize { + _ = userdata; + return file.write(buffer); +} From 238de05d2ca629237e434ca0d577dbd0b8ed0c9a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 28 Mar 2025 21:07:12 -0700 Subject: [PATCH 014/244] WIP --- lib/std/Io/EventLoop.zig | 241 ++++++++++++++++++++++++++++++++++----- 1 file changed, 213 insertions(+), 28 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 84ea623954..ca42627005 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator; const Io = std.Io; const EventLoop = @This(); const Alignment = std.mem.Alignment; +const IoUring = std.os.linux.IoUring; gpa: Allocator, mutex: std.Thread.Mutex, @@ -13,18 +14,27 @@ queue: std.DoublyLinkedList(void), free: std.DoublyLinkedList(void), main_context: Context, exit_awaiter: ?*Fiber, -idle_count: usize, threads: std.ArrayListUnmanaged(Thread), +/// 1 bit per thread, same order as `thread_index`. +idle_iourings: []usize, -threadlocal var current_idle_context: *Context = undefined; -threadlocal var current_context: *Context = undefined; +threadlocal var thread_index: u32 = undefined; /// Empirically saw 10KB being used by the self-hosted backend for logging. const idle_stack_size = 32 * 1024; +const io_uring_entries = 64; + const Thread = struct { thread: std.Thread, idle_context: Context, + current_idle_context: *Context, + current_context: *Context, + io_uring: IoUring, + + fn currentFiber(thread: *Thread) *Fiber { + return @fieldParentPtr("context", thread.current_context); + } }; const Fiber = struct { @@ -83,16 +93,25 @@ pub fn io(el: *EventLoop) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .createFile = createFile, + .openFile = openFile, + .closeFile = closeFile, + .read = read, + .write = write, }, }; } -pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { - const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread); +pub fn init(el: *EventLoop, gpa: Allocator) !void { + const n_threads: usize = @max((std.Thread.getCpuCount() catch 1), 1); + const threads_bytes = n_threads * @sizeOf(Thread); const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context)), idle_stack_end_offset); errdefer gpa.free(allocated_slice); + const idle_iourings = try gpa.alloc(usize, (n_threads + @bitSizeOf(usize) - 1) / @bitSizeOf(usize)); + errdefer gpa.free(idle_iourings); + @memset(idle_iourings, 0); el.* = .{ .gpa = gpa, .mutex = .{}, @@ -101,9 +120,11 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { .free = .{}, .main_context = undefined, .exit_awaiter = null, - .idle_count = 0, .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])), + .idle_iourings = idle_iourings, }; + const main_thread = el.threads.addOneAssumeCapacity(); + main_thread.io_uring = try IoUring.init(io_uring_entries, 0); const main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)])); const idle_stack_end: [*]align(@max(@alignOf(Thread), @alignOf(Context))) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; @@ -113,9 +134,9 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { .rip = @intFromPtr(&mainIdleEntry), }; std.log.debug("created main idle {*}", .{main_idle_context}); - current_idle_context = main_idle_context; + main_thread.current_idle_context = main_idle_context; std.log.debug("created main {*}", .{&el.main_context}); - current_context = &el.main_context; + main_thread.current_context = &el.main_context; } pub fn deinit(el: *EventLoop) void { @@ -125,14 +146,21 @@ pub fn deinit(el: *EventLoop) void { const free_fiber: *Fiber = @alignCast(@fieldParentPtr("queue_node", free_node)); el.gpa.free(free_fiber.allocatedSlice()); } - const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context)); + const idle_context_offset = std.mem.alignForward(usize, el.threads.capacity * @sizeOf(Thread), @alignOf(Context)); const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context))) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); - for (el.threads.items) |*thread| thread.thread.join(); + for (el.threads.items[1..]) |*thread| thread.thread.join(); el.gpa.free(allocated_ptr[0..idle_stack_end]); } -fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { +const PendingTask = union(enum) { + none, + register_awaiter: *?*Fiber, + io_uring_submit: *IoUring, +}; + +fn yield(el: *EventLoop, optional_fiber: ?*Fiber, pending_task: 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(); @@ -141,13 +169,13 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v }) |ready_node| @alignCast(@fieldParentPtr("queue_node", ready_node)) else - break :ready_context current_idle_context; + break :ready_context thread.current_idle_context; break :ready_context &ready_fiber.context; }; const message: SwitchMessage = .{ - .prev_context = current_context, + .prev_context = thread.current_context, .ready_context = ready_context, - .register_awaiter = register_awaiter, + .pending_task = pending_task, }; std.log.debug("switching from {*} to {*}", .{ message.prev_context, message.ready_context }); contextSwitch(&message).handle(el); @@ -156,6 +184,11 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v fn schedule(el: *EventLoop, fiber: *Fiber) void { el.mutex.lock(); el.queue.append(&fiber.queue_node); + //for (el.idle_iourings) |*int| { + // const idler_subset = @atomicLoad(usize, int, .unordered); + // if (idler_subset == 0) continue; + // + //} if (el.idle_count > 0) { el.mutex.unlock(); el.cond.signal(); @@ -167,7 +200,7 @@ fn schedule(el: *EventLoop, fiber: *Fiber) void { thread.thread = std.Thread.spawn(.{ .stack_size = idle_stack_size, .allocator = el.gpa, - }, threadEntry, .{ el, thread }) catch { + }, threadEntry, .{ el, el.threads.items.len - 1 }) catch { el.threads.items.len -= 1; return; }; @@ -187,38 +220,61 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAl unreachable; // switched to dead fiber } -fn threadEntry(el: *EventLoop, thread: *Thread) void { +fn threadEntry(el: *EventLoop, index: usize) void { + thread_index = index; + const thread: *Thread = &el.threads.items[index]; std.log.debug("created thread idle {*}", .{&thread.idle_context}); - current_idle_context = &thread.idle_context; - current_context = &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_idle_context = &thread.idle_context; + thread.current_context = &thread.idle_context; _ = el.idle(); } fn idle(el: *EventLoop) *Fiber { + const thread: *Thread = &el.threads.items[thread_index]; + // The idle fiber only runs on one thread. + const iou = &thread.io_uring; + var cqes_buffer: [io_uring_entries]std.os.linux.io_uring_cqe = undefined; + while (true) { el.yield(null, null); if (@atomicLoad(?*Fiber, &el.exit_awaiter, .acquire)) |exit_awaiter| { el.cond.broadcast(); return exit_awaiter; } - el.mutex.lock(); - defer el.mutex.unlock(); - el.idle_count += 1; - defer el.idle_count -= 1; - el.cond.wait(&el.mutex); + // TODO add uring to bit set + const n = iou.copy_cqes(&cqes_buffer, 1) catch @panic("TODO handle copy_cqes error"); + const cqes = cqes_buffer[0..n]; + for (cqes) |cqe| { + const fiber: *Fiber = @ptrFromInt(cqe.user_data); + const res: *i32 = @ptrCast(@alignCast(fiber.resultPointer())); + res.* = cqe.res; + el.schedule(fiber); + } } } const SwitchMessage = extern struct { prev_context: *Context, ready_context: *Context, - register_awaiter: ?*?*Fiber, + pending_task: PendingTask, fn handle(message: *const SwitchMessage, el: *EventLoop) void { - current_context = message.ready_context; - if (message.register_awaiter) |awaiter| { - const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); - if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); + const thread: *Thread = &el.threads.items[thread_index]; + thread.current_context = message.ready_context; + switch (message.pending_task) { + .none => {}, + .register_awaiter => |awaiter| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); + if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); + }, + .io_uring_submit => |iou| { + _ = iou.flush_sq(); + // TODO: determine whether this return value should be used + }, } } }; @@ -357,3 +413,132 @@ pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: [] @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 { + _ = userdata; + _ = dir; + _ = sub_path; + _ = flags; + @panic("TODO"); +} + +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, .{ .io_uring_submit = iou }); + + 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), + } + + return .{ .handle = result.* }; +} + +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 { + _ = userdata; + _ = file; + @panic("TODO"); +} + +pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) std.fs.File.ReadError!usize { + _ = userdata; + _ = file; + _ = buffer; + @panic("TODO"); +} + +pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) std.fs.File.WriteError!usize { + _ = userdata; + _ = file; + _ = buffer; + @panic("TODO"); +} From db0dd3a48007484aec92a53495fd8899515dc876 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 29 Mar 2025 02:31:27 -0400 Subject: [PATCH 015/244] EventLoop: get file operations working Something is horribly wrong with scheduling, as can be seen in the debug output, but at least it somehow manages to exit cleanly... --- lib/std/Io/EventLoop.zig | 408 ++++++++++++++++++++++++++++----------- 1 file changed, 300 insertions(+), 108 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index ca42627005..a02e1f3b40 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -9,26 +9,25 @@ const IoUring = std.os.linux.IoUring; gpa: Allocator, mutex: std.Thread.Mutex, -cond: std.Thread.Condition, queue: std.DoublyLinkedList(void), +/// Atomic copy of queue.len +queue_len: usize, free: std.DoublyLinkedList(void), -main_context: Context, -exit_awaiter: ?*Fiber, +main_fiber: Fiber, +idle_count: usize, threads: std.ArrayListUnmanaged(Thread), -/// 1 bit per thread, same order as `thread_index`. -idle_iourings: []usize, +exiting: bool, threadlocal var thread_index: u32 = undefined; /// Empirically saw 10KB being used by the self-hosted backend for logging. -const idle_stack_size = 32 * 1024; +const idle_stack_size = 64 * 1024; const io_uring_entries = 64; const Thread = struct { thread: std.Thread, idle_context: Context, - current_idle_context: *Context, current_context: *Context, io_uring: IoUring, @@ -103,98 +102,92 @@ pub fn io(el: *EventLoop) Io { } pub fn init(el: *EventLoop, gpa: Allocator) !void { - const n_threads: usize = @max((std.Thread.getCpuCount() catch 1), 1); - const threads_bytes = n_threads * @sizeOf(Thread); - const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); - const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); - const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context)), idle_stack_end_offset); + 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); - const idle_iourings = try gpa.alloc(usize, (n_threads + @bitSizeOf(usize) - 1) / @bitSizeOf(usize)); - errdefer gpa.free(idle_iourings); - @memset(idle_iourings, 0); el.* = .{ .gpa = gpa, .mutex = .{}, - .cond = .{}, .queue = .{}, + .queue_len = 0, .free = .{}, - .main_context = undefined, - .exit_awaiter = null, - .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])), - .idle_iourings = idle_iourings, + .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 main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)])); - const idle_stack_end: [*]align(@max(@alignOf(Thread), @alignOf(Context))) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); + const idle_stack_end: [*]usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; - main_idle_context.* = .{ + main_thread.idle_context = .{ .rsp = @intFromPtr(idle_stack_end - 1), .rbp = 0, .rip = @intFromPtr(&mainIdleEntry), }; - std.log.debug("created main idle {*}", .{main_idle_context}); - main_thread.current_idle_context = main_idle_context; - std.log.debug("created main {*}", .{&el.main_context}); - main_thread.current_context = &el.main_context; + 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, &el.exit_awaiter); + 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_context_offset = std.mem.alignForward(usize, el.threads.capacity * @sizeOf(Thread), @alignOf(Context)); - const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); - const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context))) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); + 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]); + el.gpa.free(allocated_ptr[0..idle_stack_end_offset]); } -const PendingTask = union(enum) { - none, - register_awaiter: *?*Fiber, - io_uring_submit: *IoUring, -}; - -fn yield(el: *EventLoop, optional_fiber: ?*Fiber, pending_task: PendingTask) void { +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(); - break :ready_node el.queue.pop(); + const ready_node = el.queue.pop(); + @atomicStore(usize, &el.queue_len, el.queue.len, .unordered); + break :ready_node ready_node; }) |ready_node| @alignCast(@fieldParentPtr("queue_node", ready_node)) else - break :ready_context thread.current_idle_context; + break :ready_context &thread.idle_context; break :ready_context &ready_fiber.context; }; const message: SwitchMessage = .{ - .prev_context = thread.current_context, - .ready_context = ready_context, + .contexts = .{ + .prev = thread.current_context, + .ready = ready_context, + }, .pending_task = pending_task, }; - std.log.debug("switching from {*} to {*}", .{ message.prev_context, message.ready_context }); + std.log.debug("switching from {*} to {*}", .{ message.contexts.prev, message.contexts.ready }); contextSwitch(&message).handle(el); } fn schedule(el: *EventLoop, fiber: *Fiber) void { - el.mutex.lock(); - el.queue.append(&fiber.queue_node); - //for (el.idle_iourings) |*int| { - // const idler_subset = @atomicLoad(usize, int, .unordered); - // if (idler_subset == 0) continue; - // - //} - if (el.idle_count > 0) { - el.mutex.unlock(); - el.cond.signal(); + if (idle_count: { + el.mutex.lock(); + defer el.mutex.unlock(); + el.queue.append(&fiber.queue_node); + @atomicStore(usize, &el.queue_len, el.queue.len, .unordered); + break :idle_count el.idle_count; + } > 0) { + _ = std.os.linux.futex2_wake(&el.queue_len, std.math.maxInt(usize), 1, switch (@bitSizeOf(usize)) { + 8 => std.os.linux.FUTEX2.SIZE_U8, + 16 => std.os.linux.FUTEX2.SIZE_U16, + 32 => std.os.linux.FUTEX2.SIZE_U32, + 64 => std.os.linux.FUTEX2.SIZE_U64, + else => @compileError("unsupported @sizeOf(usize)"), + } | std.os.linux.FUTEX2.PRIVATE); // TODO: io_uring return; } - defer el.mutex.unlock(); if (el.threads.items.len == el.threads.capacity) return; const thread = el.threads.addOneAssumeCapacity(); thread.thread = std.Thread.spawn(.{ @@ -216,64 +209,101 @@ fn recycle(el: *EventLoop, fiber: *Fiber) void { fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { message.handle(el); - el.yield(el.idle(), null); + el.idle(); + el.yield(&el.main_fiber, .nothing); unreachable; // switched to dead fiber } fn threadEntry(el: *EventLoop, index: usize) void { - thread_index = index; + 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_idle_context = &thread.idle_context; thread.current_context = &thread.idle_context; - _ = el.idle(); + el.idle(); } -fn idle(el: *EventLoop) *Fiber { +const UserData = enum(u64) { + queue_len_futex_wait, + _, +}; + +fn idle(el: *EventLoop) void { const thread: *Thread = &el.threads.items[thread_index]; - // The idle fiber only runs on one thread. const iou = &thread.io_uring; var cqes_buffer: [io_uring_entries]std.os.linux.io_uring_cqe = undefined; + var futex_is_scheduled: bool = false; while (true) { - el.yield(null, null); - if (@atomicLoad(?*Fiber, &el.exit_awaiter, .acquire)) |exit_awaiter| { - el.cond.broadcast(); - return exit_awaiter; - } - // TODO add uring to bit set - const n = iou.copy_cqes(&cqes_buffer, 1) catch @panic("TODO handle copy_cqes error"); - const cqes = cqes_buffer[0..n]; - for (cqes) |cqe| { - const fiber: *Fiber = @ptrFromInt(cqe.user_data); - const res: *i32 = @ptrCast(@alignCast(fiber.resultPointer())); - res.* = cqe.res; - el.schedule(fiber); + el.yield(null, .nothing); + if (@atomicLoad(bool, &el.exiting, .acquire)) return; + if (!futex_is_scheduled) { + const sqe = getSqe(&thread.io_uring); + sqe.prep_rw(.FUTEX_WAIT, switch (@bitSizeOf(usize)) { + 8 => std.os.linux.FUTEX2.SIZE_U8, + 16 => std.os.linux.FUTEX2.SIZE_U16, + 32 => std.os.linux.FUTEX2.SIZE_U32, + 64 => std.os.linux.FUTEX2.SIZE_U64, + else => @compileError("unsupported @sizeOf(usize)"), + } | std.os.linux.FUTEX2.PRIVATE, @intFromPtr(&el.queue_len), 0, 0); + sqe.addr3 = std.math.maxInt(u64); + sqe.user_data = @intFromEnum(UserData.queue_len_futex_wait); + futex_is_scheduled = true; } + _ = iou.submit_and_wait(1) catch |err| switch (err) { + error.SignalInterrupt => 0, + else => @panic(@errorName(err)), + }; + for (cqes_buffer[0 .. iou.copy_cqes(&cqes_buffer, 1) catch |err| switch (err) { + error.SignalInterrupt => 0, + else => @panic(@errorName(err)), + }]) |cqe| switch (@as(UserData, @enumFromInt(cqe.user_data))) { + .queue_len_futex_wait => 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 = extern struct { - prev_context: *Context, - ready_context: *Context, +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.ready_context; + thread.current_context = message.contexts.ready; switch (message.pending_task) { - .none => {}, + .nothing => {}, .register_awaiter => |awaiter| { - const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); + 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); }, - .io_uring_submit => |iou| { - _ = iou.flush_sq(); - // TODO: determine whether this return value should be used + .exit => { + @atomicStore(bool, &el.exiting, true, .unordered); + @atomicStore(usize, &el.queue_len, std.math.maxInt(usize), .release); + _ = std.os.linux.futex2_wake(&el.queue_len, std.math.maxInt(usize), std.math.maxInt(i32), switch (@bitSizeOf(usize)) { + 8 => std.os.linux.FUTEX2.SIZE_U8, + 16 => std.os.linux.FUTEX2.SIZE_U16, + 32 => std.os.linux.FUTEX2.SIZE_U32, + 64 => std.os.linux.FUTEX2.SIZE_U64, + else => @compileError("unsupported @sizeOf(usize)"), + } | std.os.linux.FUTEX2.PRIVATE); // TODO: use io_uring }, } } @@ -289,7 +319,7 @@ const Context = switch (builtin.cpu.arch) { }; inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { - return switch (builtin.cpu.arch) { + return @fieldParentPtr("contexts", switch (builtin.cpu.arch) { .x86_64 => asm volatile ( \\ movq 0(%%rsi), %%rax \\ movq 8(%%rsi), %%rcx @@ -301,8 +331,8 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { \\ movq 8(%%rcx), %%rbp \\ jmpq *16(%%rcx) \\0: - : [received_message] "={rsi}" (-> *const SwitchMessage), - : [message_to_send] "{rsi}" (message), + : [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", // @@ -313,7 +343,7 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { "fpsr", "fpcr", "mxcsr", "rflags", "dirflag", "memory" ), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), - }; + }); } fn mainIdleEntry() callconv(.naked) void { @@ -401,7 +431,7 @@ const AsyncClosure = struct { 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, null); + closure.event_loop.yield(awaiter, .nothing); unreachable; // switched to dead fiber } }; @@ -409,17 +439,93 @@ const AsyncClosure = struct { 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, &future_fiber.awaiter); + 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 { - _ = userdata; - _ = dir; - _ = sub_path; - _ = flags; - @panic("TODO"); + 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 { @@ -476,7 +582,7 @@ pub fn openFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, fl sqe.prep_openat(dir.fd, &sub_path_c, os_flags, 0); sqe.user_data = @intFromPtr(fiber); - el.yield(null, .{ .io_uring_submit = iou }); + el.yield(null, .nothing); const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)])); const rc = result.*; @@ -510,8 +616,6 @@ pub fn openFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, fl .NXIO => return error.NoDevice, else => |err| return posix.unexpectedErrno(err), } - - return .{ .handle = result.* }; } fn errno(signed: i32) std.posix.E { @@ -524,21 +628,109 @@ fn getSqe(iou: *IoUring) *std.os.linux.io_uring_sqe { } pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { - _ = userdata; - _ = file; - @panic("TODO"); + 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 { - _ = userdata; - _ = file; - _ = buffer; - @panic("TODO"); + 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 { - _ = userdata; - _ = file; - _ = buffer; - @panic("TODO"); + 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), + } } From d958077203bbf022947e36935def869dcb66be12 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sat, 29 Mar 2025 10:56:45 -0400 Subject: [PATCH 016/244] EventLoop: fix futex usage How silly of me to forget that the kernel doesn't implement its own API. The scheduling is not great, but at least doesn't deadlock or hammer. --- lib/std/Io/EventLoop.zig | 79 +++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index a02e1f3b40..7baca5c853 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -11,7 +11,7 @@ gpa: Allocator, mutex: std.Thread.Mutex, queue: std.DoublyLinkedList(void), /// Atomic copy of queue.len -queue_len: usize, +queue_len: u32, free: std.DoublyLinkedList(void), main_fiber: Fiber, idle_count: usize, @@ -20,8 +20,8 @@ exiting: bool, threadlocal var thread_index: u32 = undefined; -/// Empirically saw 10KB being used by the self-hosted backend for logging. -const idle_stack_size = 64 * 1024; +/// Empirically saw >128KB being used by the self-hosted backend to panic. +const idle_stack_size = 256 * 1024; const io_uring_entries = 64; @@ -143,6 +143,7 @@ pub fn deinit(el: *EventLoop) void { 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 { @@ -151,8 +152,9 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, pending_task: SwitchMessage.Pe 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(); - @atomicStore(usize, &el.queue_len, el.queue.len, .unordered); + _ = @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)) @@ -172,20 +174,16 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, pending_task: SwitchMessage.Pe } 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); - @atomicStore(usize, &el.queue_len, el.queue.len, .unordered); + _ = @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(usize), 1, switch (@bitSizeOf(usize)) { - 8 => std.os.linux.FUTEX2.SIZE_U8, - 16 => std.os.linux.FUTEX2.SIZE_U16, - 32 => std.os.linux.FUTEX2.SIZE_U32, - 64 => std.os.linux.FUTEX2.SIZE_U64, - else => @compileError("unsupported @sizeOf(usize)"), - } | std.os.linux.FUTEX2.PRIVATE); // TODO: io_uring + _ = 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; @@ -226,8 +224,8 @@ fn threadEntry(el: *EventLoop, index: usize) void { el.idle(); } -const UserData = enum(u64) { - queue_len_futex_wait, +const CompletionKey = enum(u64) { + queue_len_futex_wait = 1, _, }; @@ -235,33 +233,44 @@ 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 futex_is_scheduled: bool = false; + var queue_len_futex_is_scheduled: bool = false; while (true) { el.yield(null, .nothing); if (@atomicLoad(bool, &el.exiting, .acquire)) return; - if (!futex_is_scheduled) { + if (!queue_len_futex_is_scheduled) { const sqe = getSqe(&thread.io_uring); - sqe.prep_rw(.FUTEX_WAIT, switch (@bitSizeOf(usize)) { - 8 => std.os.linux.FUTEX2.SIZE_U8, - 16 => std.os.linux.FUTEX2.SIZE_U16, - 32 => std.os.linux.FUTEX2.SIZE_U32, - 64 => std.os.linux.FUTEX2.SIZE_U64, - else => @compileError("unsupported @sizeOf(usize)"), - } | std.os.linux.FUTEX2.PRIVATE, @intFromPtr(&el.queue_len), 0, 0); - sqe.addr3 = std.math.maxInt(u64); - sqe.user_data = @intFromEnum(UserData.queue_len_futex_wait); - futex_is_scheduled = true; + 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 => 0, + 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 => 0, + error.SignalInterrupt => cqes_len: { + std.log.debug("copy_cqes: SignalInterrupt", .{}); + break :cqes_len 0; + }, else => @panic(@errorName(err)), - }]) |cqe| switch (@as(UserData, @enumFromInt(cqe.user_data))) { - .queue_len_futex_wait => futex_is_scheduled = false, + }]) |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())); @@ -296,14 +305,8 @@ const SwitchMessage = struct { }, .exit => { @atomicStore(bool, &el.exiting, true, .unordered); - @atomicStore(usize, &el.queue_len, std.math.maxInt(usize), .release); - _ = std.os.linux.futex2_wake(&el.queue_len, std.math.maxInt(usize), std.math.maxInt(i32), switch (@bitSizeOf(usize)) { - 8 => std.os.linux.FUTEX2.SIZE_U8, - 16 => std.os.linux.FUTEX2.SIZE_U16, - 32 => std.os.linux.FUTEX2.SIZE_U32, - 64 => std.os.linux.FUTEX2.SIZE_U64, - else => @compileError("unsupported @sizeOf(usize)"), - } | std.os.linux.FUTEX2.PRIVATE); // TODO: use io_uring + @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 }, } } From a29b2122d2b7659bb4b7d2990c415681ae4c3d86 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 29 Mar 2025 15:22:00 -0700 Subject: [PATCH 017/244] better API for Io.async --- lib/std/Io.zig | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 142e3d4dc4..962eaf64fb 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -555,7 +555,6 @@ test { } const Io = @This(); -const fs = std.fs; pub const EventLoop = @import("Io/EventLoop.zig"); @@ -613,20 +612,16 @@ pub fn Future(Result: type) type { }; } -/// `s` is a struct instance that contains a function like this: -/// ``` -/// struct { -/// pub fn start(s: S) Result { ... } -/// } -/// ``` -/// where `Result` is any type. -pub fn async(io: Io, S: type, s: S) Future(@typeInfo(@TypeOf(S.start)).@"fn".return_type.?) { - const Result = @typeInfo(@TypeOf(S.start)).@"fn".return_type.?; +/// Calls `function` with `args`, such that the return value of the function is +/// not guaranteed to be available until `await` is called. +pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { + const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; + const Args = @TypeOf(args); const TypeErased = struct { fn start(context: *const anyopaque, result: *anyopaque) void { - const context_casted: *const S = @alignCast(@ptrCast(context)); + const args_casted: *const Args = @alignCast(@ptrCast(context)); const result_casted: *Result = @ptrCast(@alignCast(result)); - result_casted.* = S.start(context_casted.*); + result_casted.* = @call(.auto, function, args_casted.*); } }; var future: Future(Result) = undefined; @@ -634,8 +629,8 @@ pub fn async(io: Io, S: type, s: S) Future(@typeInfo(@TypeOf(S.start)).@"fn".ret io.userdata, @ptrCast((&future.result)[0..1]), .fromByteUnits(@alignOf(Result)), - if (@sizeOf(S) == 0) &.{} else @ptrCast((&s)[0..1]), // work around compiler bug - .fromByteUnits(@alignOf(S)), + if (@sizeOf(Args) == 0) &.{} else @ptrCast((&args)[0..1]), // work around compiler bug + .fromByteUnits(@alignOf(Args)), TypeErased.start, ); return future; From e7caf3a54c24264734e4f464f0acbdb27b890893 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 29 Mar 2025 20:58:07 -0700 Subject: [PATCH 018/244] std.Io: introduce cancellation --- lib/std/Io.zig | 63 +++++++-- lib/std/Io/EventLoop.zig | 16 ++- lib/std/Thread/Pool.zig | 277 ++++++++++++++++++++++++--------------- 3 files changed, 239 insertions(+), 117 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 962eaf64fb..ba0eab1b83 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -564,6 +564,8 @@ vtable: *const VTable, pub const VTable = struct { /// If it returns `null` it means `result` has been already populated and /// `await` will be a no-op. + /// + /// Thread-safe. async: *const fn ( /// Corresponds to `Io.userdata`. userdata: ?*anyopaque, @@ -579,6 +581,8 @@ pub const VTable = struct { ) ?*AnyFuture, /// This function is only called when `async` returns a non-null value. + /// + /// Thread-safe. await: *const fn ( /// Corresponds to `Io.userdata`. userdata: ?*anyopaque, @@ -589,13 +593,41 @@ pub const VTable = struct { result: []u8, ) void, - createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) fs.File.OpenError!fs.File, - openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) fs.File.OpenError!fs.File, + /// Equivalent to `await` but initiates cancel request. + /// + /// This function is only called when `async` returns a non-null value. + /// + /// Thread-safe. + cancel: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// The same value that was returned from `async`. + any_future: *AnyFuture, + /// Points to a buffer where the result is written. + /// The length is equal to size in bytes of result type. + result: []u8, + ) void, + + /// Returns whether the current thread of execution is known to have + /// been requested to cancel. + /// + /// Thread-safe. + cancelRequested: *const fn (?*anyopaque) bool, + + createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, + openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, closeFile: *const fn (?*anyopaque, fs.File) void, - read: *const fn (?*anyopaque, file: fs.File, buffer: []u8) fs.File.ReadError!usize, - write: *const fn (?*anyopaque, file: fs.File, buffer: []const u8) fs.File.WriteError!usize, + read: *const fn (?*anyopaque, file: fs.File, buffer: []u8) FileReadError!usize, + write: *const fn (?*anyopaque, file: fs.File, buffer: []const u8) FileWriteError!usize, }; +pub const OpenFlags = fs.File.OpenFlags; +pub const CreateFlags = fs.File.CreateFlags; + +pub const FileOpenError = fs.File.OpenError || error{AsyncCancel}; +pub const FileReadError = fs.File.ReadError || error{AsyncCancel}; +pub const FileWriteError = fs.File.WriteError || error{AsyncCancel}; + pub const AnyFuture = opaque {}; pub fn Future(Result: type) type { @@ -603,6 +635,17 @@ pub fn Future(Result: type) type { any_future: ?*AnyFuture, result: Result, + /// Equivalent to `await` but sets a flag observable to application + /// code that cancellation has been requested. + /// + /// Idempotent. + pub fn cancel(f: *@This(), io: Io) Result { + const any_future = f.any_future orelse return f.result; + io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1])); + f.any_future = null; + return f.result; + } + pub fn await(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1])); @@ -636,11 +679,11 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( return future; } -pub fn openFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) fs.File.OpenError!fs.File { +pub fn openFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File { return io.vtable.openFile(io.userdata, dir, sub_path, flags); } -pub fn createFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) fs.File.OpenError!fs.File { +pub fn createFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File { return io.vtable.createFile(io.userdata, dir, sub_path, flags); } @@ -648,22 +691,22 @@ pub fn closeFile(io: Io, file: fs.File) void { return io.vtable.closeFile(io.userdata, file); } -pub fn read(io: Io, file: fs.File, buffer: []u8) fs.File.ReadError!usize { +pub fn read(io: Io, file: fs.File, buffer: []u8) FileReadError!usize { return io.vtable.read(io.userdata, file, buffer); } -pub fn write(io: Io, file: fs.File, buffer: []const u8) fs.File.WriteError!usize { +pub fn write(io: Io, file: fs.File, buffer: []const u8) FileWriteError!usize { return io.vtable.write(io.userdata, file, buffer); } -pub fn writeAll(io: Io, file: fs.File, bytes: []const u8) fs.File.WriteError!void { +pub fn writeAll(io: Io, file: fs.File, bytes: []const u8) FileWriteError!void { var index: usize = 0; while (index < bytes.len) { index += try io.write(file, bytes[index..]); } } -pub fn readAll(io: Io, file: fs.File, buffer: []u8) fs.File.ReadError!usize { +pub fn readAll(io: Io, file: fs.File, buffer: []u8) FileReadError!usize { var index: usize = 0; while (index != buffer.len) { const amt = try io.read(file, buffer[index..]); diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 7baca5c853..f4147b16ee 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -7,12 +7,13 @@ 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(void), +queue: std.DoublyLinkedList, /// Atomic copy of queue.len queue_len: u32, -free: std.DoublyLinkedList(void), +free: std.DoublyLinkedList, main_fiber: Fiber, idle_count: usize, threads: std.ArrayListUnmanaged(Thread), @@ -39,7 +40,7 @@ const Thread = struct { const Fiber = struct { context: Context, awaiter: ?*Fiber, - queue_node: std.DoublyLinkedList(void).Node, + queue_node: std.DoublyLinkedList.Node, result_align: Alignment, const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); @@ -447,6 +448,15 @@ pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: [] 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)); diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 486d067330..03fdbe21a1 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -1,22 +1,27 @@ const builtin = @import("builtin"); const std = @import("std"); +const Allocator = std.mem.Allocator; const assert = std.debug.assert; const WaitGroup = @import("WaitGroup.zig"); +const Io = std.Io; const Pool = @This(); +/// Must be a thread-safe allocator. +allocator: std.mem.Allocator, mutex: std.Thread.Mutex = .{}, cond: std.Thread.Condition = .{}, run_queue: std.SinglyLinkedList = .{}, is_running: bool = true, -/// Must be a thread-safe allocator. -allocator: std.mem.Allocator, -threads: if (builtin.single_threaded) [0]std.Thread else []std.Thread, +threads: std.ArrayListUnmanaged(std.Thread), ids: if (builtin.single_threaded) struct { inline fn deinit(_: @This(), _: std.mem.Allocator) void {} fn getIndex(_: @This(), _: std.Thread.Id) usize { return 0; } } else std.AutoArrayHashMapUnmanaged(std.Thread.Id, void), +stack_size: usize, + +threadlocal var current_closure: ?*AsyncClosure = null; pub const Runnable = struct { runFn: RunProto, @@ -33,48 +38,36 @@ pub const Options = struct { }; pub fn init(pool: *Pool, options: Options) !void { - const allocator = options.allocator; + const gpa = options.allocator; + const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1); + const threads = try gpa.alloc(std.Thread, thread_count); + errdefer gpa.free(threads); pool.* = .{ - .allocator = allocator, - .threads = if (builtin.single_threaded) .{} else &.{}, + .allocator = gpa, + .threads = .initBuffer(threads), .ids = .{}, + .stack_size = options.stack_size, }; - if (builtin.single_threaded) { - return; - } + if (builtin.single_threaded) return; - const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1); if (options.track_ids) { - try pool.ids.ensureTotalCapacity(allocator, 1 + thread_count); + try pool.ids.ensureTotalCapacity(gpa, 1 + thread_count); pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {}); } - - // kill and join any threads we spawned and free memory on error. - pool.threads = try allocator.alloc(std.Thread, thread_count); - var spawned: usize = 0; - errdefer pool.join(spawned); - - for (pool.threads) |*thread| { - thread.* = try std.Thread.spawn(.{ - .stack_size = options.stack_size, - .allocator = allocator, - }, worker, .{pool}); - spawned += 1; - } } pub fn deinit(pool: *Pool) void { - pool.join(pool.threads.len); // kill and join all threads. - pool.ids.deinit(pool.allocator); + const gpa = pool.allocator; + pool.join(); + pool.threads.deinit(gpa); + pool.ids.deinit(gpa); pool.* = undefined; } -fn join(pool: *Pool, spawned: usize) void { - if (builtin.single_threaded) { - return; - } +fn join(pool: *Pool) void { + if (builtin.single_threaded) return; { pool.mutex.lock(); @@ -87,11 +80,7 @@ fn join(pool: *Pool, spawned: usize) void { // wake up any sleeping threads (this can be done outside the mutex) // then wait for all the threads we know are spawned to complete. pool.cond.broadcast(); - for (pool.threads[0..spawned]) |thread| { - thread.join(); - } - - pool.allocator.free(pool.threads); + for (pool.threads.items) |thread| thread.join(); } /// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and @@ -123,26 +112,34 @@ pub fn spawnWg(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args } }; - { - pool.mutex.lock(); + pool.mutex.lock(); - const closure = pool.allocator.create(Closure) catch { - pool.mutex.unlock(); - @call(.auto, func, args); - wait_group.finish(); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - .wait_group = wait_group, - }; - - pool.run_queue.prepend(&closure.runnable.node); + const gpa = pool.allocator; + const closure = gpa.create(Closure) catch { pool.mutex.unlock(); + @call(.auto, func, args); + wait_group.finish(); + return; + }; + closure.* = .{ + .arguments = args, + .pool = pool, + .wait_group = wait_group, + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; } - // Notify waiting threads outside the lock to try and keep the critical section small. + pool.mutex.unlock(); pool.cond.signal(); } @@ -179,31 +176,39 @@ pub fn spawnWgId(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, ar } }; - { - pool.mutex.lock(); + pool.mutex.lock(); - const closure = pool.allocator.create(Closure) catch { - const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId()); - pool.mutex.unlock(); - @call(.auto, func, .{id.?} ++ args); - wait_group.finish(); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - .wait_group = wait_group, - }; - - pool.run_queue.prepend(&closure.runnable.node); + const gpa = pool.allocator; + const closure = gpa.create(Closure) catch { + const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId()); pool.mutex.unlock(); + @call(.auto, func, .{id.?} ++ args); + wait_group.finish(); + return; + }; + closure.* = .{ + .arguments = args, + .pool = pool, + .wait_group = wait_group, + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; } - // Notify waiting threads outside the lock to try and keep the critical section small. + pool.mutex.unlock(); pool.cond.signal(); } -pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) !void { +pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) void { if (builtin.single_threaded) { @call(.auto, func, args); return; @@ -222,20 +227,32 @@ pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) !void { } }; - { - pool.mutex.lock(); - defer pool.mutex.unlock(); + pool.mutex.lock(); - const closure = try pool.allocator.create(Closure); - closure.* = .{ - .arguments = args, - .pool = pool, + const gpa = pool.allocator; + const closure = gpa.create(Closure) catch { + pool.mutex.unlock(); + @call(.auto, func, args); + return; + }; + closure.* = .{ + .arguments = args, + .pool = pool, + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; }; - - pool.run_queue.prepend(&closure.runnable.node); } - // Notify waiting threads outside the lock to try and keep the critical section small. + pool.mutex.unlock(); pool.cond.signal(); } @@ -254,7 +271,7 @@ test spawn { .allocator = std.testing.allocator, }); defer pool.deinit(); - try pool.spawn(TestFn.checkRun, .{&completed}); + pool.spawn(TestFn.checkRun, .{&completed}); } try std.testing.expectEqual(true, completed); @@ -306,15 +323,17 @@ pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void { } pub fn getIdCount(pool: *Pool) usize { - return @intCast(1 + pool.threads.len); + return @intCast(1 + pool.threads.items.len); } -pub fn io(pool: *Pool) std.Io { +pub fn io(pool: *Pool) Io { return .{ .userdata = pool, .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .cancel = cancel, + .cancelRequested = cancelRequested, .createFile = createFile, .openFile = openFile, .closeFile = closeFile, @@ -326,15 +345,17 @@ pub fn io(pool: *Pool) std.Io { const AsyncClosure = struct { func: *const fn (context: *anyopaque, result: *anyopaque) void, - run_node: std.Thread.Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, + runnable: Runnable = .{ .runFn = runFn }, reset_event: std.Thread.ResetEvent, + cancel_flag: bool, context_offset: usize, result_offset: usize, fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { - const run_node: *std.Thread.Pool.RunQueue.Node = @fieldParentPtr("data", runnable); - const closure: *AsyncClosure = @alignCast(@fieldParentPtr("run_node", run_node)); + const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable)); + current_closure = closure; closure.func(closure.contextPointer(), closure.resultPointer()); + current_closure = null; closure.reset_event.set(); } @@ -359,16 +380,23 @@ const AsyncClosure = struct { const base: [*]u8 = @ptrCast(closure); return base + closure.context_offset; } + + fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void { + closure.reset_event.wait(); + const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); + @memcpy(result, closure.resultPointer()[0..result.len]); + gpa.free(base[0 .. closure.result_offset + result.len]); + } }; -pub fn @"async"( +fn @"async"( userdata: ?*anyopaque, result: []u8, result_alignment: std.mem.Alignment, context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) ?*std.Io.AnyFuture { +) ?*Io.AnyFuture { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); pool.mutex.lock(); @@ -386,46 +414,87 @@ pub fn @"async"( .context_offset = context_offset, .result_offset = result_offset, .reset_event = .{}, + .cancel_flag = false, }; @memcpy(closure.contextPointer()[0..context.len], context); - pool.run_queue.prepend(&closure.run_node); - pool.mutex.unlock(); + pool.run_queue.prepend(&closure.runnable.node); + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); pool.cond.signal(); return @ptrCast(closure); } -pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void { - const thread_pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); +fn @"await"(userdata: ?*anyopaque, any_future: *Io.AnyFuture, result: []u8) void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - closure.reset_event.wait(); - const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); - @memcpy(result, closure.resultPointer()[0..result.len]); - thread_pool.allocator.free(base[0 .. closure.result_offset + result.len]); + closure.waitAndFree(pool.allocator, result); } -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 { - _ = userdata; +fn cancel(userdata: ?*anyopaque, any_future: *Io.AnyFuture, result: []u8) void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); + @atomicStore(bool, &closure.cancel_flag, true, .seq_cst); + closure.waitAndFree(pool.allocator, result); +} + +fn cancelRequested(userdata: ?*anyopaque) bool { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + const closure = current_closure orelse return false; + return @atomicLoad(bool, &closure.cancel_flag, .unordered); +} + +fn checkCancel(pool: *Pool) error{AsyncCancel}!void { + if (cancelRequested(pool)) return error.AsyncCancel; +} + +pub fn createFile( + userdata: ?*anyopaque, + dir: std.fs.Dir, + sub_path: []const u8, + flags: std.fs.File.CreateFlags, +) Io.FileOpenError!std.fs.File { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); return dir.createFile(sub_path, flags); } -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 { - _ = userdata; +pub fn openFile( + userdata: ?*anyopaque, + dir: std.fs.Dir, + sub_path: []const u8, + flags: std.fs.File.OpenFlags, +) Io.FileOpenError!std.fs.File { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); return dir.openFile(sub_path, flags); } pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { - _ = userdata; + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + _ = pool; return file.close(); } -pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) std.fs.File.ReadError!usize { - _ = userdata; +pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) Io.FileReadError!usize { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); return file.read(buffer); } -pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) std.fs.File.WriteError!usize { - _ = userdata; +pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) Io.FileWriteError!usize { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); return file.write(buffer); } From 5041c9ad9cbf479e62416cd06ef8a178f3467127 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sun, 30 Mar 2025 01:54:02 -0400 Subject: [PATCH 019/244] EventLoop: implement thread-local queues and cancellation --- lib/std/Io.zig | 10 +- lib/std/Io/EventLoop.zig | 638 +++++++++++++++++++++++++-------------- lib/std/Thread/Pool.zig | 16 +- 3 files changed, 427 insertions(+), 237 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index ba0eab1b83..256e113ba0 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -591,6 +591,7 @@ pub const VTable = struct { /// Points to a buffer where the result is written. /// The length is equal to size in bytes of result type. result: []u8, + result_alignment: std.mem.Alignment, ) void, /// Equivalent to `await` but initiates cancel request. @@ -606,6 +607,7 @@ pub const VTable = struct { /// Points to a buffer where the result is written. /// The length is equal to size in bytes of result type. result: []u8, + result_alignment: std.mem.Alignment, ) void, /// Returns whether the current thread of execution is known to have @@ -641,14 +643,14 @@ pub fn Future(Result: type) type { /// Idempotent. pub fn cancel(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; - io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1])); + io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); f.any_future = null; return f.result; } pub fn await(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; - io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1])); + io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); f.any_future = null; return f.result; } @@ -671,9 +673,9 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( future.any_future = io.vtable.async( io.userdata, @ptrCast((&future.result)[0..1]), - .fromByteUnits(@alignOf(Result)), + .of(Result), if (@sizeOf(Args) == 0) &.{} else @ptrCast((&args)[0..1]), // work around compiler bug - .fromByteUnits(@alignOf(Args)), + .of(Args), TypeErased.start, ); return future; diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index f4147b16ee..5de161d9f9 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -10,38 +10,50 @@ 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; +threads: Thread.List, /// Empirically saw >128KB being used by the self-hosted backend to panic. const idle_stack_size = 256 * 1024; +const max_idle_search = 4; +const max_steal_ready_search = 4; + const io_uring_entries = 64; const Thread = struct { thread: std.Thread, idle_context: Context, current_context: *Context, + ready_queue: ?*Fiber, + free_queue: ?*Fiber, io_uring: IoUring, + idle_search_index: u32, + steal_ready_search_index: u32, + + threadlocal var index: u32 = undefined; + + fn current(el: *EventLoop) *Thread { + return &el.threads.allocated[index]; + } fn currentFiber(thread: *Thread) *Fiber { return @fieldParentPtr("context", thread.current_context); } + + const List = struct { + allocated: []Thread, + reserved: u32, + active: u32, + }; }; const Fiber = struct { context: Context, awaiter: ?*Fiber, - queue_node: std.DoublyLinkedList.Node, - result_align: Alignment, + queue_next: ?*Fiber, + can_cancel: bool, + canceled: bool, const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); @@ -63,14 +75,13 @@ const Fiber = struct { ); 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)); + const thread: *Thread = .current(el); + if (thread.free_queue) |free_fiber| { + thread.free_queue = free_fiber.queue_next; + free_fiber.queue_next = null; + return free_fiber; + } + return @ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), allocation_size)); } fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { @@ -82,9 +93,15 @@ const Fiber = struct { return allocated_slice[allocated_slice.len..].ptr; } - fn resultPointer(f: *Fiber) [*]u8 { - return @ptrFromInt(f.result_align.forward(@intFromPtr(f) + @sizeOf(Fiber))); + fn resultPointer(f: *Fiber, comptime Result: type) *Result { + return @alignCast(@ptrCast(f.resultBytes(.of(Result)))); } + + fn resultBytes(f: *Fiber, alignment: Alignment) [*]u8 { + return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); + } + + const Queue = struct { head: *Fiber, tail: *Fiber }; }; pub fn io(el: *EventLoop) Io { @@ -93,6 +110,8 @@ pub fn io(el: *EventLoop) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .cancel = cancel, + .cancelRequested = cancelRequested, .createFile = createFile, .openFile = openFile, .closeFile = closeFile, @@ -110,58 +129,86 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { 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, + .main_fiber = .{ + .context = undefined, + .awaiter = null, + .queue_next = null, + .can_cancel = false, + .canceled = false, + }, + .threads = .{ + .allocated = @ptrCast(allocated_slice[0..threads_size]), + .reserved = 1, + .active = 1, + }, }; - thread_index = 0; - const main_thread = el.threads.addOneAssumeCapacity(); - main_thread.io_uring = try IoUring.init(io_uring_entries, 0); + Thread.index = 0; + const main_thread = &el.threads.allocated[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), + main_thread.* = .{ + .thread = undefined, + .idle_context = .{ + .rsp = @intFromPtr(idle_stack_end - 1), + .rbp = 0, + .rip = @intFromPtr(&mainIdleEntry), + }, + .current_context = &el.main_fiber.context, + .ready_queue = null, + .free_queue = null, + .io_uring = try IoUring.init(io_uring_entries, 0), + .idle_search_index = 1, + .steal_ready_search_index = 1, }; + errdefer main_thread.io_uring.deinit(); 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 + const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); + for (el.threads.allocated[0..active_threads]) |*thread| + assert(@atomicLoad(?*Fiber, &thread.ready_queue, .unordered) == null); // 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 allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.allocated.ptr)); + const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); + for (el.threads.allocated[1..active_threads]) |*thread| { + thread.thread.join(); + while (thread.free_queue) |free_fiber| { + thread.free_queue = free_fiber.queue_next; + free_fiber.queue_next = null; + 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; +fn yield(el: *EventLoop, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void { + const thread: *Thread = .current(el); + const ready_context: *Context = if (maybe_ready_fiber) |ready_fiber| + &ready_fiber.context + else if (thread.ready_queue) |ready_fiber| ready_context: { + thread.ready_queue = ready_fiber.queue_next; + ready_fiber.queue_next = null; break :ready_context &ready_fiber.context; + } else ready_context: { + const ready_threads = @atomicLoad(u32, &el.threads.active, .acquire); + break :ready_context for (0..max_steal_ready_search) |_| { + defer thread.steal_ready_search_index += 1; + if (thread.steal_ready_search_index == ready_threads) thread.steal_ready_search_index = 0; + const steal_ready_search_thread = &el.threads.allocated[thread.steal_ready_search_index]; + const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; + if (@cmpxchgWeak( + ?*Fiber, + &steal_ready_search_thread.ready_queue, + ready_fiber, + @atomicLoad(?*Fiber, &ready_fiber.queue_next, .acquire), + .acq_rel, + .monotonic, + )) |_| continue; + break &ready_fiber.context; + } else &thread.idle_context; }; const message: SwitchMessage = .{ .contexts = .{ @@ -174,111 +221,177 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, pending_task: SwitchMessage.Pe 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 +fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { + { + var fiber = ready_queue.head; + while (true) { + std.log.debug("scheduling {*}", .{fiber}); + fiber = fiber.queue_next orelse break; + } + assert(fiber == ready_queue.tail); + } + // shared fields of previous `Thread` must be initialized before later ones are marked as active + const new_thread_index = @atomicLoad(u32, &el.threads.active, .acquire); + for (0..max_idle_search) |_| { + defer thread.idle_search_index += 1; + if (thread.idle_search_index == new_thread_index) thread.idle_search_index = 0; + const idle_search_thread = &el.threads.allocated[thread.idle_search_index]; + if (@cmpxchgWeak( + ?*Fiber, + &idle_search_thread.ready_queue, + null, + ready_queue.head, + .acq_rel, + .monotonic, + )) |_| continue; + getSqe(&thread.io_uring).* = .{ + .opcode = .MSG_RING, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = idle_search_thread.io_uring.fd, + .off = @intFromEnum(Completion.Key.wakeup), + .addr = 0, + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.Key.wakeup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; 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; + spawn_thread: { + // previous failed reservations must have completed before retrying + if (new_thread_index == el.threads.allocated.len or @cmpxchgWeak( + u32, + &el.threads.reserved, + new_thread_index, + new_thread_index + 1, + .acquire, + .monotonic, + ) != null) break :spawn_thread; + const new_thread = &el.threads.allocated[new_thread_index]; + const next_thread_index = new_thread_index + 1; + new_thread.* = .{ + .thread = undefined, + .idle_context = undefined, + .current_context = &new_thread.idle_context, + .ready_queue = ready_queue.head, + .free_queue = null, + .io_uring = IoUring.init(io_uring_entries, 0) catch |err| { + @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due to io_uring init failure: {s}", .{@errorName(err)}); + break :spawn_thread; + }, + .idle_search_index = next_thread_index, + .steal_ready_search_index = next_thread_index, + }; + new_thread.thread = std.Thread.spawn(.{ + .stack_size = idle_stack_size, + .allocator = el.gpa, + }, threadEntry, .{ el, new_thread_index }) catch |err| { + new_thread.io_uring.deinit(); + @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due spawn failure: {s}", .{@errorName(err)}); + break :spawn_thread; + }; + // shared fields of `Thread` must be initialized before being marked active + @atomicStore(u32, &el.threads.active, next_thread_index, .release); return; - }; + } + // nobody wanted it, so just queue it on ourselves + while (@cmpxchgWeak( + ?*Fiber, + &thread.ready_queue, + ready_queue.tail.queue_next, + ready_queue.head, + .acq_rel, + .acquire, + )) |old_head| ready_queue.tail.queue_next = old_head; } fn recycle(el: *EventLoop, fiber: *Fiber) void { + const thread: *Thread = .current(el); std.log.debug("recyling {*}", .{fiber}); + assert(fiber.queue_next == null); @memset(fiber.allocatedSlice(), undefined); - el.mutex.lock(); - defer el.mutex.unlock(); - el.free.append(&fiber.queue_node); + fiber.queue_next = thread.free_queue; + thread.free_queue = fiber; } fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { message.handle(el); - el.idle(); + const thread: *Thread = &el.threads.allocated[0]; + el.idle(thread); 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]; +fn threadEntry(el: *EventLoop, index: u32) void { + Thread.index = index; + const thread: *Thread = &el.threads.allocated[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(); + el.idle(thread); } -const CompletionKey = enum(u64) { - queue_len_futex_wait = 1, - _, +const Completion = struct { + const Key = enum(usize) { + unused, + wakeup, + cancel, + cleanup, + exit, + /// *Fiber + _, + }; + result: i32, + flags: u32, }; -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; - +fn idle(el: *EventLoop, thread: *Thread) void { + var maybe_ready_fiber: ?*Fiber = null; 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)), + el.yield(maybe_ready_fiber, .nothing); + maybe_ready_fiber = null; + _ = thread.io_uring.submit_and_wait(1) catch |err| switch (err) { + error.SignalInterrupt => std.log.warn("submit_and_wait failed with SignalInterrupt", .{}), + else => |e| @panic(@errorName(e)), }; - for (cqes_buffer[0 .. iou.copy_cqes(&cqes_buffer, 1) catch |err| switch (err) { + var cqes_buffer: [io_uring_entries]std.os.linux.io_uring_cqe = undefined; + var maybe_ready_queue: ?Fiber.Queue = null; + for (cqes_buffer[0 .. thread.io_uring.copy_cqes(&cqes_buffer, 0) catch |err| switch (err) { error.SignalInterrupt => cqes_len: { - std.log.debug("copy_cqes: SignalInterrupt", .{}); + std.log.warn("copy_cqes failed with 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; + else => |e| @panic(@errorName(e)), + }]) |cqe| switch (@as(Completion.Key, @enumFromInt(cqe.user_data))) { + .unused => unreachable, // bad submission queued? + .wakeup => {}, + .cancel => {}, + .cleanup => @panic("failed to notify other threads that we are exiting"), + .exit => { + assert(maybe_ready_fiber == null and maybe_ready_queue == null); // pending async + return; }, _ => { const fiber: *Fiber = @ptrFromInt(cqe.user_data); - const res: *i32 = @ptrCast(@alignCast(fiber.resultPointer())); - res.* = cqe.res; - el.schedule(fiber); + assert(fiber.queue_next == null); + fiber.resultPointer(Completion).* = .{ + .result = cqe.res, + .flags = cqe.flags, + }; + if (maybe_ready_fiber == null) maybe_ready_fiber = fiber else if (maybe_ready_queue) |*ready_queue| { + ready_queue.tail.queue_next = fiber; + ready_queue.tail = fiber; + } else maybe_ready_queue = .{ .head = fiber, .tail = fiber }; }, }; + if (maybe_ready_queue) |ready_queue| el.schedule(thread, ready_queue); } } @@ -296,18 +409,37 @@ const SwitchMessage = struct { }; fn handle(message: *const SwitchMessage, el: *EventLoop) void { - const thread: *Thread = &el.threads.items[thread_index]; + const thread: *Thread = .current(el); 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); + if (@atomicRmw( + ?*Fiber, + awaiter, + .Xchg, + prev_fiber, + .acq_rel, + ) == Fiber.finished) el.schedule(thread, .{ .head = prev_fiber, .tail = 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 + .exit => for (el.threads.allocated[0..@atomicLoad(u32, &el.threads.active, .acquire)]) |*each_thread| { + getSqe(&thread.io_uring).* = .{ + .opcode = .MSG_RING, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = each_thread.io_uring.fd, + .off = @intFromEnum(Completion.Key.exit), + .addr = 0, + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.Key.cleanup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; }, } } @@ -374,7 +506,27 @@ fn fiberEntry() callconv(.naked) void { } } -pub fn @"async"( +const AsyncClosure = struct { + event_loop: *EventLoop, + fiber: *Fiber, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + result_align: Alignment, + + 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.resultBytes(closure.result_align)); + const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + closure.event_loop.yield(awaiter, .nothing); + unreachable; // switched to dead fiber + } +}; + +fn @"async"( userdata: ?*anyopaque, result: []u8, result_alignment: Alignment, @@ -407,58 +559,79 @@ pub fn @"async"( else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), }, .awaiter = null, - .queue_node = undefined, - .result_align = result_alignment, + .queue_next = null, + .can_cancel = false, + .canceled = false, }; closure.* = .{ .event_loop = event_loop, .fiber = fiber, .start = start, + .result_align = result_alignment, }; @memcpy(closure.contextPointer(), context); - event_loop.schedule(fiber); + event_loop.schedule(.current(event_loop), .{ .head = fiber, .tail = 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 { +fn @"await"( + userdata: ?*anyopaque, + any_future: *std.Io.AnyFuture, + result: []u8, + result_alignment: Alignment, +) 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()); + @memcpy(result, future_fiber.resultBytes(result_alignment)); event_loop.recycle(future_fiber); } -pub fn cancel(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void { +fn cancel( + userdata: ?*anyopaque, + any_future: *std.Io.AnyFuture, + result: []u8, + result_alignment: Alignment, +) 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); + @atomicStore(bool, &future_fiber.canceled, true, .release); + if (@atomicLoad(bool, &future_fiber.can_cancel, .acquire)) { + const thread: *Thread = .current(event_loop); + getSqe(&thread.io_uring).* = .{ + .opcode = .ASYNC_CANCEL, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = 0, + .off = 0, + .addr = @intFromPtr(future_fiber), + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.Key.cancel), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + } + @"await"(userdata, any_future, result, result_alignment); } -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)); +fn cancelRequested(userdata: ?*anyopaque) bool { + const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); + const thread: *Thread = .current(event_loop); + return thread.currentFiber().canceled; +} + +pub fn createFile( + userdata: ?*anyopaque, + dir: std.fs.Dir, + sub_path: []const u8, + flags: Io.CreateFlags, +) Io.FileOpenError!std.fs.File { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); const posix = std.posix; const sub_path_c = try posix.toPosixPath(sub_path); @@ -497,22 +670,24 @@ pub fn createFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, @panic("TODO"); } - const thread: *Thread = &el.threads.items[thread_index]; + const thread: *Thread = .current(el); const iou = &thread.io_uring; - const sqe = getSqe(iou); const fiber = thread.currentFiber(); + if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + const sqe = getSqe(iou); sqe.prep_openat(dir.fd, &sub_path_c, os_flags, flags.mode); sqe.user_data = @intFromPtr(fiber); + @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); + @atomicStore(bool, &fiber.can_cancel, false, .release); - const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)])); - const rc = result.*; - switch (errno(rc)) { - .SUCCESS => return .{ .handle = rc }, + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return .{ .handle = completion.result }, .INTR => @panic("TODO is this reachable?"), - .CANCELED => @panic("TODO figure out how this error code fits into things"), + .CANCELED => return error.AsyncCancel, .FAULT => unreachable, .INVAL => return error.BadPathName, @@ -541,8 +716,17 @@ pub fn createFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, } } -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)); +pub fn openFile( + userdata: ?*anyopaque, + dir: std.fs.Dir, + sub_path: []const u8, + flags: Io.OpenFlags, +) Io.FileOpenError!std.fs.File { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const thread: *Thread = .current(el); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; const posix = std.posix; const sub_path_c = try posix.toPosixPath(sub_path); @@ -587,22 +771,19 @@ pub fn openFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, fl @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); + @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); + @atomicStore(bool, &fiber.can_cancel, false, .release); - const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)])); - const rc = result.*; - switch (errno(rc)) { - .SUCCESS => return .{ .handle = rc }, + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return .{ .handle = completion.result }, .INTR => @panic("TODO is this reachable?"), - .CANCELED => @panic("TODO figure out how this error code fits into things"), + .CANCELED => return error.AsyncCancel, .FAULT => unreachable, .INVAL => return error.BadPathName, @@ -631,63 +812,49 @@ pub fn openFile(userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, fl } } -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 el: *EventLoop = @alignCast(@ptrCast(userdata)); + const thread: *Thread = .current(el); const iou = &thread.io_uring; - const sqe = getSqe(iou); const fiber = thread.currentFiber(); + const sqe = getSqe(iou); 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)) { + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { .SUCCESS => return, .INTR => @panic("TODO is this reachable?"), - .CANCELED => @panic("TODO figure out how this error code fits into things"), + .CANCELED => return, .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]; +pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) Io.FileReadError!usize { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const thread: *Thread = .current(el); const iou = &thread.io_uring; - const sqe = getSqe(iou); const fiber = thread.currentFiber(); + if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + const sqe = getSqe(iou); sqe.prep_read(file.handle, buffer, std.math.maxInt(u64)); sqe.user_data = @intFromPtr(fiber); + @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); + @atomicStore(bool, &fiber.can_cancel, false, .release); - const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)])); - const rc = result.*; - switch (errno(rc)) { - .SUCCESS => return @as(u32, @bitCast(rc)), + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return @as(u32, @bitCast(completion.result)), .INTR => @panic("TODO is this reachable?"), - .CANCELED => @panic("TODO figure out how this error code fits into things"), + .CANCELED => return error.AsyncCancel, .INVAL => unreachable, .FAULT => unreachable, @@ -701,31 +868,31 @@ pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) std.fs.File. .NOTCONN => return error.SocketNotConnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, - else => |err| return posix.unexpectedErrno(err), + else => |err| return std.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)); +pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) Io.FileWriteError!usize { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); - const posix = std.posix; - - const thread: *Thread = &el.threads.items[thread_index]; + const thread: *Thread = .current(el); const iou = &thread.io_uring; - const sqe = getSqe(iou); const fiber = thread.currentFiber(); + if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + const sqe = getSqe(iou); sqe.prep_write(file.handle, buffer, std.math.maxInt(u64)); sqe.user_data = @intFromPtr(fiber); + @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); + @atomicStore(bool, &fiber.can_cancel, false, .release); - const result: *i32 = @alignCast(@ptrCast(fiber.resultPointer()[0..@sizeOf(posix.fd_t)])); - const rc = result.*; - switch (errno(rc)) { - .SUCCESS => return @as(u32, @bitCast(rc)), + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS => return @as(u32, @bitCast(completion.result)), .INTR => @panic("TODO is this reachable?"), - .CANCELED => @panic("TODO figure out how this error code fits into things"), + .CANCELED => return error.AsyncCancel, .INVAL => return error.InvalidArgument, .FAULT => unreachable, @@ -744,6 +911,15 @@ pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) std.f .BUSY => return error.DeviceBusy, .NXIO => return error.NoDevice, .MSGSIZE => return error.MessageTooBig, - else => |err| return posix.unexpectedErrno(err), + else => |err| return std.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"); +} diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 03fdbe21a1..0cddadf40d 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -435,13 +435,25 @@ fn @"async"( return @ptrCast(closure); } -fn @"await"(userdata: ?*anyopaque, any_future: *Io.AnyFuture, result: []u8) void { +fn @"await"( + userdata: ?*anyopaque, + any_future: *std.Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = result_alignment; const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); closure.waitAndFree(pool.allocator, result); } -fn cancel(userdata: ?*anyopaque, any_future: *Io.AnyFuture, result: []u8) void { +fn cancel( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = result_alignment; const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); @atomicStore(bool, &closure.cancel_flag, true, .seq_cst); From 08b609a79ff151e747c6b860c90b634daaa68f1c Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Sun, 30 Mar 2025 15:13:41 -0400 Subject: [PATCH 020/244] Io: implement sleep and fix cancel bugs --- lib/std/Io.zig | 49 ++++- lib/std/Io/EventLoop.zig | 408 +++++++++++++++++++++++++++------------ lib/std/Thread/Pool.zig | 108 ++++++++++- lib/std/start.zig | 34 ++-- lib/std/std.zig | 3 + 5 files changed, 449 insertions(+), 153 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 256e113ba0..219c0daee8 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -579,7 +579,6 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*AnyFuture, - /// This function is only called when `async` returns a non-null value. /// /// Thread-safe. @@ -609,7 +608,6 @@ pub const VTable = struct { result: []u8, result_alignment: std.mem.Alignment, ) void, - /// Returns whether the current thread of execution is known to have /// been requested to cancel. /// @@ -619,8 +617,11 @@ pub const VTable = struct { createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, closeFile: *const fn (?*anyopaque, fs.File) void, - read: *const fn (?*anyopaque, file: fs.File, buffer: []u8) FileReadError!usize, - write: *const fn (?*anyopaque, file: fs.File, buffer: []const u8) FileWriteError!usize, + pread: *const fn (?*anyopaque, file: fs.File, buffer: []u8, offset: std.posix.off_t) FilePReadError!usize, + pwrite: *const fn (?*anyopaque, file: fs.File, buffer: []const u8, offset: std.posix.off_t) FilePWriteError!usize, + + now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp, + sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, }; pub const OpenFlags = fs.File.OpenFlags; @@ -628,7 +629,27 @@ pub const CreateFlags = fs.File.CreateFlags; pub const FileOpenError = fs.File.OpenError || error{AsyncCancel}; pub const FileReadError = fs.File.ReadError || error{AsyncCancel}; +pub const FilePReadError = fs.File.PReadError || error{AsyncCancel}; pub const FileWriteError = fs.File.WriteError || error{AsyncCancel}; +pub const FilePWriteError = fs.File.PWriteError || error{AsyncCancel}; + +pub const Timestamp = enum(i96) { + _, + + pub fn durationTo(from: Timestamp, to: Timestamp) i96 { + return @intFromEnum(to) - @intFromEnum(from); + } + + pub fn addDuration(from: Timestamp, duration: i96) Timestamp { + return @enumFromInt(@intFromEnum(from) + duration); + } +}; +pub const Deadline = union(enum) { + nanoseconds: i96, + timestamp: Timestamp, +}; +pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{AsyncCancel}; +pub const SleepError = error{ UnsupportedClock, Unexpected, AsyncCancel }; pub const AnyFuture = opaque {}; @@ -694,11 +715,19 @@ pub fn closeFile(io: Io, file: fs.File) void { } pub fn read(io: Io, file: fs.File, buffer: []u8) FileReadError!usize { - return io.vtable.read(io.userdata, file, buffer); + return @errorCast(io.pread(file, buffer, -1)); +} + +pub fn pread(io: Io, file: fs.File, buffer: []u8, offset: std.posix.off_t) FilePReadError!usize { + return io.vtable.pread(io.userdata, file, buffer, offset); } pub fn write(io: Io, file: fs.File, buffer: []const u8) FileWriteError!usize { - return io.vtable.write(io.userdata, file, buffer); + return @errorCast(io.pwrite(file, buffer, -1)); +} + +pub fn pwrite(io: Io, file: fs.File, buffer: []const u8, offset: std.posix.off_t) FilePWriteError!usize { + return io.vtable.pwrite(io.userdata, file, buffer, offset); } pub fn writeAll(io: Io, file: fs.File, bytes: []const u8) FileWriteError!void { @@ -717,3 +746,11 @@ pub fn readAll(io: Io, file: fs.File, buffer: []u8) FileReadError!usize { } return index; } + +pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { + return io.vtable.now(io.userdata, clockid); +} + +pub fn sleep(io: Io, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void { + return io.vtable.sleep(io.userdata, clockid, deadline); +} diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 5de161d9f9..a24d5173e2 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -31,10 +31,12 @@ const Thread = struct { idle_search_index: u32, steal_ready_search_index: u32, - threadlocal var index: u32 = undefined; + const canceling: ?*Thread = @ptrFromInt(@alignOf(Thread)); - fn current(el: *EventLoop) *Thread { - return &el.threads.allocated[index]; + threadlocal var self: *Thread = undefined; + + fn current() *Thread { + return self; } fn currentFiber(thread: *Thread) *Fiber { @@ -52,10 +54,9 @@ const Fiber = struct { context: Context, awaiter: ?*Fiber, queue_next: ?*Fiber, - can_cancel: bool, - canceled: bool, + cancel_thread: ?*Thread, - const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); + const finished: ?*Fiber = @ptrFromInt(@alignOf(Thread)); const max_result_align: Alignment = .@"16"; const max_result_size = max_result_align.forward(64); @@ -75,7 +76,7 @@ const Fiber = struct { ); fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber { - const thread: *Thread = .current(el); + const thread: *Thread = .current(); if (thread.free_queue) |free_fiber| { thread.free_queue = free_fiber.queue_next; free_fiber.queue_next = null; @@ -101,6 +102,40 @@ const Fiber = struct { return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); } + fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{AsyncCancel}!void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + null, + thread, + .acq_rel, + .acquire, + )) |cancel_thread| { + assert(cancel_thread == Thread.canceling); + return error.AsyncCancel; + } + } + + fn exitCancelRegion(fiber: *Fiber, thread: *Thread) void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + thread, + null, + .acq_rel, + .acquire, + )) |cancel_thread| assert(cancel_thread == Thread.canceling); + } + + fn recycle(fiber: *Fiber) void { + const thread: *Thread = .current(); + std.log.debug("recyling {*}", .{fiber}); + assert(fiber.queue_next == null); + @memset(fiber.allocatedSlice(), undefined); + fiber.queue_next = thread.free_queue; + thread.free_queue = fiber; + } + const Queue = struct { head: *Fiber, tail: *Fiber }; }; @@ -110,13 +145,18 @@ pub fn io(el: *EventLoop) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .cancel = cancel, .cancelRequested = cancelRequested, + .createFile = createFile, .openFile = openFile, .closeFile = closeFile, - .read = read, - .write = write, + .pread = pread, + .pwrite = pwrite, + + .now = now, + .sleep = sleep, }, }; } @@ -133,8 +173,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { .context = undefined, .awaiter = null, .queue_next = null, - .can_cancel = false, - .canceled = false, + .cancel_thread = null, }, .threads = .{ .allocated = @ptrCast(allocated_slice[0..threads_size]), @@ -142,8 +181,8 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { .active = 1, }, }; - Thread.index = 0; const main_thread = &el.threads.allocated[0]; + Thread.self = main_thread; const idle_stack_end: [*]usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; main_thread.* = .{ @@ -168,24 +207,22 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { pub fn deinit(el: *EventLoop) void { const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); for (el.threads.allocated[0..active_threads]) |*thread| - assert(@atomicLoad(?*Fiber, &thread.ready_queue, .unordered) == null); // pending async + assert(@atomicLoad(?*Fiber, &thread.ready_queue, .acquire) == null); // pending async el.yield(null, .exit); + for (el.threads.allocated[0..active_threads]) |*thread| while (thread.free_queue) |free_fiber| { + thread.free_queue = free_fiber.queue_next; + free_fiber.queue_next = null; + el.gpa.free(free_fiber.allocatedSlice()); + }; const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.allocated.ptr)); const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); - for (el.threads.allocated[1..active_threads]) |*thread| { - thread.thread.join(); - while (thread.free_queue) |free_fiber| { - thread.free_queue = free_fiber.queue_next; - free_fiber.queue_next = null; - el.gpa.free(free_fiber.allocatedSlice()); - } - } + for (el.threads.allocated[1..active_threads]) |thread| thread.thread.join(); el.gpa.free(allocated_ptr[0..idle_stack_end_offset]); el.* = undefined; } fn yield(el: *EventLoop, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void { - const thread: *Thread = .current(el); + const thread: *Thread = .current(); const ready_context: *Context = if (maybe_ready_fiber) |ready_fiber| &ready_fiber.context else if (thread.ready_queue) |ready_fiber| ready_context: { @@ -198,6 +235,7 @@ fn yield(el: *EventLoop, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage defer thread.steal_ready_search_index += 1; if (thread.steal_ready_search_index == ready_threads) thread.steal_ready_search_index = 0; const steal_ready_search_thread = &el.threads.allocated[thread.steal_ready_search_index]; + if (steal_ready_search_thread == thread) continue; const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; if (@cmpxchgWeak( ?*Fiber, @@ -236,6 +274,7 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { defer thread.idle_search_index += 1; if (thread.idle_search_index == new_thread_index) thread.idle_search_index = 0; const idle_search_thread = &el.threads.allocated[thread.idle_search_index]; + if (idle_search_thread == thread) continue; if (@cmpxchgWeak( ?*Fiber, &idle_search_thread.ready_queue, @@ -249,11 +288,11 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, .ioprio = 0, .fd = idle_search_thread.io_uring.fd, - .off = @intFromEnum(Completion.Key.wakeup), + .off = @intFromEnum(Completion.UserData.wakeup), .addr = 0, .len = 0, .rw_flags = 0, - .user_data = @intFromEnum(Completion.Key.wakeup), + .user_data = @intFromEnum(Completion.UserData.wakeup), .buf_index = 0, .personality = 0, .splice_fd_in = 0, @@ -314,15 +353,6 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { )) |old_head| ready_queue.tail.queue_next = old_head; } -fn recycle(el: *EventLoop, fiber: *Fiber) void { - const thread: *Thread = .current(el); - std.log.debug("recyling {*}", .{fiber}); - assert(fiber.queue_next == null); - @memset(fiber.allocatedSlice(), undefined); - fiber.queue_next = thread.free_queue; - thread.free_queue = fiber; -} - fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { message.handle(el); const thread: *Thread = &el.threads.allocated[0]; @@ -332,17 +362,16 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAl } fn threadEntry(el: *EventLoop, index: u32) void { - Thread.index = index; const thread: *Thread = &el.threads.allocated[index]; + Thread.self = thread; std.log.debug("created thread idle {*}", .{&thread.idle_context}); el.idle(thread); } const Completion = struct { - const Key = enum(usize) { + const UserData = enum(usize) { unused, wakeup, - cancel, cleanup, exit, /// *Fiber @@ -369,26 +398,43 @@ fn idle(el: *EventLoop, thread: *Thread) void { break :cqes_len 0; }, else => |e| @panic(@errorName(e)), - }]) |cqe| switch (@as(Completion.Key, @enumFromInt(cqe.user_data))) { + }]) |cqe| switch (@as(Completion.UserData, @enumFromInt(cqe.user_data))) { .unused => unreachable, // bad submission queued? .wakeup => {}, - .cancel => {}, .cleanup => @panic("failed to notify other threads that we are exiting"), .exit => { assert(maybe_ready_fiber == null and maybe_ready_queue == null); // pending async return; }, - _ => { - const fiber: *Fiber = @ptrFromInt(cqe.user_data); - assert(fiber.queue_next == null); - fiber.resultPointer(Completion).* = .{ - .result = cqe.res, - .flags = cqe.flags, - }; - if (maybe_ready_fiber == null) maybe_ready_fiber = fiber else if (maybe_ready_queue) |*ready_queue| { - ready_queue.tail.queue_next = fiber; - ready_queue.tail = fiber; - } else maybe_ready_queue = .{ .head = fiber, .tail = fiber }; + _ => switch (errno(cqe.res)) { + .INTR => getSqe(&thread.io_uring).* = .{ + .opcode = .ASYNC_CANCEL, + .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, + .ioprio = 0, + .fd = 0, + .off = 0, + .addr = cqe.user_data, + .len = 0, + .rw_flags = 0, + .user_data = @intFromEnum(Completion.UserData.wakeup), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }, + else => { + const fiber: *Fiber = @ptrFromInt(cqe.user_data); + assert(fiber.queue_next == null); + fiber.resultPointer(Completion).* = .{ + .result = cqe.res, + .flags = cqe.flags, + }; + if (maybe_ready_fiber == null) maybe_ready_fiber = fiber else if (maybe_ready_queue) |*ready_queue| { + ready_queue.tail.queue_next = fiber; + ready_queue.tail = fiber; + } else maybe_ready_queue = .{ .head = fiber, .tail = fiber }; + }, }, }; if (maybe_ready_queue) |ready_queue| el.schedule(thread, ready_queue); @@ -409,7 +455,7 @@ const SwitchMessage = struct { }; fn handle(message: *const SwitchMessage, el: *EventLoop) void { - const thread: *Thread = .current(el); + const thread: *Thread = .current(); thread.current_context = message.contexts.ready; switch (message.pending_task) { .nothing => {}, @@ -429,11 +475,11 @@ const SwitchMessage = struct { .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, .ioprio = 0, .fd = each_thread.io_uring.fd, - .off = @intFromEnum(Completion.Key.exit), + .off = @intFromEnum(Completion.UserData.exit), .addr = 0, .len = 0, .rw_flags = 0, - .user_data = @intFromEnum(Completion.Key.cleanup), + .user_data = @intFromEnum(Completion.UserData.cleanup), .buf_index = 0, .personality = 0, .splice_fd_in = 0, @@ -544,6 +590,7 @@ fn @"async"( start(context.ptr, result.ptr); return null; }; + errdefer fiber.recycle(); std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( @@ -560,8 +607,7 @@ fn @"async"( }, .awaiter = null, .queue_next = null, - .can_cancel = false, - .canceled = false, + .cancel_thread = null, }; closure.* = .{ .event_loop = event_loop, @@ -571,7 +617,7 @@ fn @"async"( }; @memcpy(closure.contextPointer(), context); - event_loop.schedule(.current(event_loop), .{ .head = fiber, .tail = fiber }); + event_loop.schedule(.current(), .{ .head = fiber, .tail = fiber }); return @ptrCast(fiber); } @@ -585,7 +631,7 @@ fn @"await"( 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.resultBytes(result_alignment)); - event_loop.recycle(future_fiber); + future_fiber.recycle(); } fn cancel( @@ -594,35 +640,37 @@ fn cancel( result: []u8, result_alignment: Alignment, ) void { - const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); - @atomicStore(bool, &future_fiber.canceled, true, .release); - if (@atomicLoad(bool, &future_fiber.can_cancel, .acquire)) { - const thread: *Thread = .current(event_loop); - getSqe(&thread.io_uring).* = .{ - .opcode = .ASYNC_CANCEL, + if (@atomicRmw( + ?*Thread, + &future_fiber.cancel_thread, + .Xchg, + Thread.canceling, + .acq_rel, + )) |cancel_thread| if (cancel_thread != Thread.canceling) { + getSqe(&Thread.current().io_uring).* = .{ + .opcode = .MSG_RING, .flags = std.os.linux.IOSQE_CQE_SKIP_SUCCESS, .ioprio = 0, - .fd = 0, - .off = 0, - .addr = @intFromPtr(future_fiber), - .len = 0, + .fd = cancel_thread.io_uring.fd, + .off = @intFromPtr(future_fiber), + .addr = 0, + .len = @bitCast(-@as(i32, @intFromEnum(std.os.linux.E.INTR))), .rw_flags = 0, - .user_data = @intFromEnum(Completion.Key.cancel), + .user_data = @intFromEnum(Completion.UserData.cleanup), .buf_index = 0, .personality = 0, .splice_fd_in = 0, .addr3 = 0, .resv = 0, }; - } + }; @"await"(userdata, any_future, result, result_alignment); } fn cancelRequested(userdata: ?*anyopaque) bool { - const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); - const thread: *Thread = .current(event_loop); - return thread.currentFiber().canceled; + _ = userdata; + return @atomicLoad(?*Thread, &Thread.current().currentFiber().cancel_thread, .acquire) == Thread.canceling; } pub fn createFile( @@ -632,6 +680,10 @@ pub fn createFile( flags: Io.CreateFlags, ) Io.FileOpenError!std.fs.File { const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); const posix = std.posix; const sub_path_c = try posix.toPosixPath(sub_path); @@ -670,23 +722,30 @@ pub fn createFile( @panic("TODO"); } - const thread: *Thread = .current(el); - const iou = &thread.io_uring; - const fiber = thread.currentFiber(); - if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + getSqe(iou).* = .{ + .opcode = .OPENAT, + .flags = 0, + .ioprio = 0, + .fd = dir.fd, + .off = 0, + .addr = @intFromPtr(&sub_path_c), + .len = @intCast(flags.mode), + .rw_flags = @bitCast(os_flags), + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; - const sqe = getSqe(iou); - sqe.prep_openat(dir.fd, &sub_path_c, os_flags, flags.mode); - sqe.user_data = @intFromPtr(fiber); - - @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); - @atomicStore(bool, &fiber.can_cancel, false, .release); + fiber.exitCancelRegion(thread); const completion = fiber.resultPointer(Completion); switch (errno(completion.result)) { .SUCCESS => return .{ .handle = completion.result }, - .INTR => @panic("TODO is this reachable?"), + .INTR => unreachable, .CANCELED => return error.AsyncCancel, .FAULT => unreachable, @@ -723,10 +782,10 @@ pub fn openFile( flags: Io.OpenFlags, ) Io.FileOpenError!std.fs.File { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - const thread: *Thread = .current(el); + const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); - if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + try fiber.enterCancelRegion(thread); const posix = std.posix; const sub_path_c = try posix.toPosixPath(sub_path); @@ -771,18 +830,30 @@ pub fn openFile( @panic("TODO"); } - const sqe = getSqe(iou); - sqe.prep_openat(dir.fd, &sub_path_c, os_flags, 0); - sqe.user_data = @intFromPtr(fiber); + getSqe(iou).* = .{ + .opcode = .OPENAT, + .flags = 0, + .ioprio = 0, + .fd = dir.fd, + .off = 0, + .addr = @intFromPtr(&sub_path_c), + .len = 0, + .rw_flags = @bitCast(os_flags), + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; - @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); - @atomicStore(bool, &fiber.can_cancel, false, .release); + fiber.exitCancelRegion(thread); const completion = fiber.resultPointer(Completion); switch (errno(completion.result)) { .SUCCESS => return .{ .handle = completion.result }, - .INTR => @panic("TODO is this reachable?"), + .INTR => unreachable, .CANCELED => return error.AsyncCancel, .FAULT => unreachable, @@ -814,20 +885,33 @@ pub fn openFile( pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - const thread: *Thread = .current(el); + const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); - const sqe = getSqe(iou); - sqe.prep_close(file.handle); - sqe.user_data = @intFromPtr(fiber); + getSqe(iou).* = .{ + .opcode = .CLOSE, + .flags = 0, + .ioprio = 0, + .fd = file.handle, + .off = 0, + .addr = 0, + .len = 0, + .rw_flags = 0, + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; el.yield(null, .nothing); const completion = fiber.resultPointer(Completion); switch (errno(completion.result)) { .SUCCESS => return, - .INTR => @panic("TODO is this reachable?"), + .INTR => unreachable, .CANCELED => return, .BADF => unreachable, // Always a race condition. @@ -835,25 +919,37 @@ pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { } } -pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) Io.FileReadError!usize { +pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - const thread: *Thread = .current(el); + const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); - if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + try fiber.enterCancelRegion(thread); - const sqe = getSqe(iou); - sqe.prep_read(file.handle, buffer, std.math.maxInt(u64)); - sqe.user_data = @intFromPtr(fiber); + getSqe(iou).* = .{ + .opcode = .READ, + .flags = 0, + .ioprio = 0, + .fd = file.handle, + .off = @bitCast(offset), + .addr = @intFromPtr(buffer.ptr), + .len = @min(buffer.len, 0x7ffff000), + .rw_flags = 0, + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; - @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); - @atomicStore(bool, &fiber.can_cancel, false, .release); + fiber.exitCancelRegion(thread); const completion = fiber.resultPointer(Completion); switch (errno(completion.result)) { .SUCCESS => return @as(u32, @bitCast(completion.result)), - .INTR => @panic("TODO is this reachable?"), + .INTR => unreachable, .CANCELED => return error.AsyncCancel, .INVAL => unreachable, @@ -868,30 +964,44 @@ pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) Io.FileReadE .NOTCONN => return error.SocketNotConnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, else => |err| return std.posix.unexpectedErrno(err), } } -pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) Io.FileWriteError!usize { +pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - - const thread: *Thread = .current(el); + const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); - if (@atomicLoad(bool, &fiber.canceled, .acquire)) return error.AsyncCancel; + try fiber.enterCancelRegion(thread); - const sqe = getSqe(iou); - sqe.prep_write(file.handle, buffer, std.math.maxInt(u64)); - sqe.user_data = @intFromPtr(fiber); + getSqe(iou).* = .{ + .opcode = .WRITE, + .flags = 0, + .ioprio = 0, + .fd = file.handle, + .off = @bitCast(offset), + .addr = @intFromPtr(buffer.ptr), + .len = @min(buffer.len, 0x7ffff000), + .rw_flags = 0, + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; - @atomicStore(bool, &fiber.can_cancel, true, .release); el.yield(null, .nothing); - @atomicStore(bool, &fiber.can_cancel, false, .release); + fiber.exitCancelRegion(thread); const completion = fiber.resultPointer(Completion); switch (errno(completion.result)) { .SUCCESS => return @as(u32, @bitCast(completion.result)), - .INTR => @panic("TODO is this reachable?"), + .INTR => unreachable, .CANCELED => return error.AsyncCancel, .INVAL => return error.InvalidArgument, @@ -907,17 +1017,77 @@ pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) Io.Fi .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, .PIPE => return error.BrokenPipe, - .CONNRESET => return error.ConnectionResetByPeer, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, .BUSY => return error.DeviceBusy, - .NXIO => return error.NoDevice, + .CONNRESET => return error.ConnectionResetByPeer, .MSGSIZE => return error.MessageTooBig, else => |err| return std.posix.unexpectedErrno(err), } } -fn errno(signed: i32) std.posix.E { - const int = if (signed > -4096 and signed < 0) -signed else 0; - return @enumFromInt(int); +pub fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { + _ = userdata; + const timespec = try std.posix.clock_gettime(clockid); + return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); +} + +pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const thread: *Thread = .current(); + const iou = &thread.io_uring; + const fiber = thread.currentFiber(); + try fiber.enterCancelRegion(thread); + + const deadline_nanoseconds: i96 = switch (deadline) { + .nanoseconds => |nanoseconds| nanoseconds, + .timestamp => |timestamp| @intFromEnum(timestamp), + }; + const timespec: std.os.linux.kernel_timespec = .{ + .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), + .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), + }; + getSqe(iou).* = .{ + .opcode = .TIMEOUT, + .flags = 0, + .ioprio = 0, + .fd = 0, + .off = 0, + .addr = @intFromPtr(×pec), + .len = 1, + .rw_flags = @as(u32, switch (deadline) { + .nanoseconds => 0, + .timestamp => std.os.linux.IORING_TIMEOUT_ABS, + }) | @as(u32, switch (clockid) { + .REALTIME => std.os.linux.IORING_TIMEOUT_REALTIME, + .MONOTONIC => 0, + .BOOTTIME => std.os.linux.IORING_TIMEOUT_BOOTTIME, + else => return error.UnsupportedClock, + }), + .user_data = @intFromPtr(fiber), + .buf_index = 0, + .personality = 0, + .splice_fd_in = 0, + .addr3 = 0, + .resv = 0, + }; + + el.yield(null, .nothing); + fiber.exitCancelRegion(thread); + + const completion = fiber.resultPointer(Completion); + switch (errno(completion.result)) { + .SUCCESS, .TIME => return, + .INTR => unreachable, + .CANCELED => return error.AsyncCancel, + + else => |err| return std.posix.unexpectedErrno(err), + } +} + +fn errno(signed: i32) std.os.linux.E { + return .init(@bitCast(@as(isize, signed))); } fn getSqe(iou: *IoUring) *std.os.linux.io_uring_sqe { diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 0cddadf40d..6ec6c89040 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -332,13 +332,18 @@ pub fn io(pool: *Pool) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .cancel = cancel, .cancelRequested = cancelRequested, + .createFile = createFile, .openFile = openFile, .closeFile = closeFile, - .read = read, - .write = write, + .pread = pread, + .pwrite = pwrite, + + .now = now, + .sleep = sleep, }, }; } @@ -347,15 +352,44 @@ const AsyncClosure = struct { func: *const fn (context: *anyopaque, result: *anyopaque) void, runnable: Runnable = .{ .runFn = runFn }, reset_event: std.Thread.ResetEvent, - cancel_flag: bool, + cancel_tid: std.Thread.Id, context_offset: usize, result_offset: usize, + const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { + .int => |int_info| switch (int_info.signedness) { + .signed => -1, + .unsigned => std.math.maxInt(std.Thread.Id), + }, + .pointer => @ptrFromInt(std.math.maxInt(usize)), + else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), + }; + fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable)); + const tid = std.Thread.getCurrentId(); + if (@cmpxchgStrong( + std.Thread.Id, + &closure.cancel_tid, + 0, + tid, + .acq_rel, + .acquire, + )) |cancel_tid| { + assert(cancel_tid == canceling_tid); + return; + } current_closure = closure; closure.func(closure.contextPointer(), closure.resultPointer()); current_closure = null; + if (@cmpxchgStrong( + std.Thread.Id, + &closure.cancel_tid, + tid, + 0, + .acq_rel, + .acquire, + )) |cancel_tid| assert(cancel_tid == canceling_tid); closure.reset_event.set(); } @@ -414,7 +448,7 @@ fn @"async"( .context_offset = context_offset, .result_offset = result_offset, .reset_event = .{}, - .cancel_flag = false, + .cancel_tid = 0, }; @memcpy(closure.contextPointer()[0..context.len], context); pool.run_queue.prepend(&closure.runnable.node); @@ -456,7 +490,23 @@ fn cancel( _ = result_alignment; const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - @atomicStore(bool, &closure.cancel_flag, true, .seq_cst); + switch (@atomicRmw( + std.Thread.Id, + &closure.cancel_tid, + .Xchg, + AsyncClosure.canceling_tid, + .acq_rel, + )) { + 0, AsyncClosure.canceling_tid => {}, + else => |cancel_tid| switch (builtin.os.tag) { + .linux => _ = std.os.linux.tgkill( + std.os.linux.getpid(), + @bitCast(cancel_tid), + std.posix.SIG.IO, + ), + else => {}, + }, + } closure.waitAndFree(pool.allocator, result); } @@ -464,7 +514,7 @@ fn cancelRequested(userdata: ?*anyopaque) bool { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); _ = pool; const closure = current_closure orelse return false; - return @atomicLoad(bool, &closure.cancel_flag, .unordered); + return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; } fn checkCancel(pool: *Pool) error{AsyncCancel}!void { @@ -499,14 +549,52 @@ pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { return file.close(); } -pub fn read(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8) Io.FileReadError!usize { +pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); - return file.read(buffer); + return switch (offset) { + -1 => file.read(buffer), + else => file.pread(buffer, @bitCast(offset)), + }; } -pub fn write(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8) Io.FileWriteError!usize { +pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); - return file.write(buffer); + return switch (offset) { + -1 => file.write(buffer), + else => file.pwrite(buffer, @bitCast(offset)), + }; +} + +pub fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + const timespec = try std.posix.clock_gettime(clockid); + return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); +} + +pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + const deadline_nanoseconds: i96 = switch (deadline) { + .nanoseconds => |nanoseconds| nanoseconds, + .timestamp => |timestamp| @intFromEnum(timestamp), + }; + var timespec: std.posix.timespec = .{ + .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), + .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), + }; + while (true) { + try pool.checkCancel(); + switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (deadline) { + .nanoseconds => false, + .timestamp => true, + } }, ×pec, ×pec))) { + .SUCCESS => return, + .FAULT => unreachable, + .INTR => {}, + .INVAL => return error.UnsupportedClock, + else => |err| return std.posix.unexpectedErrno(err), + } + } } diff --git a/lib/std/start.zig b/lib/std/start.zig index 69207e690d..09c1f3b5c4 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -651,8 +651,8 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 { std.os.argv = argv[0..argc]; std.os.environ = envp; + maybeIgnoreSignals(); std.debug.maybeEnableSegfaultHandler(); - maybeIgnoreSigpipe(); return callMain(); } @@ -757,8 +757,8 @@ pub fn call_wWinMain() std.os.windows.INT { return root.wWinMain(hInstance, null, lpCmdLine, nCmdShow); } -fn maybeIgnoreSigpipe() void { - const have_sigpipe_support = switch (builtin.os.tag) { +fn maybeIgnoreSignals() void { + switch (builtin.os.tag) { .linux, .plan9, .illumos, @@ -773,22 +773,20 @@ fn maybeIgnoreSigpipe() void { .dragonfly, .freebsd, .serenity, - => true, - - else => false, - }; - - if (have_sigpipe_support and !std.options.keep_sigpipe) { - const posix = std.posix; - const act: posix.Sigaction = .{ - // Set handler to a noop function instead of `SIG.IGN` to prevent - // leaking signal disposition to a child process. - .handler = .{ .handler = noopSigHandler }, - .mask = posix.sigemptyset(), - .flags = 0, - }; - posix.sigaction(posix.SIG.PIPE, &act, null); + => {}, + else => return, } + const posix = std.posix; + const act: posix.Sigaction = .{ + // Set handler to a noop function instead of `SIG.IGN` to prevent + // leaking signal disposition to a child process. + .handler = .{ .handler = noopSigHandler }, + .mask = posix.sigemptyset(), + .flags = 0, + }; + if (!std.options.keep_sigpoll) posix.sigaction(posix.SIG.POLL, &act, null); + if (@hasField(posix.SIG, "IO") and posix.SIG.IO != posix.SIG.POLL and !std.options.keep_sigio) posix.sigaction(posix.SIG.IO, &act, null); + if (!std.options.keep_sigpipe) posix.sigaction(posix.SIG.PIPE, &act, null); } fn noopSigHandler(_: i32) callconv(.c) void {} diff --git a/lib/std/std.zig b/lib/std/std.zig index 4e68d1d611..6853109f26 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -145,6 +145,9 @@ pub const Options = struct { crypto_fork_safety: bool = true, + keep_sigpoll: bool = false, + keep_sigio: bool = false, + /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option /// to `true` to prevent that. /// From 0d4b358dd8fd6bf4258ac571970dae19be8133e2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 30 Mar 2025 19:56:30 -0700 Subject: [PATCH 021/244] implement Mutex, Condition, and Queue --- lib/std/Io.zig | 319 ++++++++++++++++++++++++++++++++++++++- lib/std/Io/EventLoop.zig | 14 +- lib/std/Thread/Pool.zig | 187 ++++++++++++++++++++++- 3 files changed, 497 insertions(+), 23 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 219c0daee8..9444d45414 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -6,6 +6,7 @@ const windows = std.os.windows; const posix = std.posix; const math = std.math; const assert = std.debug.assert; +const fs = std.fs; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; @@ -614,6 +615,12 @@ pub const VTable = struct { /// Thread-safe. cancelRequested: *const fn (?*anyopaque) bool, + mutexLock: *const fn (?*anyopaque, mutex: *Mutex) void, + mutexUnlock: *const fn (?*anyopaque, mutex: *Mutex) void, + + conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex, timeout_ns: ?u64) Condition.WaitError!void, + conditionWake: *const fn (?*anyopaque, cond: *Condition, notify: Condition.Notify) void, + createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, closeFile: *const fn (?*anyopaque, fs.File) void, @@ -627,11 +634,11 @@ pub const VTable = struct { pub const OpenFlags = fs.File.OpenFlags; pub const CreateFlags = fs.File.CreateFlags; -pub const FileOpenError = fs.File.OpenError || error{AsyncCancel}; -pub const FileReadError = fs.File.ReadError || error{AsyncCancel}; -pub const FilePReadError = fs.File.PReadError || error{AsyncCancel}; -pub const FileWriteError = fs.File.WriteError || error{AsyncCancel}; -pub const FilePWriteError = fs.File.PWriteError || error{AsyncCancel}; +pub const FileOpenError = fs.File.OpenError || error{Canceled}; +pub const FileReadError = fs.File.ReadError || error{Canceled}; +pub const FilePReadError = fs.File.PReadError || error{Canceled}; +pub const FileWriteError = fs.File.WriteError || error{Canceled}; +pub const FilePWriteError = fs.File.PWriteError || error{Canceled}; pub const Timestamp = enum(i96) { _, @@ -648,8 +655,8 @@ pub const Deadline = union(enum) { nanoseconds: i96, timestamp: Timestamp, }; -pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{AsyncCancel}; -pub const SleepError = error{ UnsupportedClock, Unexpected, AsyncCancel }; +pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{Canceled}; +pub const SleepError = error{ UnsupportedClock, Unexpected, Canceled }; pub const AnyFuture = opaque {}; @@ -678,6 +685,302 @@ pub fn Future(Result: type) type { }; } +pub const Mutex = struct { + state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unlocked), + + pub const unlocked: u32 = 0b00; + pub const locked: u32 = 0b01; + pub const contended: u32 = 0b11; // must contain the `locked` bit for x86 optimization below + + pub fn tryLock(m: *Mutex) bool { + // On x86, use `lock bts` instead of `lock cmpxchg` as: + // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048 + // - `lock bts` is smaller instruction-wise which makes it better for inlining + if (builtin.target.cpu.arch.isX86()) { + const locked_bit = @ctz(locked); + return m.state.bitSet(locked_bit, .acquire) == 0; + } + + // Acquire barrier ensures grabbing the lock happens before the critical section + // and that the previous lock holder's critical section happens before we grab the lock. + return m.state.cmpxchgWeak(unlocked, locked, .acquire, .monotonic) == null; + } + + /// Avoids the vtable for uncontended locks. + pub fn lock(m: *Mutex, io: Io) void { + if (!m.tryLock()) { + @branchHint(.unlikely); + io.vtable.mutexLock(io.userdata, m); + } + } + + pub fn unlock(m: *Mutex, io: Io) void { + io.vtable.mutexUnlock(io.userdata, m); + } +}; + +pub const Condition = struct { + state: u64 = 0, + + pub const WaitError = error{ + Timeout, + Canceled, + }; + + /// How many waiters to wake up. + pub const Notify = enum { + one, + all, + }; + + pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) void { + io.vtable.conditionWait(io.userdata, cond, mutex, null) catch |err| switch (err) { + error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out + error.Canceled => return, // handled as spurious wakeup + }; + } + + pub fn timedWait(cond: *Condition, io: Io, mutex: *Mutex, timeout_ns: u64) WaitError!void { + return io.vtable.conditionWait(io.userdata, cond, mutex, timeout_ns); + } + + pub fn signal(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond, .one); + } + + pub fn broadcast(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond, .all); + } +}; + +pub const TypeErasedQueue = struct { + mutex: Mutex, + + /// Ring buffer. This data is logically *after* queued getters. + buffer: []u8, + put_index: usize, + get_index: usize, + + putters: std.DoublyLinkedList(PutNode), + getters: std.DoublyLinkedList(GetNode), + + const PutNode = struct { + remaining: []const u8, + condition: Condition, + }; + + const GetNode = struct { + remaining: []u8, + condition: Condition, + }; + + pub fn init(buffer: []u8) TypeErasedQueue { + return .{ + .mutex = .{}, + .buffer = buffer, + .put_index = 0, + .get_index = 0, + .putters = .{}, + .getters = .{}, + }; + } + + pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize { + assert(elements.len >= min); + + q.mutex.lock(io); + defer q.mutex.unlock(io); + + // Getters have first priority on the data, and only when the getters + // queue is empty do we start populating the buffer. + + var remaining = elements; + while (true) { + const getter = q.getters.popFirst() orelse break; + const copy_len = @min(getter.data.remaining.len, remaining.len); + @memcpy(getter.data.remaining[0..copy_len], remaining[0..copy_len]); + remaining = remaining[copy_len..]; + getter.data.remaining = getter.data.remaining[copy_len..]; + if (getter.data.remaining.len == 0) { + getter.data.condition.signal(io); + continue; + } + q.getters.prepend(getter); + assert(remaining.len == 0); + return elements.len; + } + + while (true) { + { + const available = q.buffer[q.put_index..]; + const copy_len = @min(available.len, remaining.len); + @memcpy(available[0..copy_len], remaining[0..copy_len]); + remaining = remaining[copy_len..]; + q.put_index += copy_len; + if (remaining.len == 0) return elements.len; + } + { + const available = q.buffer[0..q.get_index]; + const copy_len = @min(available.len, remaining.len); + @memcpy(available[0..copy_len], remaining[0..copy_len]); + remaining = remaining[copy_len..]; + q.put_index = copy_len; + if (remaining.len == 0) return elements.len; + } + + const total_filled = elements.len - remaining.len; + if (total_filled >= min) return total_filled; + + var node: std.DoublyLinkedList(PutNode).Node = .{ + .data = .{ .remaining = remaining, .condition = .{} }, + }; + q.putters.append(&node); + node.data.condition.wait(io, &q.mutex); + remaining = node.data.remaining; + } + } + + pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) usize { + assert(buffer.len >= min); + + q.mutex.lock(io); + defer q.mutex.unlock(io); + + // The ring buffer gets first priority, then data should come from any + // queued putters, then finally the ring buffer should be filled with + // data from putters so they can be resumed. + + var remaining = buffer; + while (true) { + if (q.get_index <= q.put_index) { + const available = q.buffer[q.get_index..q.put_index]; + const copy_len = @min(available.len, remaining.len); + @memcpy(remaining[0..copy_len], available[0..copy_len]); + q.get_index += copy_len; + remaining = remaining[copy_len..]; + if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); + } else { + { + const available = q.buffer[q.get_index..]; + const copy_len = @min(available.len, remaining.len); + @memcpy(remaining[0..copy_len], available[0..copy_len]); + q.get_index += copy_len; + remaining = remaining[copy_len..]; + if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); + } + { + const available = q.buffer[0..q.put_index]; + const copy_len = @min(available.len, remaining.len); + @memcpy(remaining[0..copy_len], available[0..copy_len]); + q.get_index = copy_len; + remaining = remaining[copy_len..]; + if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); + } + } + // Copy directly from putters into buffer. + while (remaining.len > 0) { + const putter = q.putters.popFirst() orelse break; + const copy_len = @min(putter.data.remaining.len, remaining.len); + @memcpy(remaining[0..copy_len], putter.data.remaining[0..copy_len]); + putter.data.remaining = putter.data.remaining[copy_len..]; + remaining = remaining[copy_len..]; + if (putter.data.remaining.len == 0) { + putter.data.condition.signal(io); + } else { + assert(remaining.len == 0); + q.putters.prepend(putter); + return fillRingBufferFromPutters(q, io, buffer.len); + } + } + // Both ring buffer and putters queue is empty. + const total_filled = buffer.len - remaining.len; + if (total_filled >= min) return total_filled; + + var node: std.DoublyLinkedList(GetNode).Node = .{ + .data = .{ .remaining = remaining, .condition = .{} }, + }; + q.getters.append(&node); + node.data.condition.wait(io, &q.mutex); + remaining = node.data.remaining; + } + } + + /// Called when there is nonzero space available in the ring buffer and + /// potentially putters waiting. The mutex is already held and the task is + /// to copy putter data to the ring buffer and signal any putters whose + /// buffers been fully copied. + fn fillRingBufferFromPutters(q: *TypeErasedQueue, io: Io, len: usize) usize { + while (true) { + const putter = q.putters.popFirst() orelse return len; + const available = q.buffer[q.put_index..]; + const copy_len = @min(available.len, putter.data.remaining.len); + @memcpy(available[0..copy_len], putter.data.remaining[0..copy_len]); + putter.data.remaining = putter.data.remaining[copy_len..]; + q.put_index += copy_len; + if (putter.data.remaining.len == 0) { + putter.data.condition.signal(io); + continue; + } + const second_available = q.buffer[0..q.get_index]; + const second_copy_len = @min(second_available.len, putter.data.remaining.len); + @memcpy(second_available[0..second_copy_len], putter.data.remaining[0..second_copy_len]); + putter.data.remaining = putter.data.remaining[copy_len..]; + q.put_index = copy_len; + if (putter.data.remaining.len == 0) { + putter.data.condition.signal(io); + continue; + } + q.putters.prepend(putter); + return len; + } + } +}; + +/// Many producer, many consumer, thread-safe, runtime configurable buffer size. +/// When buffer is empty, consumers suspend and are resumed by producers. +/// When buffer is full, producers suspend and are resumed by consumers. +pub fn Queue(Elem: type) type { + return struct { + type_erased: TypeErasedQueue, + + pub fn init(buffer: []Elem) @This() { + return .{ .type_erased = .init(@ptrCast(buffer)) }; + } + + /// Appends elements to the end of the queue. The function returns when + /// at least `min` elements have been added to the buffer or sent + /// directly to a consumer. + /// + /// Returns how many elements have been added to the queue. + /// + /// Asserts that `elements.len >= min`. + pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) usize { + return @divExact(q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + + /// Receives elements from the beginning of the queue. The function + /// returns when at least `min` elements have been populated inside + /// `buffer`. + /// + /// Returns how many elements of `buffer` have been populated. + /// + /// Asserts that `buffer.len >= min`. + pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) usize { + return @divExact(q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + + pub fn putOne(q: *@This(), io: Io, item: Elem) void { + assert(q.put(io, &.{item}, 1) == 1); + } + + pub fn getOne(q: *@This(), io: Io) Elem { + var buf: [1]Elem = undefined; + assert(q.get(io, &buf, 1) == 1); + return buf[0]; + } + }; +} + /// Calls `function` with `args`, such that the return value of the function is /// not guaranteed to be available until `await` is called. pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { @@ -685,7 +988,7 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( const Args = @TypeOf(args); const TypeErased = struct { fn start(context: *const anyopaque, result: *anyopaque) void { - const args_casted: *const Args = @alignCast(@ptrCast(context)); + const args_casted: *const Args = @ptrCast(@alignCast(context)); const result_casted: *Result = @ptrCast(@alignCast(result)); result_casted.* = @call(.auto, function, args_casted.*); } diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index a24d5173e2..55ed05b146 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -102,7 +102,7 @@ const Fiber = struct { return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); } - fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{AsyncCancel}!void { + fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{Canceled}!void { if (@cmpxchgStrong( ?*Thread, &fiber.cancel_thread, @@ -112,7 +112,7 @@ const Fiber = struct { .acquire, )) |cancel_thread| { assert(cancel_thread == Thread.canceling); - return error.AsyncCancel; + return error.Canceled; } } @@ -746,7 +746,7 @@ pub fn createFile( switch (errno(completion.result)) { .SUCCESS => return .{ .handle = completion.result }, .INTR => unreachable, - .CANCELED => return error.AsyncCancel, + .CANCELED => return error.Canceled, .FAULT => unreachable, .INVAL => return error.BadPathName, @@ -854,7 +854,7 @@ pub fn openFile( switch (errno(completion.result)) { .SUCCESS => return .{ .handle = completion.result }, .INTR => unreachable, - .CANCELED => return error.AsyncCancel, + .CANCELED => return error.Canceled, .FAULT => unreachable, .INVAL => return error.BadPathName, @@ -950,7 +950,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std switch (errno(completion.result)) { .SUCCESS => return @as(u32, @bitCast(completion.result)), .INTR => unreachable, - .CANCELED => return error.AsyncCancel, + .CANCELED => return error.Canceled, .INVAL => unreachable, .FAULT => unreachable, @@ -1002,7 +1002,7 @@ pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offs switch (errno(completion.result)) { .SUCCESS => return @as(u32, @bitCast(completion.result)), .INTR => unreachable, - .CANCELED => return error.AsyncCancel, + .CANCELED => return error.Canceled, .INVAL => return error.InvalidArgument, .FAULT => unreachable, @@ -1080,7 +1080,7 @@ pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.D switch (errno(completion.result)) { .SUCCESS, .TIME => return, .INTR => unreachable, - .CANCELED => return error.AsyncCancel, + .CANCELED => return error.Canceled, else => |err| return std.posix.unexpectedErrno(err), } diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 6ec6c89040..37018f2ab7 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -332,9 +332,12 @@ pub fn io(pool: *Pool) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", - .cancel = cancel, .cancelRequested = cancelRequested, + .mutexLock = mutexLock, + .mutexUnlock = mutexUnlock, + .conditionWait = conditionWait, + .conditionWake = conditionWake, .createFile = createFile, .openFile = openFile, @@ -517,11 +520,179 @@ fn cancelRequested(userdata: ?*anyopaque) bool { return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; } -fn checkCancel(pool: *Pool) error{AsyncCancel}!void { - if (cancelRequested(pool)) return error.AsyncCancel; +fn checkCancel(pool: *Pool) error{Canceled}!void { + if (cancelRequested(pool)) return error.Canceled; } -pub fn createFile( +fn mutexLock(userdata: ?*anyopaque, m: *Io.Mutex) void { + @branchHint(.cold); + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + + // Avoid doing an atomic swap below if we already know the state is contended. + // An atomic swap unconditionally stores which marks the cache-line as modified unnecessarily. + if (m.state.load(.monotonic) == Io.Mutex.contended) { + std.Thread.Futex.wait(&m.state, Io.Mutex.contended); + } + + // Try to acquire the lock while also telling the existing lock holder that there are threads waiting. + // + // Once we sleep on the Futex, we must acquire the mutex using `contended` rather than `locked`. + // If not, threads sleeping on the Futex wouldn't see the state change in unlock and potentially deadlock. + // The downside is that the last mutex unlocker will see `contended` and do an unnecessary Futex wake + // but this is better than having to wake all waiting threads on mutex unlock. + // + // Acquire barrier ensures grabbing the lock happens before the critical section + // and that the previous lock holder's critical section happens before we grab the lock. + while (m.state.swap(Io.Mutex.contended, .acquire) != Io.Mutex.unlocked) { + std.Thread.Futex.wait(&m.state, Io.Mutex.contended); + } +} + +fn mutexUnlock(userdata: ?*anyopaque, m: *Io.Mutex) void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + // Needs to also wake up a waiting thread if any. + // + // A waiting thread will acquire with `contended` instead of `locked` + // which ensures that it wakes up another thread on the next unlock(). + // + // Release barrier ensures the critical section happens before we let go of the lock + // and that our critical section happens before the next lock holder grabs the lock. + const state = m.state.swap(Io.Mutex.unlocked, .release); + assert(state != Io.Mutex.unlocked); + + if (state == Io.Mutex.contended) { + std.Thread.Futex.wake(&m.state, 1); + } +} + +fn mutexLockInternal(pool: *std.Thread.Pool, m: *Io.Mutex) void { + if (!m.tryLock()) { + @branchHint(.unlikely); + mutexLock(pool, m); + } +} + +fn conditionWait( + userdata: ?*anyopaque, + cond: *Io.Condition, + mutex: *Io.Mutex, + timeout: ?u64, +) Io.Condition.WaitError!void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + // Observe the epoch, then check the state again to see if we should wake up. + // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock: + // + // - T1: s = LOAD(&state) + // - T2: UPDATE(&s, signal) + // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch) + // - T1: e = LOAD(&epoch) (was reordered after the state load) + // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change) + // + // Acquire barrier to ensure the epoch load happens before the state load. + var epoch = cond_epoch.load(.acquire); + var state = cond_state.fetchAdd(one_waiter, .monotonic); + assert(state & waiter_mask != waiter_mask); + state += one_waiter; + + mutexUnlock(pool, mutex); + defer mutexLockInternal(pool, mutex); + + var futex_deadline = std.Thread.Futex.Deadline.init(timeout); + + while (true) { + futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { + // On timeout, we must decrement the waiter we added above. + error.Timeout => { + while (true) { + // If there's a signal when we're timing out, consume it and report being woken up instead. + // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. + while (state & signal_mask != 0) { + const new_state = state - one_waiter - one_signal; + state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; + } + + // Remove the waiter we added and officially return timed out. + const new_state = state - one_waiter; + state = cond_state.cmpxchgWeak(state, new_state, .monotonic, .monotonic) orelse return err; + } + }, + }; + + epoch = cond_epoch.load(.acquire); + state = cond_state.load(.monotonic); + + // Try to wake up by consuming a signal and decremented the waiter we added previously. + // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. + while (state & signal_mask != 0) { + const new_state = state - one_waiter - one_signal; + state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; + } + } +} + +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Condition.Notify) void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + var state = cond_state.load(.monotonic); + while (true) { + const waiters = (state & waiter_mask) / one_waiter; + const signals = (state & signal_mask) / one_signal; + + // Reserves which waiters to wake up by incrementing the signals count. + // Therefore, the signals count is always less than or equal to the waiters count. + // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters. + const wakeable = waiters - signals; + if (wakeable == 0) { + return; + } + + const to_wake = switch (notify) { + .one => 1, + .all => wakeable, + }; + + // Reserve the amount of waiters to wake by incrementing the signals count. + // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. + const new_state = state + (one_signal * to_wake); + state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { + // Wake up the waiting threads we reserved above by changing the epoch value. + // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it. + // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption. + // + // Release barrier ensures the signal being added to the state happens before the epoch is changed. + // If not, the waiting thread could potentially deadlock from missing both the state and epoch change: + // + // - T2: UPDATE(&epoch, 1) (reordered before the state change) + // - T1: e = LOAD(&epoch) + // - T1: s = LOAD(&state) + // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) + // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) + _ = cond_epoch.fetchAdd(1, .release); + std.Thread.Futex.wake(cond_epoch, to_wake); + return; + }; + } +} + +fn createFile( userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, @@ -532,7 +703,7 @@ pub fn createFile( return dir.createFile(sub_path, flags); } -pub fn openFile( +fn openFile( userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, @@ -543,13 +714,13 @@ pub fn openFile( return dir.openFile(sub_path, flags); } -pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { +fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); _ = pool; return file.close(); } -pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { +fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); return switch (offset) { @@ -558,7 +729,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std }; } -pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { +fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); return switch (offset) { From a1c1d06b19986cb9a585993ec2bb33a3d5302aa7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 31 Mar 2025 02:10:50 -0700 Subject: [PATCH 022/244] std.Io: add detached async --- lib/std/Io.zig | 47 ++++++++++++++++++++++++--- lib/std/Thread/Pool.zig | 70 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 9444d45414..be7f905ece 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -580,6 +580,18 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*AnyFuture, + /// Executes `start` asynchronously in a manner such that it cleans itself + /// up. This mode does not support results, await, or cancel. + /// + /// Thread-safe. + go: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, + ) void, /// This function is only called when `async` returns a non-null value. /// /// Thread-safe. @@ -593,7 +605,6 @@ pub const VTable = struct { result: []u8, result_alignment: std.mem.Alignment, ) void, - /// Equivalent to `await` but initiates cancel request. /// /// This function is only called when `async` returns a non-null value. @@ -671,14 +682,24 @@ pub fn Future(Result: type) type { /// Idempotent. pub fn cancel(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; - io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); + io.vtable.cancel( + io.userdata, + any_future, + if (@sizeOf(Result) == 0) &.{} else @ptrCast((&f.result)[0..1]), // work around compiler bug + .of(Result), + ); f.any_future = null; return f.result; } pub fn await(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; - io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); + io.vtable.await( + io.userdata, + any_future, + if (@sizeOf(Result) == 0) &.{} else @ptrCast((&f.result)[0..1]), // work around compiler bug + .of(Result), + ); f.any_future = null; return f.result; } @@ -996,7 +1017,7 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( var future: Future(Result) = undefined; future.any_future = io.vtable.async( io.userdata, - @ptrCast((&future.result)[0..1]), + if (@sizeOf(Result) == 0) &.{} else @ptrCast((&future.result)[0..1]), // work around compiler bug .of(Result), if (@sizeOf(Args) == 0) &.{} else @ptrCast((&args)[0..1]), // work around compiler bug .of(Args), @@ -1005,6 +1026,24 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( return future; } +/// Calls `function` with `args` asynchronously. The resource cleans itself up +/// when the function returns. Does not support await, cancel, or a return value. +pub fn go(io: Io, function: anytype, args: anytype) void { + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(context: *const anyopaque) void { + const args_casted: *const Args = @alignCast(@ptrCast(context)); + @call(.auto, function, args_casted.*); + } + }; + io.vtable.go( + io.userdata, + if (@sizeOf(Args) == 0) &.{} else @ptrCast((&args)[0..1]), // work around compiler bug + .of(Args), + TypeErased.start, + ); +} + pub fn openFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File { return io.vtable.openFile(io.userdata, dir, sub_path, flags); } diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 37018f2ab7..f46c6f6802 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -332,6 +332,7 @@ pub fn io(pool: *Pool) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .go = go, .cancel = cancel, .cancelRequested = cancelRequested, .mutexLock = mutexLock, @@ -472,6 +473,75 @@ fn @"async"( return @ptrCast(closure); } +const DetachedClosure = struct { + pool: *Pool, + func: *const fn (context: *anyopaque) void, + run_node: std.Thread.Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, + context_alignment: std.mem.Alignment, + context_len: usize, + + fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { + const run_node: *std.Thread.Pool.RunQueue.Node = @fieldParentPtr("data", runnable); + const closure: *DetachedClosure = @alignCast(@fieldParentPtr("run_node", run_node)); + closure.func(closure.contextPointer()); + const gpa = closure.pool.allocator; + const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure); + gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]); + } + + fn contextOffset(context_alignment: std.mem.Alignment) usize { + return context_alignment.forward(@sizeOf(DetachedClosure)); + } + + fn contextEnd(context_alignment: std.mem.Alignment, context_len: usize) usize { + return contextOffset(context_alignment) + context_len; + } + + fn contextPointer(closure: *DetachedClosure) [*]u8 { + const base: [*]u8 = @ptrCast(closure); + return base + contextOffset(closure.context_alignment); + } +}; + +fn go( + userdata: ?*anyopaque, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, +) void { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + pool.mutex.lock(); + + const gpa = pool.allocator; + const n = DetachedClosure.contextEnd(context_alignment, context.len); + const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, @alignOf(DetachedClosure), n) catch { + pool.mutex.unlock(); + start(context.ptr); + return; + })); + closure.* = .{ + .pool = pool, + .func = start, + .context_alignment = context_alignment, + .context_len = context.len, + }; + @memcpy(closure.contextPointer()[0..context.len], context); + pool.run_queue.prepend(&closure.run_node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); + pool.cond.signal(); +} + fn @"await"( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, From f84aca36c31a24b72a6559c2fcac2b689df8354d Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Mon, 31 Mar 2025 08:06:20 -0400 Subject: [PATCH 023/244] Io: implement faster mutex --- lib/std/Io.zig | 69 +++++++++-- lib/std/Io/EventLoop.zig | 239 ++++++++++++++++++++++++++++++--------- lib/std/Thread/Pool.zig | 71 ++++-------- 3 files changed, 269 insertions(+), 110 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index be7f905ece..8b241add2d 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -626,8 +626,8 @@ pub const VTable = struct { /// Thread-safe. cancelRequested: *const fn (?*anyopaque) bool, - mutexLock: *const fn (?*anyopaque, mutex: *Mutex) void, - mutexUnlock: *const fn (?*anyopaque, mutex: *Mutex) void, + mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) error{Canceled}!void, + mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex, timeout_ns: ?u64) Condition.WaitError!void, conditionWake: *const fn (?*anyopaque, cond: *Condition, notify: Condition.Notify) void, @@ -706,8 +706,63 @@ pub fn Future(Result: type) type { }; } -pub const Mutex = struct { - state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unlocked), +pub const Mutex = if (true) struct { + state: State, + + pub const State = enum(usize) { + locked_once = 0b00, + unlocked = 0b01, + contended = 0b10, + /// contended + _, + + pub fn isUnlocked(state: State) bool { + return @intFromEnum(state) & @intFromEnum(State.unlocked) == @intFromEnum(State.unlocked); + } + }; + + pub const init: Mutex = .{ .state = .unlocked }; + + pub fn tryLock(mutex: *Mutex) bool { + const prev_state: State = @enumFromInt(@atomicRmw( + usize, + @as(*usize, @ptrCast(&mutex.state)), + .And, + ~@intFromEnum(State.unlocked), + .acquire, + )); + return prev_state.isUnlocked(); + } + + pub fn lock(mutex: *Mutex, io: std.Io) error{Canceled}!void { + const prev_state: State = @enumFromInt(@atomicRmw( + usize, + @as(*usize, @ptrCast(&mutex.state)), + .And, + ~@intFromEnum(State.unlocked), + .acquire, + )); + if (prev_state.isUnlocked()) { + @branchHint(.likely); + return; + } + return io.vtable.mutexLock(io.userdata, prev_state, mutex); + } + + pub fn unlock(mutex: *Mutex, io: std.Io) void { + const prev_state = @cmpxchgWeak(State, &mutex.state, .locked_once, .unlocked, .release, .acquire) orelse { + @branchHint(.likely); + return; + }; + std.debug.assert(prev_state != .unlocked); // mutex not locked + return io.vtable.mutexUnlock(io.userdata, prev_state, mutex); + } +} else struct { + state: std.atomic.Value(u32), + + pub const State = void; + + pub const init: Mutex = .{ .state = .init(unlocked) }; pub const unlocked: u32 = 0b00; pub const locked: u32 = 0b01; @@ -728,15 +783,15 @@ pub const Mutex = struct { } /// Avoids the vtable for uncontended locks. - pub fn lock(m: *Mutex, io: Io) void { + pub fn lock(m: *Mutex, io: Io) error{Canceled}!void { if (!m.tryLock()) { @branchHint(.unlikely); - io.vtable.mutexLock(io.userdata, m); + try io.vtable.mutexLock(io.userdata, {}, m); } } pub fn unlock(m: *Mutex, io: Io) void { - io.vtable.mutexUnlock(io.userdata, m); + io.vtable.mutexUnlock(io.userdata, {}, m); } }; diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 55ed05b146..d642a0e227 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -10,7 +10,7 @@ const IoUring = std.os.linux.IoUring; /// Must be a thread-safe allocator. gpa: Allocator, mutex: std.Thread.Mutex, -main_fiber: Fiber, +main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)), threads: Thread.List, /// Empirically saw >128KB being used by the self-hosted backend to panic. @@ -51,10 +51,12 @@ const Thread = struct { }; const Fiber = struct { + required_align: void align(4), context: Context, awaiter: ?*Fiber, queue_next: ?*Fiber, cancel_thread: ?*Thread, + awaiting_completions: std.StaticBitSet(3), const finished: ?*Fiber = @ptrFromInt(@alignOf(Thread)); @@ -131,7 +133,7 @@ const Fiber = struct { const thread: *Thread = .current(); std.log.debug("recyling {*}", .{fiber}); assert(fiber.queue_next == null); - @memset(fiber.allocatedSlice(), undefined); + //@memset(fiber.allocatedSlice(), undefined); // (race) fiber.queue_next = thread.free_queue; thread.free_queue = fiber; } @@ -145,10 +147,17 @@ pub fn io(el: *EventLoop) Io { .vtable = &.{ .@"async" = @"async", .@"await" = @"await", + .go = go, .cancel = cancel, .cancelRequested = cancelRequested, + .mutexLock = mutexLock, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWake = conditionWake, + .createFile = createFile, .openFile = openFile, .closeFile = closeFile, @@ -169,18 +178,22 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { el.* = .{ .gpa = gpa, .mutex = .{}, - .main_fiber = .{ - .context = undefined, - .awaiter = null, - .queue_next = null, - .cancel_thread = null, - }, + .main_fiber_buffer = undefined, .threads = .{ .allocated = @ptrCast(allocated_slice[0..threads_size]), .reserved = 1, .active = 1, }, }; + const main_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); + main_fiber.* = .{ + .required_align = {}, + .context = undefined, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; const main_thread = &el.threads.allocated[0]; Thread.self = main_thread; const idle_stack_end: [*]usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); @@ -192,7 +205,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { .rbp = 0, .rip = @intFromPtr(&mainIdleEntry), }, - .current_context = &el.main_fiber.context, + .current_context = &main_fiber.context, .ready_queue = null, .free_queue = null, .io_uring = try IoUring.init(io_uring_entries, 0), @@ -201,53 +214,57 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { }; errdefer main_thread.io_uring.deinit(); std.log.debug("created main idle {*}", .{&main_thread.idle_context}); - std.log.debug("created main {*}", .{&el.main_fiber}); + std.log.debug("created main {*}", .{main_fiber}); } pub fn deinit(el: *EventLoop) void { const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); - for (el.threads.allocated[0..active_threads]) |*thread| - assert(@atomicLoad(?*Fiber, &thread.ready_queue, .acquire) == null); // pending async + for (el.threads.allocated[0..active_threads]) |*thread| { + const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); + assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async + } el.yield(null, .exit); + const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.allocated.ptr)); + const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); + for (el.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); for (el.threads.allocated[0..active_threads]) |*thread| while (thread.free_queue) |free_fiber| { thread.free_queue = free_fiber.queue_next; free_fiber.queue_next = null; el.gpa.free(free_fiber.allocatedSlice()); }; - const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.allocated.ptr)); - const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); - for (el.threads.allocated[1..active_threads]) |thread| thread.thread.join(); el.gpa.free(allocated_ptr[0..idle_stack_end_offset]); el.* = undefined; } +fn findReadyFiber(el: *EventLoop, thread: *Thread) ?*Fiber { + if (@atomicRmw(?*Fiber, &thread.ready_queue, .Xchg, Fiber.finished, .acquire)) |ready_fiber| { + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); + for (0..@min(max_steal_ready_search, active_threads)) |_| { + defer thread.steal_ready_search_index += 1; + if (thread.steal_ready_search_index == active_threads) thread.steal_ready_search_index = 0; + const steal_ready_search_thread = &el.threads.allocated[0..active_threads][thread.steal_ready_search_index]; + if (steal_ready_search_thread == thread) continue; + const ready_fiber = @atomicRmw(?*Fiber, &steal_ready_search_thread.ready_queue, .And, Fiber.finished, .acquire) orelse continue; + if (ready_fiber == Fiber.finished) continue; + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + // couldn't find anything to do, so we are now open for business + @atomicStore(?*Fiber, &thread.ready_queue, null, .monotonic); + return null; +} + fn yield(el: *EventLoop, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void { const thread: *Thread = .current(); - const ready_context: *Context = if (maybe_ready_fiber) |ready_fiber| + const ready_context = if (maybe_ready_fiber orelse el.findReadyFiber(thread)) |ready_fiber| &ready_fiber.context - else if (thread.ready_queue) |ready_fiber| ready_context: { - thread.ready_queue = ready_fiber.queue_next; - ready_fiber.queue_next = null; - break :ready_context &ready_fiber.context; - } else ready_context: { - const ready_threads = @atomicLoad(u32, &el.threads.active, .acquire); - break :ready_context for (0..max_steal_ready_search) |_| { - defer thread.steal_ready_search_index += 1; - if (thread.steal_ready_search_index == ready_threads) thread.steal_ready_search_index = 0; - const steal_ready_search_thread = &el.threads.allocated[thread.steal_ready_search_index]; - if (steal_ready_search_thread == thread) continue; - const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; - if (@cmpxchgWeak( - ?*Fiber, - &steal_ready_search_thread.ready_queue, - ready_fiber, - @atomicLoad(?*Fiber, &ready_fiber.queue_next, .acquire), - .acq_rel, - .monotonic, - )) |_| continue; - break &ready_fiber.context; - } else &thread.idle_context; - }; + else + &thread.idle_context; const message: SwitchMessage = .{ .contexts = .{ .prev = thread.current_context, @@ -270,10 +287,10 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { } // shared fields of previous `Thread` must be initialized before later ones are marked as active const new_thread_index = @atomicLoad(u32, &el.threads.active, .acquire); - for (0..max_idle_search) |_| { + for (0..@min(max_idle_search, new_thread_index)) |_| { defer thread.idle_search_index += 1; if (thread.idle_search_index == new_thread_index) thread.idle_search_index = 0; - const idle_search_thread = &el.threads.allocated[thread.idle_search_index]; + const idle_search_thread = &el.threads.allocated[0..new_thread_index][thread.idle_search_index]; if (idle_search_thread == thread) continue; if (@cmpxchgWeak( ?*Fiber, @@ -325,8 +342,8 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { std.log.warn("unable to create worker thread due to io_uring init failure: {s}", .{@errorName(err)}); break :spawn_thread; }, - .idle_search_index = next_thread_index, - .steal_ready_search_index = next_thread_index, + .idle_search_index = 0, + .steal_ready_search_index = 0, }; new_thread.thread = std.Thread.spawn(.{ .stack_size = idle_stack_size, @@ -357,7 +374,7 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAl message.handle(el); const thread: *Thread = &el.threads.allocated[0]; el.idle(thread); - el.yield(&el.main_fiber, .nothing); + el.yield(@ptrCast(&el.main_fiber_buffer), .nothing); unreachable; // switched to dead fiber } @@ -384,8 +401,10 @@ const Completion = struct { fn idle(el: *EventLoop, thread: *Thread) void { var maybe_ready_fiber: ?*Fiber = null; while (true) { - el.yield(maybe_ready_fiber, .nothing); - maybe_ready_fiber = null; + while (maybe_ready_fiber orelse el.findReadyFiber(thread)) |ready_fiber| { + el.yield(ready_fiber, .nothing); + maybe_ready_fiber = null; + } _ = thread.io_uring.submit_and_wait(1) catch |err| switch (err) { error.SignalInterrupt => std.log.warn("submit_and_wait failed with SignalInterrupt", .{}), else => |e| @panic(@errorName(e)), @@ -450,7 +469,12 @@ const SwitchMessage = struct { const PendingTask = union(enum) { nothing, + reschedule, register_awaiter: *?*Fiber, + lock_mutex: struct { + prev_state: Io.Mutex.State, + mutex: *Io.Mutex, + }, exit, }; @@ -459,8 +483,14 @@ const SwitchMessage = struct { thread.current_context = message.contexts.ready; switch (message.pending_task) { .nothing => {}, + .reschedule => { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, .register_awaiter => |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); if (@atomicRmw( ?*Fiber, awaiter, @@ -469,6 +499,36 @@ const SwitchMessage = struct { .acq_rel, ) == Fiber.finished) el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); }, + .lock_mutex => |lock_mutex| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + var prev_state = lock_mutex.prev_state; + while (switch (prev_state) { + else => next_state: { + prev_fiber.queue_next = @ptrFromInt(@intFromEnum(prev_state)); + break :next_state @cmpxchgWeak( + Io.Mutex.State, + &lock_mutex.mutex.state, + prev_state, + @enumFromInt(@intFromPtr(prev_fiber)), + .release, + .acquire, + ); + }, + .unlocked => @cmpxchgWeak( + Io.Mutex.State, + &lock_mutex.mutex.state, + .unlocked, + .locked_once, + .acquire, + .acquire, + ) orelse { + prev_fiber.queue_next = null; + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + return; + }, + }) |next_state| prev_state = next_state; + }, .exit => for (el.threads.allocated[0..@atomicLoad(u32, &el.threads.active, .acquire)]) |*each_thread| { getSqe(&thread.io_uring).* = .{ .opcode = .MSG_RING, @@ -590,13 +650,13 @@ fn @"async"( start(context.ptr, result.ptr); return null; }; - errdefer fiber.recycle(); 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.* = .{ + .required_align = {}, .context = switch (builtin.cpu.arch) { .x86_64 => .{ .rsp = @intFromPtr(closure) - @sizeOf(usize), @@ -608,6 +668,7 @@ fn @"async"( .awaiter = null, .queue_next = null, .cancel_thread = null, + .awaiting_completions = .initEmpty(), }; closure.* = .{ .event_loop = event_loop, @@ -634,6 +695,19 @@ fn @"await"( future_fiber.recycle(); } +fn go( + userdata: ?*anyopaque, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, +) void { + _ = userdata; + _ = context; + _ = context_alignment; + _ = start; + @panic("TODO"); +} + fn cancel( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, @@ -673,7 +747,7 @@ fn cancelRequested(userdata: ?*anyopaque) bool { return @atomicLoad(?*Thread, &Thread.current().currentFiber().cancel_thread, .acquire) == Thread.canceling; } -pub fn createFile( +fn createFile( userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, @@ -775,7 +849,7 @@ pub fn createFile( } } -pub fn openFile( +fn openFile( userdata: ?*anyopaque, dir: std.fs.Dir, sub_path: []const u8, @@ -883,7 +957,7 @@ pub fn openFile( } } -pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { +fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -919,7 +993,7 @@ pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { } } -pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { +fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -971,7 +1045,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std } } -pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { +fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -1027,13 +1101,13 @@ pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offs } } -pub fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { +fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { _ = userdata; const timespec = try std.posix.clock_gettime(clockid); return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); } -pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { +fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -1086,10 +1160,65 @@ pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.D } } +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + el.yield(null, .{ .lock_mutex = .{ + .prev_state = prev_state, + .mutex = mutex, + } }); +} +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + var maybe_waiting_fiber: ?*Fiber = @ptrFromInt(@intFromEnum(prev_state)); + while (if (maybe_waiting_fiber) |waiting_fiber| @cmpxchgWeak( + Io.Mutex.State, + &mutex.state, + @enumFromInt(@intFromPtr(waiting_fiber)), + @enumFromInt(@intFromPtr(waiting_fiber.queue_next)), + .release, + .acquire, + ) else @cmpxchgWeak( + Io.Mutex.State, + &mutex.state, + .locked_once, + .unlocked, + .release, + .acquire, + ) orelse return) |next_state| maybe_waiting_fiber = @ptrFromInt(@intFromEnum(next_state)); + maybe_waiting_fiber.?.queue_next = null; + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + el.yield(maybe_waiting_fiber.?, .reschedule); +} + +fn conditionWait( + userdata: ?*anyopaque, + cond: *Io.Condition, + mutex: *Io.Mutex, + timeout: ?u64, +) Io.Condition.WaitError!void { + _ = userdata; + _ = cond; + _ = mutex; + _ = timeout; + @panic("TODO"); +} + +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Condition.Notify) void { + _ = userdata; + _ = cond; + _ = notify; + @panic("TODO"); +} + fn errno(signed: i32) std.os.linux.E { return .init(@bitCast(@as(isize, signed))); } fn getSqe(iou: *IoUring) *std.os.linux.io_uring_sqe { - return iou.get_sqe() catch @panic("TODO: handle submission queue full"); + while (true) return iou.get_sqe() catch { + _ = iou.submit_and_wait(0) catch |err| switch (err) { + error.SignalInterrupt => std.log.warn("submit_and_wait failed with SignalInterrupt", .{}), + else => |e| @panic(@errorName(e)), + }; + continue; + }; } diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index f46c6f6802..bb577f401b 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -335,8 +335,10 @@ pub fn io(pool: *Pool) Io { .go = go, .cancel = cancel, .cancelRequested = cancelRequested, + .mutexLock = mutexLock, .mutexUnlock = mutexUnlock, + .conditionWait = conditionWait, .conditionWake = conditionWake, @@ -594,53 +596,26 @@ fn checkCancel(pool: *Pool) error{Canceled}!void { if (cancelRequested(pool)) return error.Canceled; } -fn mutexLock(userdata: ?*anyopaque, m: *Io.Mutex) void { - @branchHint(.cold); - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - _ = pool; - - // Avoid doing an atomic swap below if we already know the state is contended. - // An atomic swap unconditionally stores which marks the cache-line as modified unnecessarily. - if (m.state.load(.monotonic) == Io.Mutex.contended) { - std.Thread.Futex.wait(&m.state, Io.Mutex.contended); +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { + _ = userdata; + if (prev_state == .contended) { + std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } - - // Try to acquire the lock while also telling the existing lock holder that there are threads waiting. - // - // Once we sleep on the Futex, we must acquire the mutex using `contended` rather than `locked`. - // If not, threads sleeping on the Futex wouldn't see the state change in unlock and potentially deadlock. - // The downside is that the last mutex unlocker will see `contended` and do an unnecessary Futex wake - // but this is better than having to wake all waiting threads on mutex unlock. - // - // Acquire barrier ensures grabbing the lock happens before the critical section - // and that the previous lock holder's critical section happens before we grab the lock. - while (m.state.swap(Io.Mutex.contended, .acquire) != Io.Mutex.unlocked) { - std.Thread.Futex.wait(&m.state, Io.Mutex.contended); + while (@atomicRmw( + Io.Mutex.State, + &mutex.state, + .Xchg, + .contended, + .acquire, + ) != .unlocked) { + std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } } - -fn mutexUnlock(userdata: ?*anyopaque, m: *Io.Mutex) void { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - _ = pool; - // Needs to also wake up a waiting thread if any. - // - // A waiting thread will acquire with `contended` instead of `locked` - // which ensures that it wakes up another thread on the next unlock(). - // - // Release barrier ensures the critical section happens before we let go of the lock - // and that our critical section happens before the next lock holder grabs the lock. - const state = m.state.swap(Io.Mutex.unlocked, .release); - assert(state != Io.Mutex.unlocked); - - if (state == Io.Mutex.contended) { - std.Thread.Futex.wake(&m.state, 1); - } -} - -fn mutexLockInternal(pool: *std.Thread.Pool, m: *Io.Mutex) void { - if (!m.tryLock()) { - @branchHint(.unlikely); - mutexLock(pool, m); +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + _ = userdata; + _ = prev_state; + if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { + std.Thread.Futex.wake(@ptrCast(&mutex.state), 1); } } @@ -674,8 +649,8 @@ fn conditionWait( assert(state & waiter_mask != waiter_mask); state += one_waiter; - mutexUnlock(pool, mutex); - defer mutexLockInternal(pool, mutex); + mutex.unlock(pool.io()); + defer mutex.lock(pool.io()) catch @panic("TODO"); var futex_deadline = std.Thread.Futex.Deadline.init(timeout); @@ -808,14 +783,14 @@ fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: }; } -pub fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { +fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); const timespec = try std.posix.clock_gettime(clockid); return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); } -pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { +fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); const deadline_nanoseconds: i96 = switch (deadline) { .nanoseconds => |nanoseconds| nanoseconds, From 266bcfbf2f9fc6fa2c13229f8a00d57e57613c91 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 31 Mar 2025 14:36:20 -0700 Subject: [PATCH 024/244] EventLoop: implement detached async data races on deinit tho --- lib/std/Io.zig | 57 ++++++++++--------- lib/std/Io/EventLoop.zig | 116 ++++++++++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 40 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 8b241add2d..413d31b396 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -626,7 +626,7 @@ pub const VTable = struct { /// Thread-safe. cancelRequested: *const fn (?*anyopaque) bool, - mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) error{Canceled}!void, + mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void, mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex, timeout_ns: ?u64) Condition.WaitError!void, @@ -645,11 +645,11 @@ pub const VTable = struct { pub const OpenFlags = fs.File.OpenFlags; pub const CreateFlags = fs.File.CreateFlags; -pub const FileOpenError = fs.File.OpenError || error{Canceled}; -pub const FileReadError = fs.File.ReadError || error{Canceled}; -pub const FilePReadError = fs.File.PReadError || error{Canceled}; -pub const FileWriteError = fs.File.WriteError || error{Canceled}; -pub const FilePWriteError = fs.File.PWriteError || error{Canceled}; +pub const FileOpenError = fs.File.OpenError || Cancelable; +pub const FileReadError = fs.File.ReadError || Cancelable; +pub const FilePReadError = fs.File.PReadError || Cancelable; +pub const FileWriteError = fs.File.WriteError || Cancelable; +pub const FilePWriteError = fs.File.PWriteError || Cancelable; pub const Timestamp = enum(i96) { _, @@ -666,7 +666,7 @@ pub const Deadline = union(enum) { nanoseconds: i96, timestamp: Timestamp, }; -pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{Canceled}; +pub const ClockGetTimeError = std.posix.ClockGetTimeError || Cancelable; pub const SleepError = error{ UnsupportedClock, Unexpected, Canceled }; pub const AnyFuture = opaque {}; @@ -734,7 +734,7 @@ pub const Mutex = if (true) struct { return prev_state.isUnlocked(); } - pub fn lock(mutex: *Mutex, io: std.Io) error{Canceled}!void { + pub fn lock(mutex: *Mutex, io: std.Io) Cancelable!void { const prev_state: State = @enumFromInt(@atomicRmw( usize, @as(*usize, @ptrCast(&mutex.state)), @@ -783,7 +783,7 @@ pub const Mutex = if (true) struct { } /// Avoids the vtable for uncontended locks. - pub fn lock(m: *Mutex, io: Io) error{Canceled}!void { + pub fn lock(m: *Mutex, io: Io) Cancelable!void { if (!m.tryLock()) { @branchHint(.unlikely); try io.vtable.mutexLock(io.userdata, {}, m); @@ -809,10 +809,10 @@ pub const Condition = struct { all, }; - pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) void { + pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void { io.vtable.conditionWait(io.userdata, cond, mutex, null) catch |err| switch (err) { error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out - error.Canceled => return, // handled as spurious wakeup + error.Canceled => return error.Canceled, }; } @@ -829,6 +829,11 @@ pub const Condition = struct { } }; +pub const Cancelable = error{ + /// Caller has requested the async operation to stop. + Canceled, +}; + pub const TypeErasedQueue = struct { mutex: Mutex, @@ -852,7 +857,7 @@ pub const TypeErasedQueue = struct { pub fn init(buffer: []u8) TypeErasedQueue { return .{ - .mutex = .{}, + .mutex = .init, .buffer = buffer, .put_index = 0, .get_index = 0, @@ -861,10 +866,10 @@ pub const TypeErasedQueue = struct { }; } - pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize { + pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) Cancelable!usize { assert(elements.len >= min); - q.mutex.lock(io); + try q.mutex.lock(io); defer q.mutex.unlock(io); // Getters have first priority on the data, and only when the getters @@ -911,15 +916,15 @@ pub const TypeErasedQueue = struct { .data = .{ .remaining = remaining, .condition = .{} }, }; q.putters.append(&node); - node.data.condition.wait(io, &q.mutex); + try node.data.condition.wait(io, &q.mutex); remaining = node.data.remaining; } } - pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) usize { + pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) Cancelable!usize { assert(buffer.len >= min); - q.mutex.lock(io); + try q.mutex.lock(io); defer q.mutex.unlock(io); // The ring buffer gets first priority, then data should come from any @@ -976,7 +981,7 @@ pub const TypeErasedQueue = struct { .data = .{ .remaining = remaining, .condition = .{} }, }; q.getters.append(&node); - node.data.condition.wait(io, &q.mutex); + try node.data.condition.wait(io, &q.mutex); remaining = node.data.remaining; } } @@ -1030,8 +1035,8 @@ pub fn Queue(Elem: type) type { /// Returns how many elements have been added to the queue. /// /// Asserts that `elements.len >= min`. - pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) usize { - return @divExact(q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); + pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) Cancelable!usize { + return @divExact(try q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); } /// Receives elements from the beginning of the queue. The function @@ -1041,17 +1046,17 @@ pub fn Queue(Elem: type) type { /// Returns how many elements of `buffer` have been populated. /// /// Asserts that `buffer.len >= min`. - pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) usize { - return @divExact(q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); + pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) Cancelable!usize { + return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); } - pub fn putOne(q: *@This(), io: Io, item: Elem) void { - assert(q.put(io, &.{item}, 1) == 1); + pub fn putOne(q: *@This(), io: Io, item: Elem) Cancelable!void { + assert(try q.put(io, &.{item}, 1) == 1); } - pub fn getOne(q: *@This(), io: Io) Elem { + pub fn getOne(q: *@This(), io: Io) Cancelable!Elem { var buf: [1]Elem = undefined; - assert(q.get(io, &buf, 1) == 1); + assert(try q.get(io, &buf, 1) == 1); return buf[0]; } }; diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index d642a0e227..d03f339b52 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -27,6 +27,7 @@ const Thread = struct { current_context: *Context, ready_queue: ?*Fiber, free_queue: ?*Fiber, + detached_queue: ?*Fiber, io_uring: IoUring, idle_search_index: u32, steal_ready_search_index: u32, @@ -208,6 +209,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { .current_context = &main_fiber.context, .ready_queue = null, .free_queue = null, + .detached_queue = null, .io_uring = try IoUring.init(io_uring_entries, 0), .idle_search_index = 1, .steal_ready_search_index = 1, @@ -218,7 +220,16 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { } pub fn deinit(el: *EventLoop) void { + // Wait for detached fibers. const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); + for (el.threads.allocated[0..active_threads]) |*thread| { + while (thread.detached_queue) |detached_fiber| { + if (@atomicLoad(?*Fiber, &detached_fiber.awaiter, .acquire) != Fiber.finished) + el.yield(null, .{ .register_awaiter = &detached_fiber.awaiter }); + detached_fiber.recycle(); + } + } + for (el.threads.allocated[0..active_threads]) |*thread| { const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async @@ -336,6 +347,7 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { .current_context = &new_thread.idle_context, .ready_queue = ready_queue.head, .free_queue = null, + .detached_queue = null, .io_uring = IoUring.init(io_uring_entries, 0) catch |err| { @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); // no more access to `thread` after giving up reservation @@ -470,6 +482,7 @@ const SwitchMessage = struct { const PendingTask = union(enum) { nothing, reschedule, + recycle: *Fiber, register_awaiter: *?*Fiber, lock_mutex: struct { prev_state: Io.Mutex.State, @@ -488,6 +501,9 @@ const SwitchMessage = struct { assert(prev_fiber.queue_next == null); el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); }, + .recycle => |fiber| { + fiber.recycle(); + }, .register_awaiter => |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); assert(prev_fiber.queue_next == null); @@ -612,6 +628,18 @@ fn fiberEntry() callconv(.naked) void { } } +fn fiberEntryDetached() callconv(.naked) void { + switch (builtin.cpu.arch) { + .x86_64 => asm volatile ( + \\ leaq 8(%%rsp), %%rdi + \\ jmp %[DetachedClosure_call:P] + : + : [DetachedClosure_call] "X" (&DetachedClosure.call), + ), + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + } +} + const AsyncClosure = struct { event_loop: *EventLoop, fiber: *Fiber, @@ -632,6 +660,31 @@ const AsyncClosure = struct { } }; +const DetachedClosure = struct { + event_loop: *EventLoop, + fiber: *Fiber, + start: *const fn (context: *const anyopaque) void, + + fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { + return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure)); + } + + fn call(closure: *DetachedClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(DetachedClosure))) noreturn { + message.handle(closure.event_loop); + std.log.debug("{*} performing async detached", .{closure.fiber}); + closure.start(closure.contextPointer()); + const current_thread: *Thread = .current(); + current_thread.detached_queue = closure.fiber.queue_next; + const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + if (awaiter) |a| { + closure.event_loop.yield(a, .nothing); + } else { + closure.event_loop.yield(null, .{ .recycle = closure.fiber }); + } + unreachable; // switched to dead fiber + } +}; + fn @"async"( userdata: ?*anyopaque, result: []u8, @@ -682,6 +735,53 @@ fn @"async"( return @ptrCast(fiber); } +fn go( + userdata: ?*anyopaque, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, +) void { + assert(context_alignment.compare(.lte, Fiber.max_context_align)); // 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); + return; + }; + std.log.debug("allocated {*}", .{fiber}); + + const current_thread: *Thread = .current(); + const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward( + @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, + ) - @sizeOf(DetachedClosure)); + fiber.* = .{ + .required_align = {}, + .context = switch (builtin.cpu.arch) { + .x86_64 => .{ + .rsp = @intFromPtr(closure) - @sizeOf(usize), + .rbp = 0, + .rip = @intFromPtr(&fiberEntryDetached), + }, + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + }, + .awaiter = null, + .queue_next = current_thread.detached_queue, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + current_thread.detached_queue = fiber; + closure.* = .{ + .event_loop = event_loop, + .fiber = fiber, + .start = start, + }; + @memcpy(closure.contextPointer(), context); + + event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber }); +} + + fn @"await"( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, @@ -690,24 +790,12 @@ fn @"await"( ) 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 }); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) + event_loop.yield(null, .{ .register_awaiter = &future_fiber.awaiter }); @memcpy(result, future_fiber.resultBytes(result_alignment)); future_fiber.recycle(); } -fn go( - userdata: ?*anyopaque, - context: []const u8, - context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) void, -) void { - _ = userdata; - _ = context; - _ = context_alignment; - _ = start; - @panic("TODO"); -} - fn cancel( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, From 8773b632418adb23cfda847f69e971e90eb6d3a7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 31 Mar 2025 14:57:12 -0700 Subject: [PATCH 025/244] EventLoop: take DetachedClosure into account when allocating --- lib/std/Io/EventLoop.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index d03f339b52..d5b91db476 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -68,13 +68,13 @@ const Fiber = struct { const min_stack_size = 4 * 1024 * 1024; const max_context_align: Alignment = .@"16"; const max_context_size = max_context_align.forward(1024); + const max_closure_size: usize = @max(@sizeOf(AsyncClosure), @sizeOf(DetachedClosure)); + const max_closure_align: Alignment = .max(.of(AsyncClosure), .of(DetachedClosure)); const allocation_size = std.mem.alignForward( usize, - std.mem.alignForward( - usize, + max_closure_align.max(max_context_align).forward( max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size, - @max(@alignOf(AsyncClosure), max_context_align.toByteUnits()), - ) + @sizeOf(AsyncClosure) + max_context_size, + ) + max_closure_size + max_context_size, std.heap.page_size_max, ); From 929b616e0f9cb00bfba9901a83c9b4b2203ed094 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 31 Mar 2025 16:25:11 -0700 Subject: [PATCH 026/244] std.Io.Condition: change primitive to support only one and no timer --- lib/std/Io.zig | 52 +++++++++++++--------------------------- lib/std/Io/EventLoop.zig | 33 +++++++++++++------------ lib/std/Thread/Pool.zig | 32 ++++--------------------- 3 files changed, 37 insertions(+), 80 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 413d31b396..54d6b4f697 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -629,8 +629,8 @@ pub const VTable = struct { mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void, mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, - conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex, timeout_ns: ?u64) Condition.WaitError!void, - conditionWake: *const fn (?*anyopaque, cond: *Condition, notify: Condition.Notify) void, + conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, + conditionWake: *const fn (?*anyopaque, cond: *Condition) void, createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, @@ -642,6 +642,11 @@ pub const VTable = struct { sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, }; +pub const Cancelable = error{ + /// Caller has requested the async operation to stop. + Canceled, +}; + pub const OpenFlags = fs.File.OpenFlags; pub const CreateFlags = fs.File.CreateFlags; @@ -795,43 +800,18 @@ pub const Mutex = if (true) struct { } }; +/// Supports exactly 1 waiter. More than 1 simultaneous wait on the same +/// condition is illegal. pub const Condition = struct { state: u64 = 0, - pub const WaitError = error{ - Timeout, - Canceled, - }; - - /// How many waiters to wake up. - pub const Notify = enum { - one, - all, - }; - pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) Cancelable!void { - io.vtable.conditionWait(io.userdata, cond, mutex, null) catch |err| switch (err) { - error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out - error.Canceled => return error.Canceled, - }; + return io.vtable.conditionWait(io.userdata, cond, mutex); } - pub fn timedWait(cond: *Condition, io: Io, mutex: *Mutex, timeout_ns: u64) WaitError!void { - return io.vtable.conditionWait(io.userdata, cond, mutex, timeout_ns); + pub fn wake(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond); } - - pub fn signal(cond: *Condition, io: Io) void { - io.vtable.conditionWake(io.userdata, cond, .one); - } - - pub fn broadcast(cond: *Condition, io: Io) void { - io.vtable.conditionWake(io.userdata, cond, .all); - } -}; - -pub const Cancelable = error{ - /// Caller has requested the async operation to stop. - Canceled, }; pub const TypeErasedQueue = struct { @@ -883,7 +863,7 @@ pub const TypeErasedQueue = struct { remaining = remaining[copy_len..]; getter.data.remaining = getter.data.remaining[copy_len..]; if (getter.data.remaining.len == 0) { - getter.data.condition.signal(io); + getter.data.condition.wake(io); continue; } q.getters.prepend(getter); @@ -966,7 +946,7 @@ pub const TypeErasedQueue = struct { putter.data.remaining = putter.data.remaining[copy_len..]; remaining = remaining[copy_len..]; if (putter.data.remaining.len == 0) { - putter.data.condition.signal(io); + putter.data.condition.wake(io); } else { assert(remaining.len == 0); q.putters.prepend(putter); @@ -999,7 +979,7 @@ pub const TypeErasedQueue = struct { putter.data.remaining = putter.data.remaining[copy_len..]; q.put_index += copy_len; if (putter.data.remaining.len == 0) { - putter.data.condition.signal(io); + putter.data.condition.wake(io); continue; } const second_available = q.buffer[0..q.get_index]; @@ -1008,7 +988,7 @@ pub const TypeErasedQueue = struct { putter.data.remaining = putter.data.remaining[copy_len..]; q.put_index = copy_len; if (putter.data.remaining.len == 0) { - putter.data.condition.signal(io); + putter.data.condition.wake(io); continue; } q.putters.prepend(putter); diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index d5b91db476..a27e197b7d 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -781,7 +781,6 @@ fn go( event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber }); } - fn @"await"( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, @@ -1277,24 +1276,24 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut el.yield(maybe_waiting_fiber.?, .reschedule); } -fn conditionWait( - userdata: ?*anyopaque, - cond: *Io.Condition, - mutex: *Io.Mutex, - timeout: ?u64, -) Io.Condition.WaitError!void { - _ = userdata; - _ = cond; - _ = mutex; - _ = timeout; - @panic("TODO"); +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const cond_state: *?*Fiber = @ptrCast(&cond.state); + const thread: *Thread = .current(); + const fiber = thread.currentFiber(); + const prev = @atomicRmw(?*Fiber, cond_state, .Xchg, fiber, .acquire); + assert(prev == null); // More than one wait on same Condition is illegal. + mutex.unlock(io(el)); + el.yield(null, .nothing); + try mutex.lock(io(el)); } -fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Condition.Notify) void { - _ = userdata; - _ = cond; - _ = notify; - @panic("TODO"); +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition) void { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const cond_state: *?*Fiber = @ptrCast(&cond.state); + if (@atomicRmw(?*Fiber, cond_state, .Xchg, null, .acquire)) |fiber| { + el.yield(fiber, .reschedule); + } } fn errno(signed: i32) std.os.linux.E { diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index bb577f401b..05bc4801b6 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -619,12 +619,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut } } -fn conditionWait( - userdata: ?*anyopaque, - cond: *Io.Condition, - mutex: *Io.Mutex, - timeout: ?u64, -) Io.Condition.WaitError!void { +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); @@ -652,25 +647,11 @@ fn conditionWait( mutex.unlock(pool.io()); defer mutex.lock(pool.io()) catch @panic("TODO"); - var futex_deadline = std.Thread.Futex.Deadline.init(timeout); + var futex_deadline = std.Thread.Futex.Deadline.init(null); while (true) { futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { - // On timeout, we must decrement the waiter we added above. - error.Timeout => { - while (true) { - // If there's a signal when we're timing out, consume it and report being woken up instead. - // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. - while (state & signal_mask != 0) { - const new_state = state - one_waiter - one_signal; - state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; - } - - // Remove the waiter we added and officially return timed out. - const new_state = state - one_waiter; - state = cond_state.cmpxchgWeak(state, new_state, .monotonic, .monotonic) orelse return err; - } - }, + error.Timeout => unreachable, }; epoch = cond_epoch.load(.acquire); @@ -685,7 +666,7 @@ fn conditionWait( } } -fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Condition.Notify) void { +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition) void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); _ = pool; comptime assert(@TypeOf(cond.state) == u64); @@ -709,10 +690,7 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Conditio return; } - const to_wake = switch (notify) { - .one => 1, - .all => wakeable, - }; + const to_wake = 1; // Reserve the amount of waiters to wake by incrementing the signals count. // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. From 4063205746efec752faaece85355c01de55ef08e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 31 Mar 2025 17:06:05 -0700 Subject: [PATCH 027/244] EventLoop: remove broken mechanism for making deinit block on detached --- lib/std/Io/EventLoop.zig | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index a27e197b7d..e5ab70dfd0 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -27,7 +27,6 @@ const Thread = struct { current_context: *Context, ready_queue: ?*Fiber, free_queue: ?*Fiber, - detached_queue: ?*Fiber, io_uring: IoUring, idle_search_index: u32, steal_ready_search_index: u32, @@ -209,7 +208,6 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { .current_context = &main_fiber.context, .ready_queue = null, .free_queue = null, - .detached_queue = null, .io_uring = try IoUring.init(io_uring_entries, 0), .idle_search_index = 1, .steal_ready_search_index = 1, @@ -220,16 +218,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { } pub fn deinit(el: *EventLoop) void { - // Wait for detached fibers. const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); - for (el.threads.allocated[0..active_threads]) |*thread| { - while (thread.detached_queue) |detached_fiber| { - if (@atomicLoad(?*Fiber, &detached_fiber.awaiter, .acquire) != Fiber.finished) - el.yield(null, .{ .register_awaiter = &detached_fiber.awaiter }); - detached_fiber.recycle(); - } - } - for (el.threads.allocated[0..active_threads]) |*thread| { const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async @@ -347,7 +336,6 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { .current_context = &new_thread.idle_context, .ready_queue = ready_queue.head, .free_queue = null, - .detached_queue = null, .io_uring = IoUring.init(io_uring_entries, 0) catch |err| { @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); // no more access to `thread` after giving up reservation @@ -673,8 +661,6 @@ const DetachedClosure = struct { message.handle(closure.event_loop); std.log.debug("{*} performing async detached", .{closure.fiber}); closure.start(closure.contextPointer()); - const current_thread: *Thread = .current(); - current_thread.detached_queue = closure.fiber.queue_next; const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); if (awaiter) |a| { closure.event_loop.yield(a, .nothing); @@ -766,11 +752,10 @@ fn go( else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), }, .awaiter = null, - .queue_next = current_thread.detached_queue, + .queue_next = null, .cancel_thread = null, .awaiting_completions = .initEmpty(), }; - current_thread.detached_queue = fiber; closure.* = .{ .event_loop = event_loop, .fiber = fiber, From e366b13a6573979bf4e644374aaf3e3b125a6377 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Mon, 31 Mar 2025 23:45:31 -0400 Subject: [PATCH 028/244] EventLoop: revert incorrect optimization --- lib/std/Io/EventLoop.zig | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index e5ab70dfd0..11a05bef48 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -248,8 +248,16 @@ fn findReadyFiber(el: *EventLoop, thread: *Thread) ?*Fiber { if (thread.steal_ready_search_index == active_threads) thread.steal_ready_search_index = 0; const steal_ready_search_thread = &el.threads.allocated[0..active_threads][thread.steal_ready_search_index]; if (steal_ready_search_thread == thread) continue; - const ready_fiber = @atomicRmw(?*Fiber, &steal_ready_search_thread.ready_queue, .And, Fiber.finished, .acquire) orelse continue; + const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; if (ready_fiber == Fiber.finished) continue; + if (@cmpxchgWeak( + ?*Fiber, + &steal_ready_search_thread.ready_queue, + ready_fiber, + null, + .acquire, + .monotonic, + )) |_| continue; @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); ready_fiber.queue_next = null; return ready_fiber; @@ -297,7 +305,7 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { &idle_search_thread.ready_queue, null, ready_queue.head, - .acq_rel, + .release, .monotonic, )) |_| continue; getSqe(&thread.io_uring).* = .{ @@ -1268,9 +1276,9 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I const fiber = thread.currentFiber(); const prev = @atomicRmw(?*Fiber, cond_state, .Xchg, fiber, .acquire); assert(prev == null); // More than one wait on same Condition is illegal. - mutex.unlock(io(el)); + mutex.unlock(el.io()); el.yield(null, .nothing); - try mutex.lock(io(el)); + try mutex.lock(el.io()); } fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition) void { From 08ce00027651b8cfd6676341e8ae32f423c85b25 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Tue, 1 Apr 2025 02:23:41 -0400 Subject: [PATCH 029/244] EventLoop: fix `std.Io.Condition` implementation 1. a fiber can't put itself on a queue that allows it to be rescheduled 2. allow the idle fiber to unlock a mutex held by another fiber by ignoring reschedule requests originating from the idle fiber --- lib/std/Io/EventLoop.zig | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 11a05bef48..37ad088b6d 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -380,8 +380,7 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { message.handle(el); - const thread: *Thread = &el.threads.allocated[0]; - el.idle(thread); + el.idle(&el.threads.allocated[0]); el.yield(@ptrCast(&el.main_fiber_buffer), .nothing); unreachable; // switched to dead fiber } @@ -480,10 +479,14 @@ const SwitchMessage = struct { reschedule, recycle: *Fiber, register_awaiter: *?*Fiber, - lock_mutex: struct { + mutex_lock: struct { prev_state: Io.Mutex.State, mutex: *Io.Mutex, }, + condition_wait: struct { + cond: *Io.Condition, + mutex: *Io.Mutex, + }, exit, }; @@ -492,7 +495,7 @@ const SwitchMessage = struct { thread.current_context = message.contexts.ready; switch (message.pending_task) { .nothing => {}, - .reschedule => { + .reschedule => if (message.contexts.prev != &thread.idle_context) { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); assert(prev_fiber.queue_next == null); el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); @@ -511,16 +514,16 @@ const SwitchMessage = struct { .acq_rel, ) == Fiber.finished) el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); }, - .lock_mutex => |lock_mutex| { + .mutex_lock => |mutex_lock| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); assert(prev_fiber.queue_next == null); - var prev_state = lock_mutex.prev_state; + var prev_state = mutex_lock.prev_state; while (switch (prev_state) { else => next_state: { prev_fiber.queue_next = @ptrFromInt(@intFromEnum(prev_state)); break :next_state @cmpxchgWeak( Io.Mutex.State, - &lock_mutex.mutex.state, + &mutex_lock.mutex.state, prev_state, @enumFromInt(@intFromPtr(prev_fiber)), .release, @@ -529,7 +532,7 @@ const SwitchMessage = struct { }, .unlocked => @cmpxchgWeak( Io.Mutex.State, - &lock_mutex.mutex.state, + &mutex_lock.mutex.state, .unlocked, .locked_once, .acquire, @@ -541,6 +544,13 @@ const SwitchMessage = struct { }, }) |next_state| prev_state = next_state; }, + .condition_wait => |condition_wait| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + const cond_state: *?*Fiber = @ptrCast(&condition_wait.cond.state); + assert(@atomicRmw(?*Fiber, cond_state, .Xchg, prev_fiber, .release) == null); // More than one wait on same Condition is illegal. + condition_wait.mutex.unlock(el.io()); + }, .exit => for (el.threads.allocated[0..@atomicLoad(u32, &el.threads.active, .acquire)]) |*each_thread| { getSqe(&thread.io_uring).* = .{ .opcode = .MSG_RING, @@ -1242,7 +1252,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - el.yield(null, .{ .lock_mutex = .{ + el.yield(null, .{ .mutex_lock = .{ .prev_state = prev_state, .mutex = mutex, } }); @@ -1271,13 +1281,10 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - const cond_state: *?*Fiber = @ptrCast(&cond.state); - const thread: *Thread = .current(); - const fiber = thread.currentFiber(); - const prev = @atomicRmw(?*Fiber, cond_state, .Xchg, fiber, .acquire); - assert(prev == null); // More than one wait on same Condition is illegal. - mutex.unlock(el.io()); - el.yield(null, .nothing); + el.yield(null, .{ .condition_wait = .{ + .cond = cond, + .mutex = mutex, + } }); try mutex.lock(el.io()); } From 0f105a8a10eabdf55683c9629996fe033baf3fcf Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 31 Mar 2025 23:51:51 -0700 Subject: [PATCH 030/244] EventLoop: let the allocator do its job to bucket and free fiber allocations --- lib/std/Io/EventLoop.zig | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 37ad088b6d..067ed04c3d 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -26,7 +26,6 @@ const Thread = struct { idle_context: Context, current_context: *Context, ready_queue: ?*Fiber, - free_queue: ?*Fiber, io_uring: IoUring, idle_search_index: u32, steal_ready_search_index: u32, @@ -78,12 +77,6 @@ const Fiber = struct { ); fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber { - const thread: *Thread = .current(); - if (thread.free_queue) |free_fiber| { - thread.free_queue = free_fiber.queue_next; - free_fiber.queue_next = null; - return free_fiber; - } return @ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), allocation_size)); } @@ -129,18 +122,15 @@ const Fiber = struct { )) |cancel_thread| assert(cancel_thread == Thread.canceling); } - fn recycle(fiber: *Fiber) void { - const thread: *Thread = .current(); - std.log.debug("recyling {*}", .{fiber}); - assert(fiber.queue_next == null); - //@memset(fiber.allocatedSlice(), undefined); // (race) - fiber.queue_next = thread.free_queue; - thread.free_queue = fiber; - } - const Queue = struct { head: *Fiber, tail: *Fiber }; }; +fn recycle(el: *EventLoop, fiber: *Fiber) void { + std.log.debug("recyling {*}", .{fiber}); + assert(fiber.queue_next == null); + el.gpa.free(fiber.allocatedSlice()); +} + pub fn io(el: *EventLoop) Io { return .{ .userdata = el, @@ -207,7 +197,6 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { }, .current_context = &main_fiber.context, .ready_queue = null, - .free_queue = null, .io_uring = try IoUring.init(io_uring_entries, 0), .idle_search_index = 1, .steal_ready_search_index = 1, @@ -227,11 +216,6 @@ pub fn deinit(el: *EventLoop) void { const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.allocated.ptr)); const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); for (el.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); - for (el.threads.allocated[0..active_threads]) |*thread| while (thread.free_queue) |free_fiber| { - thread.free_queue = free_fiber.queue_next; - free_fiber.queue_next = null; - el.gpa.free(free_fiber.allocatedSlice()); - }; el.gpa.free(allocated_ptr[0..idle_stack_end_offset]); el.* = undefined; } @@ -343,7 +327,6 @@ fn schedule(el: *EventLoop, thread: *Thread, ready_queue: Fiber.Queue) void { .idle_context = undefined, .current_context = &new_thread.idle_context, .ready_queue = ready_queue.head, - .free_queue = null, .io_uring = IoUring.init(io_uring_entries, 0) catch |err| { @atomicStore(u32, &el.threads.reserved, new_thread_index, .release); // no more access to `thread` after giving up reservation @@ -501,7 +484,7 @@ const SwitchMessage = struct { el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); }, .recycle => |fiber| { - fiber.recycle(); + el.recycle(fiber); }, .register_awaiter => |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); @@ -795,7 +778,7 @@ fn @"await"( if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, .{ .register_awaiter = &future_fiber.awaiter }); @memcpy(result, future_fiber.resultBytes(result_alignment)); - future_fiber.recycle(); + event_loop.recycle(future_fiber); } fn cancel( From 3eb7be5cf6cb7050991f84c11f786c61bc5599b4 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Tue, 1 Apr 2025 03:45:31 -0400 Subject: [PATCH 031/244] EventLoop: implement detached fibers --- lib/std/Io/EventLoop.zig | 124 ++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 46 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 067ed04c3d..83747bd008 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -9,9 +9,12 @@ const IoUring = std.os.linux.IoUring; /// Must be a thread-safe allocator. gpa: Allocator, -mutex: std.Thread.Mutex, main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)), threads: Thread.List, +detached: struct { + mutex: std.Io.Mutex, + list: std.DoublyLinkedList(void), +}, /// Empirically saw >128KB being used by the self-hosted backend to panic. const idle_stack_size = 256 * 1024; @@ -167,13 +170,16 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { errdefer gpa.free(allocated_slice); el.* = .{ .gpa = gpa, - .mutex = .{}, .main_fiber_buffer = undefined, .threads = .{ .allocated = @ptrCast(allocated_slice[0..threads_size]), .reserved = 1, .active = 1, }, + .detached = .{ + .mutex = .init, + .list = .{}, + }, }; const main_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); main_fiber.* = .{ @@ -207,6 +213,23 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { } pub fn deinit(el: *EventLoop) void { + while (true) cancel(el, detached_future: { + el.detached.mutex.lock(el.io()) catch |err| switch (err) { + error.Canceled => unreachable, // main fiber cannot be canceled + }; + defer el.detached.mutex.unlock(el.io()); + const detached: *DetachedClosure = @fieldParentPtr( + "detached_queue_node", + el.detached.list.pop() orelse break, + ); + // notify the detached fiber that it is no longer allowed to recycle itself + detached.detached_queue_node = .{ + .prev = &detached.detached_queue_node, + .next = &detached.detached_queue_node, + .data = {}, + }; + break :detached_future @ptrCast(detached.fiber); + }, &.{}, .@"1"); const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); for (el.threads.allocated[0..active_threads]) |*thread| { const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); @@ -460,7 +483,7 @@ const SwitchMessage = struct { const PendingTask = union(enum) { nothing, reschedule, - recycle: *Fiber, + recycle, register_awaiter: *?*Fiber, mutex_lock: struct { prev_state: Io.Mutex.State, @@ -483,8 +506,10 @@ const SwitchMessage = struct { assert(prev_fiber.queue_next == null); el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); }, - .recycle => |fiber| { - el.recycle(fiber); + .recycle => { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + el.recycle(prev_fiber); }, .register_awaiter => |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); @@ -609,21 +634,7 @@ 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)), - } -} - -fn fiberEntryDetached() callconv(.naked) void { - switch (builtin.cpu.arch) { - .x86_64 => asm volatile ( - \\ leaq 8(%%rsp), %%rdi - \\ jmp %[DetachedClosure_call:P] - : - : [DetachedClosure_call] "X" (&DetachedClosure.call), + \\ jmpq *(%%rsp) ), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), } @@ -649,29 +660,6 @@ const AsyncClosure = struct { } }; -const DetachedClosure = struct { - event_loop: *EventLoop, - fiber: *Fiber, - start: *const fn (context: *const anyopaque) void, - - fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { - return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure)); - } - - fn call(closure: *DetachedClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(DetachedClosure))) noreturn { - message.handle(closure.event_loop); - std.log.debug("{*} performing async detached", .{closure.fiber}); - closure.start(closure.contextPointer()); - const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); - if (awaiter) |a| { - closure.event_loop.yield(a, .nothing); - } else { - closure.event_loop.yield(null, .{ .recycle = closure.fiber }); - } - unreachable; // switched to dead fiber - } -}; - fn @"async"( userdata: ?*anyopaque, result: []u8, @@ -695,11 +683,13 @@ fn @"async"( const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, ) - @sizeOf(AsyncClosure)); + const stack_end: [*]usize = @alignCast(@ptrCast(closure)); + (stack_end - 1)[0..1].* = .{@intFromPtr(&AsyncClosure.call)}; fiber.* = .{ .required_align = {}, .context = switch (builtin.cpu.arch) { .x86_64 => .{ - .rsp = @intFromPtr(closure) - @sizeOf(usize), + .rsp = @intFromPtr(stack_end - 1), .rbp = 0, .rip = @intFromPtr(&fiberEntry), }, @@ -722,6 +712,34 @@ fn @"async"( return @ptrCast(fiber); } +const DetachedClosure = struct { + event_loop: *EventLoop, + fiber: *Fiber, + start: *const fn (context: *const anyopaque) void, + detached_queue_node: std.DoublyLinkedList(void).Node, + + fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { + return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure)); + } + + fn call(closure: *DetachedClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(DetachedClosure))) noreturn { + message.handle(closure.event_loop); + std.log.debug("{*} performing async detached", .{closure.fiber}); + closure.start(closure.contextPointer()); + const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + closure.event_loop.yield(awaiter, pending_task: { + closure.event_loop.detached.mutex.lock(closure.event_loop.io()) catch |err| switch (err) { + error.Canceled => break :pending_task .nothing, + }; + defer closure.event_loop.detached.mutex.unlock(closure.event_loop.io()); + if (closure.detached_queue_node.next == &closure.detached_queue_node) break :pending_task .nothing; + closure.event_loop.detached.list.remove(&closure.detached_queue_node); + break :pending_task .recycle; + }); + unreachable; // switched to dead fiber + } +}; + fn go( userdata: ?*anyopaque, context: []const u8, @@ -742,13 +760,15 @@ fn go( const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward( @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, ) - @sizeOf(DetachedClosure)); + const stack_end: [*]usize = @alignCast(@ptrCast(closure)); + (stack_end - 1)[0..1].* = .{@intFromPtr(&DetachedClosure.call)}; fiber.* = .{ .required_align = {}, .context = switch (builtin.cpu.arch) { .x86_64 => .{ - .rsp = @intFromPtr(closure) - @sizeOf(usize), + .rsp = @intFromPtr(stack_end - 1), .rbp = 0, - .rip = @intFromPtr(&fiberEntryDetached), + .rip = @intFromPtr(&fiberEntry), }, else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), }, @@ -761,7 +781,19 @@ fn go( .event_loop = event_loop, .fiber = fiber, .start = start, + .detached_queue_node = .{ .data = {} }, }; + { + event_loop.detached.mutex.lock(event_loop.io()) catch |err| switch (err) { + error.Canceled => { + event_loop.recycle(fiber); + start(context.ptr); + return; + }, + }; + defer event_loop.detached.mutex.unlock(event_loop.io()); + event_loop.detached.list.append(&closure.detached_queue_node); + } @memcpy(closure.contextPointer(), context); event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber }); From c4fcf85c43be10b703fa701ba377dd215dd91595 Mon Sep 17 00:00:00 2001 From: Jacob Young Date: Wed, 2 Apr 2025 18:03:53 -0400 Subject: [PATCH 032/244] Io.Condition: implement full API --- lib/std/Io.zig | 25 ++++++++++---- lib/std/Io/EventLoop.zig | 70 +++++++++++++++++++++++++++++++--------- lib/std/Thread/Pool.zig | 7 ++-- 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 54d6b4f697..3c7ae152a0 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -630,7 +630,7 @@ pub const VTable = struct { mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, - conditionWake: *const fn (?*anyopaque, cond: *Condition) void, + conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, @@ -809,9 +809,20 @@ pub const Condition = struct { return io.vtable.conditionWait(io.userdata, cond, mutex); } - pub fn wake(cond: *Condition, io: Io) void { - io.vtable.conditionWake(io.userdata, cond); + pub fn signal(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond, .one); } + + pub fn broadcast(cond: *Condition, io: Io) void { + io.vtable.conditionWake(io.userdata, cond, .all); + } + + pub const Wake = enum { + /// wake up only one thread + one, + /// wake up all thread + all, + }; }; pub const TypeErasedQueue = struct { @@ -863,7 +874,7 @@ pub const TypeErasedQueue = struct { remaining = remaining[copy_len..]; getter.data.remaining = getter.data.remaining[copy_len..]; if (getter.data.remaining.len == 0) { - getter.data.condition.wake(io); + getter.data.condition.signal(io); continue; } q.getters.prepend(getter); @@ -946,7 +957,7 @@ pub const TypeErasedQueue = struct { putter.data.remaining = putter.data.remaining[copy_len..]; remaining = remaining[copy_len..]; if (putter.data.remaining.len == 0) { - putter.data.condition.wake(io); + putter.data.condition.signal(io); } else { assert(remaining.len == 0); q.putters.prepend(putter); @@ -979,7 +990,7 @@ pub const TypeErasedQueue = struct { putter.data.remaining = putter.data.remaining[copy_len..]; q.put_index += copy_len; if (putter.data.remaining.len == 0) { - putter.data.condition.wake(io); + putter.data.condition.signal(io); continue; } const second_available = q.buffer[0..q.get_index]; @@ -988,7 +999,7 @@ pub const TypeErasedQueue = struct { putter.data.remaining = putter.data.remaining[copy_len..]; q.put_index = copy_len; if (putter.data.remaining.len == 0) { - putter.data.condition.wake(io); + putter.data.condition.signal(io); continue; } q.putters.prepend(putter); diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 83747bd008..edd00baac6 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -555,8 +555,24 @@ const SwitchMessage = struct { .condition_wait => |condition_wait| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); assert(prev_fiber.queue_next == null); - const cond_state: *?*Fiber = @ptrCast(&condition_wait.cond.state); - assert(@atomicRmw(?*Fiber, cond_state, .Xchg, prev_fiber, .release) == null); // More than one wait on same Condition is illegal. + const cond_impl = prev_fiber.resultPointer(ConditionImpl); + cond_impl.* = .{ + .tail = prev_fiber, + .event = .queued, + }; + if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&condition_wait.cond.state)), + null, + prev_fiber, + .release, + .acquire, + )) |waiting_fiber| { + const waiting_cond_impl = waiting_fiber.?.resultPointer(ConditionImpl); + assert(waiting_cond_impl.tail.queue_next == null); + waiting_cond_impl.tail.queue_next = prev_fiber; + waiting_cond_impl.tail = prev_fiber; + } condition_wait.mutex.unlock(el.io()); }, .exit => for (el.threads.allocated[0..@atomicLoad(u32, &el.threads.active, .acquire)]) |*each_thread| { @@ -1267,10 +1283,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - el.yield(null, .{ .mutex_lock = .{ - .prev_state = prev_state, - .mutex = mutex, - } }); + el.yield(null, .{ .mutex_lock = .{ .prev_state = prev_state, .mutex = mutex } }); } fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { var maybe_waiting_fiber: ?*Fiber = @ptrFromInt(@intFromEnum(prev_state)); @@ -1294,21 +1307,48 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut el.yield(maybe_waiting_fiber.?, .reschedule); } +const ConditionImpl = struct { + tail: *Fiber, + event: union(enum) { + queued, + wake: Io.Condition.Wake, + }, +}; + fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - el.yield(null, .{ .condition_wait = .{ - .cond = cond, - .mutex = mutex, - } }); + el.yield(null, .{ .condition_wait = .{ .cond = cond, .mutex = mutex } }); + const thread = Thread.current(); + const fiber = thread.currentFiber(); + const cond_impl = fiber.resultPointer(ConditionImpl); try mutex.lock(el.io()); + switch (cond_impl.event) { + .queued => {}, + .wake => |wake| if (fiber.queue_next) |next_fiber| switch (wake) { + .one => if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&cond.state)), + null, + next_fiber, + .release, + .acquire, + )) |old_fiber| { + const old_cond_impl = old_fiber.?.resultPointer(ConditionImpl); + assert(old_cond_impl.tail.queue_next == null); + old_cond_impl.tail.queue_next = next_fiber; + old_cond_impl.tail = cond_impl.tail; + }, + .all => el.schedule(thread, .{ .head = next_fiber, .tail = cond_impl.tail }), + }, + } + fiber.queue_next = null; } -fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition) void { +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); - const cond_state: *?*Fiber = @ptrCast(&cond.state); - if (@atomicRmw(?*Fiber, cond_state, .Xchg, null, .acquire)) |fiber| { - el.yield(fiber, .reschedule); - } + const waiting_fiber = @atomicRmw(?*Fiber, @as(*?*Fiber, @ptrCast(&cond.state)), .Xchg, null, .acquire) orelse return; + waiting_fiber.resultPointer(ConditionImpl).event = .{ .wake = wake }; + el.yield(waiting_fiber, .reschedule); } fn errno(signed: i32) std.os.linux.E { diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 05bc4801b6..1e9903e45d 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -666,7 +666,7 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I } } -fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition) void { +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); _ = pool; comptime assert(@TypeOf(cond.state) == u64); @@ -690,7 +690,10 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition) void { return; } - const to_wake = 1; + const to_wake = switch (wake) { + .one => 1, + .all => wakeable, + }; // Reserve the amount of waiters to wake by incrementing the signals count. // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. From 7aa4062f5c99acbd88e7a224513f77ec20cff7b2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 3 Apr 2025 17:54:37 -0700 Subject: [PATCH 033/244] introduce Io.select and implement it in thread pool --- lib/std/Io.zig | 223 ++++++++++++++++++++++++++++------------ lib/std/Thread/Pool.zig | 91 ++++++++++++---- 2 files changed, 232 insertions(+), 82 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 3c7ae152a0..f9d99525e9 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -626,17 +626,21 @@ pub const VTable = struct { /// Thread-safe. cancelRequested: *const fn (?*anyopaque) bool, + /// Blocks until one of the futures from the list has a result ready, such + /// that awaiting it will not block. Returns that index. + select: *const fn (?*anyopaque, futures: []const *AnyFuture) usize, + mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void, mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, - createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, - openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, - closeFile: *const fn (?*anyopaque, fs.File) void, - pread: *const fn (?*anyopaque, file: fs.File, buffer: []u8, offset: std.posix.off_t) FilePReadError!usize, - pwrite: *const fn (?*anyopaque, file: fs.File, buffer: []const u8, offset: std.posix.off_t) FilePWriteError!usize, + createFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File, + openFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File, + closeFile: *const fn (?*anyopaque, File) void, + pread: *const fn (?*anyopaque, file: File, buffer: []u8, offset: std.posix.off_t) File.PReadError!usize, + pwrite: *const fn (?*anyopaque, file: File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp, sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, @@ -647,28 +651,118 @@ pub const Cancelable = error{ Canceled, }; -pub const OpenFlags = fs.File.OpenFlags; -pub const CreateFlags = fs.File.CreateFlags; +pub const Dir = struct { + handle: Handle, -pub const FileOpenError = fs.File.OpenError || Cancelable; -pub const FileReadError = fs.File.ReadError || Cancelable; -pub const FilePReadError = fs.File.PReadError || Cancelable; -pub const FileWriteError = fs.File.WriteError || Cancelable; -pub const FilePWriteError = fs.File.PWriteError || Cancelable; + pub fn cwd() Dir { + return .{ .handle = std.fs.cwd().fd }; + } + + pub const Handle = std.posix.fd_t; + + pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + return io.vtable.openFile(io.userdata, dir, sub_path, flags); + } + + pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + return io.vtable.createFile(io.userdata, dir, sub_path, flags); + } + + pub const WriteFileOptions = struct { + /// On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). + /// On WASI, `sub_path` should be encoded as valid UTF-8. + /// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. + sub_path: []const u8, + data: []const u8, + flags: File.CreateFlags = .{}, + }; + + pub const WriteFileError = File.WriteError || File.OpenError || Cancelable; + + /// Writes content to the file system, using the file creation flags provided. + pub fn writeFile(dir: Dir, io: Io, options: WriteFileOptions) WriteFileError!void { + var file = try dir.createFile(io, options.sub_path, options.flags); + defer file.close(io); + try file.writeAll(io, options.data); + } +}; + +pub const File = struct { + handle: Handle, + + pub const Handle = std.posix.fd_t; + + pub const OpenFlags = fs.File.OpenFlags; + pub const CreateFlags = fs.File.CreateFlags; + + pub const OpenError = fs.File.OpenError || Cancelable; + + pub fn close(file: File, io: Io) void { + return io.vtable.closeFile(io.userdata, file); + } + + pub const ReadError = fs.File.ReadError || Cancelable; + + pub fn read(file: File, io: Io, buffer: []u8) ReadError!usize { + return @errorCast(file.pread(io, buffer, -1)); + } + + pub const PReadError = fs.File.PReadError || Cancelable; + + pub fn pread(file: File, io: Io, buffer: []u8, offset: std.posix.off_t) PReadError!usize { + return io.vtable.pread(io.userdata, file, buffer, offset); + } + + pub const WriteError = fs.File.WriteError || Cancelable; + + pub fn write(file: File, io: Io, buffer: []const u8) WriteError!usize { + return @errorCast(file.pwrite(io, buffer, -1)); + } + + pub const PWriteError = fs.File.PWriteError || Cancelable; + + pub fn pwrite(file: File, io: Io, buffer: []const u8, offset: std.posix.off_t) PWriteError!usize { + return io.vtable.pwrite(io.userdata, file, buffer, offset); + } + + pub fn writeAll(file: File, io: Io, bytes: []const u8) WriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try file.write(io, bytes[index..]); + } + } + + pub fn readAll(file: File, io: Io, buffer: []u8) ReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try file.read(io, buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; + } +}; pub const Timestamp = enum(i96) { _, - pub fn durationTo(from: Timestamp, to: Timestamp) i96 { - return @intFromEnum(to) - @intFromEnum(from); + pub fn durationTo(from: Timestamp, to: Timestamp) Duration { + return .{ .nanoseconds = @intFromEnum(to) - @intFromEnum(from) }; } - pub fn addDuration(from: Timestamp, duration: i96) Timestamp { - return @enumFromInt(@intFromEnum(from) + duration); + pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { + return @enumFromInt(@intFromEnum(from) + duration.nanoseconds); + } +}; +pub const Duration = struct { + nanoseconds: i96, + + pub fn ms(x: u64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_ms }; } }; pub const Deadline = union(enum) { - nanoseconds: i96, + duration: Duration, timestamp: Timestamp, }; pub const ClockGetTimeError = std.posix.ClockGetTimeError || Cancelable; @@ -1055,7 +1149,7 @@ pub fn Queue(Elem: type) type { /// Calls `function` with `args`, such that the return value of the function is /// not guaranteed to be available until `await` is called. -pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { +pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; const Args = @TypeOf(args); const TypeErased = struct { @@ -1079,7 +1173,7 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( /// Calls `function` with `args` asynchronously. The resource cleans itself up /// when the function returns. Does not support await, cancel, or a return value. -pub fn go(io: Io, function: anytype, args: anytype) void { +pub fn go(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); const TypeErased = struct { fn start(context: *const anyopaque) void { @@ -1095,51 +1189,6 @@ pub fn go(io: Io, function: anytype, args: anytype) void { ); } -pub fn openFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File { - return io.vtable.openFile(io.userdata, dir, sub_path, flags); -} - -pub fn createFile(io: Io, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File { - return io.vtable.createFile(io.userdata, dir, sub_path, flags); -} - -pub fn closeFile(io: Io, file: fs.File) void { - return io.vtable.closeFile(io.userdata, file); -} - -pub fn read(io: Io, file: fs.File, buffer: []u8) FileReadError!usize { - return @errorCast(io.pread(file, buffer, -1)); -} - -pub fn pread(io: Io, file: fs.File, buffer: []u8, offset: std.posix.off_t) FilePReadError!usize { - return io.vtable.pread(io.userdata, file, buffer, offset); -} - -pub fn write(io: Io, file: fs.File, buffer: []const u8) FileWriteError!usize { - return @errorCast(io.pwrite(file, buffer, -1)); -} - -pub fn pwrite(io: Io, file: fs.File, buffer: []const u8, offset: std.posix.off_t) FilePWriteError!usize { - return io.vtable.pwrite(io.userdata, file, buffer, offset); -} - -pub fn writeAll(io: Io, file: fs.File, bytes: []const u8) FileWriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try io.write(file, bytes[index..]); - } -} - -pub fn readAll(io: Io, file: fs.File, buffer: []u8) FileReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try io.read(file, buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; -} - pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { return io.vtable.now(io.userdata, clockid); } @@ -1147,3 +1196,49 @@ pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { pub fn sleep(io: Io, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void { return io.vtable.sleep(io.userdata, clockid, deadline); } + +pub fn sleepDuration(io: Io, duration: Duration) SleepError!void { + return io.vtable.sleep(io.userdata, .MONOTONIC, .{ .duration = duration }); +} + +/// Given a struct with each field a `*Future`, returns a union with the same +/// fields, each field type the future's result. +pub fn SelectUnion(S: type) type { + const struct_fields = @typeInfo(S).@"struct".fields; + var fields: [struct_fields.len]std.builtin.Type.UnionField = undefined; + for (&fields, struct_fields) |*union_field, struct_field| { + const F = @typeInfo(struct_field.type).pointer.child; + const Result = @TypeOf(@as(F, undefined).result); + union_field.* = .{ + .name = struct_field.name, + .type = Result, + .alignment = struct_field.alignment, + }; + } + return @Type(.{ .@"union" = .{ + .layout = .auto, + .tag_type = std.meta.FieldEnum(S), + .fields = &fields, + .decls = &.{}, + } }); +} + +/// `s` is a struct with every field a `*Future(T)`, where `T` can be any type, +/// and can be different for each field. +pub fn select(io: Io, s: anytype) SelectUnion(@TypeOf(s)) { + const U = SelectUnion(@TypeOf(s)); + const S = @TypeOf(s); + const fields = @typeInfo(S).@"struct".fields; + var futures: [fields.len]*AnyFuture = undefined; + inline for (fields, &futures) |field, *any_future| { + const future = @field(s, field.name); + any_future.* = future.any_future orelse return @unionInit(U, field.name, future.result); + } + switch (io.vtable.select(io.userdata, &futures)) { + inline 0...(fields.len - 1) => |selected_index| { + const field_name = fields[selected_index].name; + return @unionInit(U, field_name, @field(s, field_name).await(io)); + }, + else => unreachable, + } +} diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 1e9903e45d..64668a1c1f 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -335,6 +335,7 @@ pub fn io(pool: *Pool) Io { .go = go, .cancel = cancel, .cancelRequested = cancelRequested, + .select = select, .mutexLock = mutexLock, .mutexUnlock = mutexUnlock, @@ -358,10 +359,13 @@ const AsyncClosure = struct { func: *const fn (context: *anyopaque, result: *anyopaque) void, runnable: Runnable = .{ .runFn = runFn }, reset_event: std.Thread.ResetEvent, + select_condition: ?*std.Thread.ResetEvent, cancel_tid: std.Thread.Id, context_offset: usize, result_offset: usize, + const done_reset_event: *std.Thread.ResetEvent = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(std.Thread.ResetEvent))); + const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { .int => |int_info| switch (int_info.signedness) { .signed => -1, @@ -396,6 +400,17 @@ const AsyncClosure = struct { .acq_rel, .acquire, )) |cancel_tid| assert(cancel_tid == canceling_tid); + + if (@atomicRmw( + ?*std.Thread.ResetEvent, + &closure.select_condition, + .Xchg, + done_reset_event, + .release, + )) |select_reset| { + assert(select_reset != done_reset_event); + select_reset.set(); + } closure.reset_event.set(); } @@ -455,6 +470,7 @@ fn @"async"( .result_offset = result_offset, .reset_event = .{}, .cancel_tid = 0, + .select_condition = null, }; @memcpy(closure.contextPointer()[0..context.len], context); pool.run_queue.prepend(&closure.runnable.node); @@ -720,47 +736,54 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. fn createFile( userdata: ?*anyopaque, - dir: std.fs.Dir, + dir: Io.Dir, sub_path: []const u8, - flags: std.fs.File.CreateFlags, -) Io.FileOpenError!std.fs.File { + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); - return dir.createFile(sub_path, flags); + const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; + const fs_file = try fs_dir.createFile(sub_path, flags); + return .{ .handle = fs_file.handle }; } fn openFile( userdata: ?*anyopaque, - dir: std.fs.Dir, + dir: Io.Dir, sub_path: []const u8, - flags: std.fs.File.OpenFlags, -) Io.FileOpenError!std.fs.File { + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); - return dir.openFile(sub_path, flags); + const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; + const fs_file = try fs_dir.openFile(sub_path, flags); + return .{ .handle = fs_file.handle }; } -fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { +fn closeFile(userdata: ?*anyopaque, file: Io.File) void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); _ = pool; - return file.close(); + const fs_file: std.fs.File = .{ .handle = file.handle }; + return fs_file.close(); } -fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { +fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); + const fs_file: std.fs.File = .{ .handle = file.handle }; return switch (offset) { - -1 => file.read(buffer), - else => file.pread(buffer, @bitCast(offset)), + -1 => fs_file.read(buffer), + else => fs_file.pread(buffer, @bitCast(offset)), }; } -fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { +fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); + const fs_file: std.fs.File = .{ .handle = file.handle }; return switch (offset) { - -1 => file.write(buffer), - else => file.pwrite(buffer, @bitCast(offset)), + -1 => fs_file.write(buffer), + else => fs_file.pwrite(buffer, @bitCast(offset)), }; } @@ -774,7 +797,7 @@ fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); const deadline_nanoseconds: i96 = switch (deadline) { - .nanoseconds => |nanoseconds| nanoseconds, + .duration => |duration| duration.nanoseconds, .timestamp => |timestamp| @intFromEnum(timestamp), }; var timespec: std.posix.timespec = .{ @@ -784,7 +807,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl while (true) { try pool.checkCancel(); switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (deadline) { - .nanoseconds => false, + .duration => false, .timestamp => true, } }, ×pec, ×pec))) { .SUCCESS => return, @@ -795,3 +818,35 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl } } } + +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { + const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + + var reset_event: std.Thread.ResetEvent = .{}; + + for (futures, 0..) |future, i| { + const closure: *AsyncClosure = @ptrCast(@alignCast(future)); + if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) { + for (futures[0..i]) |cleanup_future| { + const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future)); + if (@atomicRmw(?*std.Thread.ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + cleanup_closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. + } + } + return i; + } + } + + reset_event.wait(); + + var result: ?usize = null; + for (futures, 0..) |future, i| { + const closure: *AsyncClosure = @ptrCast(@alignCast(future)); + if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. + if (result == null) result = i; // In case multiple are ready, return first. + } + } + return result.?; +} From f158ec5530b69c83fac1826ab136ecd78378bf78 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 3 Apr 2025 18:07:46 -0700 Subject: [PATCH 034/244] Io.EventLoop: select stub --- lib/std/Io/EventLoop.zig | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index edd00baac6..3fc5aa0bac 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -141,7 +141,7 @@ pub fn io(el: *EventLoop) Io { .@"async" = @"async", .@"await" = @"await", .go = go, - + .select = select, .cancel = cancel, .cancelRequested = cancelRequested, @@ -728,6 +728,13 @@ fn @"async"( return @ptrCast(fiber); } +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + _ = el; + _ = futures; + @panic("TODO"); +} + const DetachedClosure = struct { event_loop: *EventLoop, fiber: *Fiber, @@ -870,10 +877,10 @@ fn cancelRequested(userdata: ?*anyopaque) bool { fn createFile( userdata: ?*anyopaque, - dir: std.fs.Dir, + dir: Io.Dir, sub_path: []const u8, - flags: Io.CreateFlags, -) Io.FileOpenError!std.fs.File { + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -921,7 +928,7 @@ fn createFile( .opcode = .OPENAT, .flags = 0, .ioprio = 0, - .fd = dir.fd, + .fd = dir.handle, .off = 0, .addr = @intFromPtr(&sub_path_c), .len = @intCast(flags.mode), @@ -972,10 +979,10 @@ fn createFile( fn openFile( userdata: ?*anyopaque, - dir: std.fs.Dir, + dir: Io.Dir, sub_path: []const u8, - flags: Io.OpenFlags, -) Io.FileOpenError!std.fs.File { + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -1029,7 +1036,7 @@ fn openFile( .opcode = .OPENAT, .flags = 0, .ioprio = 0, - .fd = dir.fd, + .fd = dir.handle, .off = 0, .addr = @intFromPtr(&sub_path_c), .len = 0, @@ -1078,7 +1085,7 @@ fn openFile( } } -fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { +fn closeFile(userdata: ?*anyopaque, file: Io.File) void { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -1114,7 +1121,7 @@ fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { } } -fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { +fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -1166,7 +1173,7 @@ fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.pos } } -fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { +fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { const el: *EventLoop = @alignCast(@ptrCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; @@ -1236,7 +1243,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl try fiber.enterCancelRegion(thread); const deadline_nanoseconds: i96 = switch (deadline) { - .nanoseconds => |nanoseconds| nanoseconds, + .duration => |duration| duration.nanoseconds, .timestamp => |timestamp| @intFromEnum(timestamp), }; const timespec: std.os.linux.kernel_timespec = .{ @@ -1252,7 +1259,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl .addr = @intFromPtr(×pec), .len = 1, .rw_flags = @as(u32, switch (deadline) { - .nanoseconds => 0, + .duration => 0, .timestamp => std.os.linux.IORING_TIMEOUT_ABS, }) | @as(u32, switch (clockid) { .REALTIME => std.os.linux.IORING_TIMEOUT_REALTIME, From c88b8e3c15b85ff48139f396c79e98052797052e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 3 Apr 2025 21:15:19 -0700 Subject: [PATCH 035/244] std.Io.EventLoop: implement select --- lib/std/Io/EventLoop.zig | 92 +++++++++++++++++++++++++++++++--------- lib/std/Thread/Pool.zig | 2 +- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 3fc5aa0bac..b821f2f7e4 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -485,6 +485,7 @@ const SwitchMessage = struct { reschedule, recycle, register_awaiter: *?*Fiber, + register_select: []const *Io.AnyFuture, mutex_lock: struct { prev_state: Io.Mutex.State, mutex: *Io.Mutex, @@ -514,13 +515,21 @@ const SwitchMessage = struct { .register_awaiter => |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); assert(prev_fiber.queue_next == null); - if (@atomicRmw( - ?*Fiber, - awaiter, - .Xchg, - prev_fiber, - .acq_rel, - ) == Fiber.finished) el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .register_select => |futures| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + for (futures) |any_future| { + const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + if (@atomicRmw(?*Fiber, &future_fiber.awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) { + const closure: *AsyncClosure = .fromFiber(future_fiber); + if (!@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .seq_cst)) { + el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + } + } + } }, .mutex_lock => |mutex_lock| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); @@ -661,6 +670,7 @@ const AsyncClosure = struct { fiber: *Fiber, start: *const fn (context: *const anyopaque, result: *anyopaque) void, result_align: Alignment, + already_awaited: bool, fn contextPointer(closure: *AsyncClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(AsyncClosure)); @@ -668,12 +678,24 @@ const AsyncClosure = struct { 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.resultBytes(closure.result_align)); - const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); - closure.event_loop.yield(awaiter, .nothing); + const fiber = closure.fiber; + std.log.debug("{*} performing async", .{fiber}); + closure.start(closure.contextPointer(), fiber.resultBytes(closure.result_align)); + const awaiter = @atomicRmw(?*Fiber, &fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + const ready_awaiter = r: { + const a = awaiter orelse break :r null; + if (@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .acq_rel)) break :r null; + break :r a; + }; + closure.event_loop.yield(ready_awaiter, .nothing); unreachable; // switched to dead fiber } + + fn fromFiber(fiber: *Fiber) *AsyncClosure { + return @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( + @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, + ) - @sizeOf(AsyncClosure)); + } }; fn @"async"( @@ -696,9 +718,7 @@ fn @"async"( }; 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)); + const closure: *AsyncClosure = .fromFiber(fiber); const stack_end: [*]usize = @alignCast(@ptrCast(closure)); (stack_end - 1)[0..1].* = .{@intFromPtr(&AsyncClosure.call)}; fiber.* = .{ @@ -721,6 +741,7 @@ fn @"async"( .fiber = fiber, .start = start, .result_align = result_alignment, + .already_awaited = false, }; @memcpy(closure.contextPointer(), context); @@ -728,13 +749,6 @@ fn @"async"( return @ptrCast(fiber); } -fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); - _ = el; - _ = futures; - @panic("TODO"); -} - const DetachedClosure = struct { event_loop: *EventLoop, fiber: *Fiber, @@ -836,6 +850,42 @@ fn @"await"( event_loop.recycle(future_fiber); } +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { + const el: *EventLoop = @alignCast(@ptrCast(userdata)); + + // Optimization to avoid the yield below. + for (futures, 0..) |any_future, i| { + const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) == Fiber.finished) + return i; + } + + el.yield(null, .{ .register_select = futures }); + + std.log.debug("back from select yield", .{}); + + const my_thread: *Thread = .current(); + const my_fiber = my_thread.currentFiber(); + var result: ?usize = null; + + for (futures, 0..) |any_future, i| { + const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + if (@cmpxchgStrong(?*Fiber, &future_fiber.awaiter, my_fiber, null, .seq_cst, .seq_cst)) |awaiter| { + if (awaiter == Fiber.finished) { + if (result == null) result = i; + } else if (awaiter) |a| { + const closure: *AsyncClosure = .fromFiber(a); + closure.already_awaited = false; + } + } else { + const closure: *AsyncClosure = .fromFiber(my_fiber); + closure.already_awaited = false; + } + } + + return result.?; +} + fn cancel( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 64668a1c1f..02daa473f8 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -364,7 +364,7 @@ const AsyncClosure = struct { context_offset: usize, result_offset: usize, - const done_reset_event: *std.Thread.ResetEvent = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(std.Thread.ResetEvent))); + const done_reset_event: *std.Thread.ResetEvent = @ptrFromInt(@alignOf(std.Thread.ResetEvent)); const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { .int => |int_info| switch (int_info.signedness) { From 4b657d2de560a0a97d60ab22013fc190e1819486 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 4 Apr 2025 15:02:15 -0700 Subject: [PATCH 036/244] std.Io: remove `@ptrCast` workarounds thanks to d53cc5e5b2ac51793ea19a847d8cee409af1dee3 --- lib/std/Io.zig | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index f9d99525e9..dddd86ecff 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -781,24 +781,14 @@ pub fn Future(Result: type) type { /// Idempotent. pub fn cancel(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; - io.vtable.cancel( - io.userdata, - any_future, - if (@sizeOf(Result) == 0) &.{} else @ptrCast((&f.result)[0..1]), // work around compiler bug - .of(Result), - ); + io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); f.any_future = null; return f.result; } pub fn await(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; - io.vtable.await( - io.userdata, - any_future, - if (@sizeOf(Result) == 0) &.{} else @ptrCast((&f.result)[0..1]), // work around compiler bug - .of(Result), - ); + io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); f.any_future = null; return f.result; } @@ -1162,9 +1152,9 @@ pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(functio var future: Future(Result) = undefined; future.any_future = io.vtable.async( io.userdata, - if (@sizeOf(Result) == 0) &.{} else @ptrCast((&future.result)[0..1]), // work around compiler bug + @ptrCast((&future.result)[0..1]), .of(Result), - if (@sizeOf(Args) == 0) &.{} else @ptrCast((&args)[0..1]), // work around compiler bug + @ptrCast((&args)[0..1]), .of(Args), TypeErased.start, ); @@ -1181,12 +1171,7 @@ pub fn go(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function)) @call(.auto, function, args_casted.*); } }; - io.vtable.go( - io.userdata, - if (@sizeOf(Args) == 0) &.{} else @ptrCast((&args)[0..1]), // work around compiler bug - .of(Args), - TypeErased.start, - ); + io.vtable.go(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); } pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { From e1cbcecf89d9ac60f00b3d78c3d5ca7db0f35c5c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 4 Apr 2025 15:19:55 -0700 Subject: [PATCH 037/244] Io: update for new linked list API --- lib/std/Io.zig | 82 ++++++++++++++++++++-------------------- lib/std/Io/EventLoop.zig | 7 ++-- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index dddd86ecff..ffec231b4f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -917,17 +917,19 @@ pub const TypeErasedQueue = struct { put_index: usize, get_index: usize, - putters: std.DoublyLinkedList(PutNode), - getters: std.DoublyLinkedList(GetNode), + putters: std.DoublyLinkedList, + getters: std.DoublyLinkedList, - const PutNode = struct { + const Put = struct { remaining: []const u8, condition: Condition, + node: std.DoublyLinkedList.Node, }; - const GetNode = struct { + const Get = struct { remaining: []u8, condition: Condition, + node: std.DoublyLinkedList.Node, }; pub fn init(buffer: []u8) TypeErasedQueue { @@ -952,16 +954,16 @@ pub const TypeErasedQueue = struct { var remaining = elements; while (true) { - const getter = q.getters.popFirst() orelse break; - const copy_len = @min(getter.data.remaining.len, remaining.len); - @memcpy(getter.data.remaining[0..copy_len], remaining[0..copy_len]); + const getter: *Get = @fieldParentPtr("node", q.getters.popFirst() orelse break); + const copy_len = @min(getter.remaining.len, remaining.len); + @memcpy(getter.remaining[0..copy_len], remaining[0..copy_len]); remaining = remaining[copy_len..]; - getter.data.remaining = getter.data.remaining[copy_len..]; - if (getter.data.remaining.len == 0) { - getter.data.condition.signal(io); + getter.remaining = getter.remaining[copy_len..]; + if (getter.remaining.len == 0) { + getter.condition.signal(io); continue; } - q.getters.prepend(getter); + q.getters.prepend(&getter.node); assert(remaining.len == 0); return elements.len; } @@ -987,12 +989,10 @@ pub const TypeErasedQueue = struct { const total_filled = elements.len - remaining.len; if (total_filled >= min) return total_filled; - var node: std.DoublyLinkedList(PutNode).Node = .{ - .data = .{ .remaining = remaining, .condition = .{} }, - }; - q.putters.append(&node); - try node.data.condition.wait(io, &q.mutex); - remaining = node.data.remaining; + var pending: Put = .{ .remaining = remaining, .condition = .{}, .node = .{} }; + q.putters.append(&pending.node); + try pending.condition.wait(io, &q.mutex); + remaining = pending.remaining; } } @@ -1035,16 +1035,16 @@ pub const TypeErasedQueue = struct { } // Copy directly from putters into buffer. while (remaining.len > 0) { - const putter = q.putters.popFirst() orelse break; - const copy_len = @min(putter.data.remaining.len, remaining.len); - @memcpy(remaining[0..copy_len], putter.data.remaining[0..copy_len]); - putter.data.remaining = putter.data.remaining[copy_len..]; + const putter: *Put = @fieldParentPtr("node", q.putters.popFirst() orelse break); + const copy_len = @min(putter.remaining.len, remaining.len); + @memcpy(remaining[0..copy_len], putter.remaining[0..copy_len]); + putter.remaining = putter.remaining[copy_len..]; remaining = remaining[copy_len..]; - if (putter.data.remaining.len == 0) { - putter.data.condition.signal(io); + if (putter.remaining.len == 0) { + putter.condition.signal(io); } else { assert(remaining.len == 0); - q.putters.prepend(putter); + q.putters.prepend(&putter.node); return fillRingBufferFromPutters(q, io, buffer.len); } } @@ -1052,12 +1052,10 @@ pub const TypeErasedQueue = struct { const total_filled = buffer.len - remaining.len; if (total_filled >= min) return total_filled; - var node: std.DoublyLinkedList(GetNode).Node = .{ - .data = .{ .remaining = remaining, .condition = .{} }, - }; - q.getters.append(&node); - try node.data.condition.wait(io, &q.mutex); - remaining = node.data.remaining; + var pending: Get = .{ .remaining = remaining, .condition = .{}, .node = .{} }; + q.getters.append(&pending.node); + try pending.condition.wait(io, &q.mutex); + remaining = pending.remaining; } } @@ -1067,26 +1065,26 @@ pub const TypeErasedQueue = struct { /// buffers been fully copied. fn fillRingBufferFromPutters(q: *TypeErasedQueue, io: Io, len: usize) usize { while (true) { - const putter = q.putters.popFirst() orelse return len; + const putter: *Put = @fieldParentPtr("node", q.putters.popFirst() orelse return len); const available = q.buffer[q.put_index..]; - const copy_len = @min(available.len, putter.data.remaining.len); - @memcpy(available[0..copy_len], putter.data.remaining[0..copy_len]); - putter.data.remaining = putter.data.remaining[copy_len..]; + const copy_len = @min(available.len, putter.remaining.len); + @memcpy(available[0..copy_len], putter.remaining[0..copy_len]); + putter.remaining = putter.remaining[copy_len..]; q.put_index += copy_len; - if (putter.data.remaining.len == 0) { - putter.data.condition.signal(io); + if (putter.remaining.len == 0) { + putter.condition.signal(io); continue; } const second_available = q.buffer[0..q.get_index]; - const second_copy_len = @min(second_available.len, putter.data.remaining.len); - @memcpy(second_available[0..second_copy_len], putter.data.remaining[0..second_copy_len]); - putter.data.remaining = putter.data.remaining[copy_len..]; + const second_copy_len = @min(second_available.len, putter.remaining.len); + @memcpy(second_available[0..second_copy_len], putter.remaining[0..second_copy_len]); + putter.remaining = putter.remaining[copy_len..]; q.put_index = copy_len; - if (putter.data.remaining.len == 0) { - putter.data.condition.signal(io); + if (putter.remaining.len == 0) { + putter.condition.signal(io); continue; } - q.putters.prepend(putter); + q.putters.prepend(&putter.node); return len; } } diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index b821f2f7e4..6f067821bf 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -13,7 +13,7 @@ main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fib threads: Thread.List, detached: struct { mutex: std.Io.Mutex, - list: std.DoublyLinkedList(void), + list: std.DoublyLinkedList, }, /// Empirically saw >128KB being used by the self-hosted backend to panic. @@ -226,7 +226,6 @@ pub fn deinit(el: *EventLoop) void { detached.detached_queue_node = .{ .prev = &detached.detached_queue_node, .next = &detached.detached_queue_node, - .data = {}, }; break :detached_future @ptrCast(detached.fiber); }, &.{}, .@"1"); @@ -753,7 +752,7 @@ const DetachedClosure = struct { event_loop: *EventLoop, fiber: *Fiber, start: *const fn (context: *const anyopaque) void, - detached_queue_node: std.DoublyLinkedList(void).Node, + detached_queue_node: std.DoublyLinkedList.Node, fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure)); @@ -818,7 +817,7 @@ fn go( .event_loop = event_loop, .fiber = fiber, .start = start, - .detached_queue_node = .{ .data = {} }, + .detached_queue_node = .{}, }; { event_loop.detached.mutex.lock(event_loop.io()) catch |err| switch (err) { From fd4dd3befb771b7ddd6c4f899a05ca18f91e1085 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 9 Jul 2025 22:48:24 -0700 Subject: [PATCH 038/244] update to sync with master --- lib/std/Io/EventLoop.zig | 14 +++++++------- lib/std/Thread/Pool.zig | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 6f067821bf..8f9cfd658d 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -80,7 +80,7 @@ const Fiber = struct { ); fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber { - return @ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), allocation_size)); + return @ptrCast(try el.gpa.alignedAlloc(u8, .of(Fiber), allocation_size)); } fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { @@ -138,8 +138,8 @@ pub fn io(el: *EventLoop) Io { return .{ .userdata = el, .vtable = &.{ - .@"async" = @"async", - .@"await" = @"await", + .async = async, + .await = await, .go = go, .select = select, .cancel = cancel, @@ -166,7 +166,7 @@ pub fn io(el: *EventLoop) Io { 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); + const allocated_slice = try gpa.alignedAlloc(u8, .of(Thread), idle_stack_end_offset); errdefer gpa.free(allocated_slice); el.* = .{ .gpa = gpa, @@ -697,7 +697,7 @@ const AsyncClosure = struct { } }; -fn @"async"( +fn async( userdata: ?*anyopaque, result: []u8, result_alignment: Alignment, @@ -835,7 +835,7 @@ fn go( event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber }); } -fn @"await"( +fn await( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8, @@ -916,7 +916,7 @@ fn cancel( .resv = 0, }; }; - @"await"(userdata, any_future, result, result_alignment); + await(userdata, any_future, result, result_alignment); } fn cancelRequested(userdata: ?*anyopaque) bool { diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index 02daa473f8..b9d60c3568 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -330,8 +330,8 @@ pub fn io(pool: *Pool) Io { return .{ .userdata = pool, .vtable = &.{ - .@"async" = @"async", - .@"await" = @"await", + .async = async, + .await = await, .go = go, .cancel = cancel, .cancelRequested = cancelRequested, @@ -444,7 +444,7 @@ const AsyncClosure = struct { } }; -fn @"async"( +fn async( userdata: ?*anyopaque, result: []u8, result_alignment: std.mem.Alignment, @@ -560,7 +560,7 @@ fn go( pool.cond.signal(); } -fn @"await"( +fn await( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8, From 14c3dc4c490158bfc42d02d35a6ae13c7e6a0afc Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 9 Jul 2025 23:10:31 -0700 Subject: [PATCH 039/244] revert std.Thread.Pool for now and move the Io impl to a separate file --- lib/std/Io.zig | 1 + lib/std/Io/ThreadPool.zig | 852 ++++++++++++++++++++++++++++++++++++++ lib/std/Thread/Pool.zig | 730 +++++--------------------------- 3 files changed, 955 insertions(+), 628 deletions(-) create mode 100644 lib/std/Io/ThreadPool.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index ffec231b4f..effb0e9383 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -558,6 +558,7 @@ test { const Io = @This(); pub const EventLoop = @import("Io/EventLoop.zig"); +pub const ThreadPool = @import("Io/ThreadPool.zig"); userdata: ?*anyopaque, vtable: *const VTable, diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig new file mode 100644 index 0000000000..f356a87054 --- /dev/null +++ b/lib/std/Io/ThreadPool.zig @@ -0,0 +1,852 @@ +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const WaitGroup = std.Thread.WaitGroup; +const Io = std.Io; +const Pool = @This(); + +/// Must be a thread-safe allocator. +allocator: std.mem.Allocator, +mutex: std.Thread.Mutex = .{}, +cond: std.Thread.Condition = .{}, +run_queue: std.SinglyLinkedList = .{}, +is_running: bool = true, +threads: std.ArrayListUnmanaged(std.Thread), +ids: if (builtin.single_threaded) struct { + inline fn deinit(_: @This(), _: std.mem.Allocator) void {} + fn getIndex(_: @This(), _: std.Thread.Id) usize { + return 0; + } +} else std.AutoArrayHashMapUnmanaged(std.Thread.Id, void), +stack_size: usize, + +threadlocal var current_closure: ?*AsyncClosure = null; + +pub const Runnable = struct { + runFn: RunProto, + node: std.SinglyLinkedList.Node = .{}, +}; + +pub const RunProto = *const fn (*Runnable, id: ?usize) void; + +pub const Options = struct { + allocator: std.mem.Allocator, + n_jobs: ?usize = null, + track_ids: bool = false, + stack_size: usize = std.Thread.SpawnConfig.default_stack_size, +}; + +pub fn init(pool: *Pool, options: Options) !void { + const gpa = options.allocator; + const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1); + const threads = try gpa.alloc(std.Thread, thread_count); + errdefer gpa.free(threads); + + pool.* = .{ + .allocator = gpa, + .threads = .initBuffer(threads), + .ids = .{}, + .stack_size = options.stack_size, + }; + + if (builtin.single_threaded) return; + + if (options.track_ids) { + try pool.ids.ensureTotalCapacity(gpa, 1 + thread_count); + pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {}); + } +} + +pub fn deinit(pool: *Pool) void { + const gpa = pool.allocator; + pool.join(); + pool.threads.deinit(gpa); + pool.ids.deinit(gpa); + pool.* = undefined; +} + +fn join(pool: *Pool) void { + if (builtin.single_threaded) return; + + { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + // ensure future worker threads exit the dequeue loop + pool.is_running = false; + } + + // wake up any sleeping threads (this can be done outside the mutex) + // then wait for all the threads we know are spawned to complete. + pool.cond.broadcast(); + for (pool.threads.items) |thread| thread.join(); +} + +/// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and +/// `WaitGroup.finish` after it returns. +/// +/// In the case that queuing the function call fails to allocate memory, or the +/// target is single-threaded, the function is called directly. +pub fn spawnWg(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args: anytype) void { + wait_group.start(); + + if (builtin.single_threaded) { + @call(.auto, func, args); + wait_group.finish(); + return; + } + + const Args = @TypeOf(args); + const Closure = struct { + arguments: Args, + pool: *Pool, + runnable: Runnable = .{ .runFn = runFn }, + wait_group: *WaitGroup, + + fn runFn(runnable: *Runnable, _: ?usize) void { + const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); + @call(.auto, func, closure.arguments); + closure.wait_group.finish(); + closure.pool.allocator.destroy(closure); + } + }; + + pool.mutex.lock(); + + const gpa = pool.allocator; + const closure = gpa.create(Closure) catch { + pool.mutex.unlock(); + @call(.auto, func, args); + wait_group.finish(); + return; + }; + closure.* = .{ + .arguments = args, + .pool = pool, + .wait_group = wait_group, + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); + pool.cond.signal(); +} + +/// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and +/// `WaitGroup.finish` after it returns. +/// +/// The first argument passed to `func` is a dense `usize` thread id, the rest +/// of the arguments are passed from `args`. Requires the pool to have been +/// initialized with `.track_ids = true`. +/// +/// In the case that queuing the function call fails to allocate memory, or the +/// target is single-threaded, the function is called directly. +pub fn spawnWgId(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args: anytype) void { + wait_group.start(); + + if (builtin.single_threaded) { + @call(.auto, func, .{0} ++ args); + wait_group.finish(); + return; + } + + const Args = @TypeOf(args); + const Closure = struct { + arguments: Args, + pool: *Pool, + runnable: Runnable = .{ .runFn = runFn }, + wait_group: *WaitGroup, + + fn runFn(runnable: *Runnable, id: ?usize) void { + const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); + @call(.auto, func, .{id.?} ++ closure.arguments); + closure.wait_group.finish(); + closure.pool.allocator.destroy(closure); + } + }; + + pool.mutex.lock(); + + const gpa = pool.allocator; + const closure = gpa.create(Closure) catch { + const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId()); + pool.mutex.unlock(); + @call(.auto, func, .{id.?} ++ args); + wait_group.finish(); + return; + }; + closure.* = .{ + .arguments = args, + .pool = pool, + .wait_group = wait_group, + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); + pool.cond.signal(); +} + +pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) void { + if (builtin.single_threaded) { + @call(.auto, func, args); + return; + } + + const Args = @TypeOf(args); + const Closure = struct { + arguments: Args, + pool: *Pool, + runnable: Runnable = .{ .runFn = runFn }, + + fn runFn(runnable: *Runnable, _: ?usize) void { + const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); + @call(.auto, func, closure.arguments); + closure.pool.allocator.destroy(closure); + } + }; + + pool.mutex.lock(); + + const gpa = pool.allocator; + const closure = gpa.create(Closure) catch { + pool.mutex.unlock(); + @call(.auto, func, args); + return; + }; + closure.* = .{ + .arguments = args, + .pool = pool, + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); + pool.cond.signal(); +} + +test spawn { + const TestFn = struct { + fn checkRun(completed: *bool) void { + completed.* = true; + } + }; + + var completed: bool = false; + + { + var pool: Pool = undefined; + try pool.init(.{ + .allocator = std.testing.allocator, + }); + defer pool.deinit(); + pool.spawn(TestFn.checkRun, .{&completed}); + } + + try std.testing.expectEqual(true, completed); +} + +fn worker(pool: *Pool) void { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + const id: ?usize = if (pool.ids.count() > 0) @intCast(pool.ids.count()) else null; + if (id) |_| pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {}); + + while (true) { + while (pool.run_queue.popFirst()) |run_node| { + // Temporarily unlock the mutex in order to execute the run_node + pool.mutex.unlock(); + defer pool.mutex.lock(); + + const runnable: *Runnable = @fieldParentPtr("node", run_node); + runnable.runFn(runnable, id); + } + + // Stop executing instead of waiting if the thread pool is no longer running. + if (pool.is_running) { + pool.cond.wait(&pool.mutex); + } else { + break; + } + } +} + +pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void { + var id: ?usize = null; + + while (!wait_group.isDone()) { + pool.mutex.lock(); + if (pool.run_queue.popFirst()) |run_node| { + id = id orelse pool.ids.getIndex(std.Thread.getCurrentId()); + pool.mutex.unlock(); + const runnable: *Runnable = @fieldParentPtr("node", run_node); + runnable.runFn(runnable, id); + continue; + } + + pool.mutex.unlock(); + wait_group.wait(); + return; + } +} + +pub fn getIdCount(pool: *Pool) usize { + return @intCast(1 + pool.threads.items.len); +} + +pub fn io(pool: *Pool) Io { + return .{ + .userdata = pool, + .vtable = &.{ + .async = async, + .await = await, + .go = go, + .cancel = cancel, + .cancelRequested = cancelRequested, + .select = select, + + .mutexLock = mutexLock, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWake = conditionWake, + + .createFile = createFile, + .openFile = openFile, + .closeFile = closeFile, + .pread = pread, + .pwrite = pwrite, + + .now = now, + .sleep = sleep, + }, + }; +} + +const AsyncClosure = struct { + func: *const fn (context: *anyopaque, result: *anyopaque) void, + runnable: Runnable = .{ .runFn = runFn }, + reset_event: std.Thread.ResetEvent, + select_condition: ?*std.Thread.ResetEvent, + cancel_tid: std.Thread.Id, + context_offset: usize, + result_offset: usize, + + const done_reset_event: *std.Thread.ResetEvent = @ptrFromInt(@alignOf(std.Thread.ResetEvent)); + + const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { + .int => |int_info| switch (int_info.signedness) { + .signed => -1, + .unsigned => std.math.maxInt(std.Thread.Id), + }, + .pointer => @ptrFromInt(std.math.maxInt(usize)), + else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), + }; + + fn runFn(runnable: *Pool.Runnable, _: ?usize) void { + const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable)); + const tid = std.Thread.getCurrentId(); + if (@cmpxchgStrong( + std.Thread.Id, + &closure.cancel_tid, + 0, + tid, + .acq_rel, + .acquire, + )) |cancel_tid| { + assert(cancel_tid == canceling_tid); + return; + } + current_closure = closure; + closure.func(closure.contextPointer(), closure.resultPointer()); + current_closure = null; + if (@cmpxchgStrong( + std.Thread.Id, + &closure.cancel_tid, + tid, + 0, + .acq_rel, + .acquire, + )) |cancel_tid| assert(cancel_tid == canceling_tid); + + if (@atomicRmw( + ?*std.Thread.ResetEvent, + &closure.select_condition, + .Xchg, + done_reset_event, + .release, + )) |select_reset| { + assert(select_reset != done_reset_event); + select_reset.set(); + } + closure.reset_event.set(); + } + + fn contextOffset(context_alignment: std.mem.Alignment) usize { + return context_alignment.forward(@sizeOf(AsyncClosure)); + } + + fn resultOffset( + context_alignment: std.mem.Alignment, + context_len: usize, + result_alignment: std.mem.Alignment, + ) usize { + return result_alignment.forward(contextOffset(context_alignment) + context_len); + } + + fn resultPointer(closure: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(closure); + return base + closure.result_offset; + } + + fn contextPointer(closure: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(closure); + return base + closure.context_offset; + } + + fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void { + closure.reset_event.wait(); + const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); + @memcpy(result, closure.resultPointer()[0..result.len]); + gpa.free(base[0 .. closure.result_offset + result.len]); + } +}; + +fn async( + userdata: ?*anyopaque, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*Io.AnyFuture { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + pool.mutex.lock(); + + const gpa = pool.allocator; + const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); + const result_offset = result_alignment.forward(context_offset + context.len); + const n = result_offset + result.len; + const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { + pool.mutex.unlock(); + start(context.ptr, result.ptr); + return null; + })); + closure.* = .{ + .func = start, + .context_offset = context_offset, + .result_offset = result_offset, + .reset_event = .{}, + .cancel_tid = 0, + .select_condition = null, + }; + @memcpy(closure.contextPointer()[0..context.len], context); + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); + pool.cond.signal(); + + return @ptrCast(closure); +} + +const DetachedClosure = struct { + pool: *Pool, + func: *const fn (context: *anyopaque) void, + run_node: Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, + context_alignment: std.mem.Alignment, + context_len: usize, + + fn runFn(runnable: *Pool.Runnable, _: ?usize) void { + const run_node: *Pool.RunQueue.Node = @fieldParentPtr("data", runnable); + const closure: *DetachedClosure = @alignCast(@fieldParentPtr("run_node", run_node)); + closure.func(closure.contextPointer()); + const gpa = closure.pool.allocator; + const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure); + gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]); + } + + fn contextOffset(context_alignment: std.mem.Alignment) usize { + return context_alignment.forward(@sizeOf(DetachedClosure)); + } + + fn contextEnd(context_alignment: std.mem.Alignment, context_len: usize) usize { + return contextOffset(context_alignment) + context_len; + } + + fn contextPointer(closure: *DetachedClosure) [*]u8 { + const base: [*]u8 = @ptrCast(closure); + return base + contextOffset(closure.context_alignment); + } +}; + +fn go( + userdata: ?*anyopaque, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, +) void { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + pool.mutex.lock(); + + const gpa = pool.allocator; + const n = DetachedClosure.contextEnd(context_alignment, context.len); + const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch { + pool.mutex.unlock(); + start(context.ptr); + return; + })); + closure.* = .{ + .pool = pool, + .func = start, + .context_alignment = context_alignment, + .context_len = context.len, + }; + @memcpy(closure.contextPointer()[0..context.len], context); + pool.run_queue.prepend(&closure.run_node); + + if (pool.threads.items.len < pool.threads.capacity) { + pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ + .stack_size = pool.stack_size, + .allocator = gpa, + }, worker, .{pool}) catch t: { + pool.threads.items.len -= 1; + break :t undefined; + }; + } + + pool.mutex.unlock(); + pool.cond.signal(); +} + +fn await( + userdata: ?*anyopaque, + any_future: *std.Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = result_alignment; + const pool: *Pool = @alignCast(@ptrCast(userdata)); + const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); + closure.waitAndFree(pool.allocator, result); +} + +fn cancel( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + _ = result_alignment; + const pool: *Pool = @alignCast(@ptrCast(userdata)); + const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); + switch (@atomicRmw( + std.Thread.Id, + &closure.cancel_tid, + .Xchg, + AsyncClosure.canceling_tid, + .acq_rel, + )) { + 0, AsyncClosure.canceling_tid => {}, + else => |cancel_tid| switch (builtin.os.tag) { + .linux => _ = std.os.linux.tgkill( + std.os.linux.getpid(), + @bitCast(cancel_tid), + std.posix.SIG.IO, + ), + else => {}, + }, + } + closure.waitAndFree(pool.allocator, result); +} + +fn cancelRequested(userdata: ?*anyopaque) bool { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + const closure = current_closure orelse return false; + return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; +} + +fn checkCancel(pool: *Pool) error{Canceled}!void { + if (cancelRequested(pool)) return error.Canceled; +} + +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { + _ = userdata; + if (prev_state == .contended) { + std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } + while (@atomicRmw( + Io.Mutex.State, + &mutex.state, + .Xchg, + .contended, + .acquire, + ) != .unlocked) { + std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } +} +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + _ = userdata; + _ = prev_state; + if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { + std.Thread.Futex.wake(@ptrCast(&mutex.state), 1); + } +} + +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + // Observe the epoch, then check the state again to see if we should wake up. + // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock: + // + // - T1: s = LOAD(&state) + // - T2: UPDATE(&s, signal) + // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch) + // - T1: e = LOAD(&epoch) (was reordered after the state load) + // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change) + // + // Acquire barrier to ensure the epoch load happens before the state load. + var epoch = cond_epoch.load(.acquire); + var state = cond_state.fetchAdd(one_waiter, .monotonic); + assert(state & waiter_mask != waiter_mask); + state += one_waiter; + + mutex.unlock(pool.io()); + defer mutex.lock(pool.io()) catch @panic("TODO"); + + var futex_deadline = std.Thread.Futex.Deadline.init(null); + + while (true) { + futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { + error.Timeout => unreachable, + }; + + epoch = cond_epoch.load(.acquire); + state = cond_state.load(.monotonic); + + // Try to wake up by consuming a signal and decremented the waiter we added previously. + // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. + while (state & signal_mask != 0) { + const new_state = state - one_waiter - one_signal; + state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; + } + } +} + +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + var state = cond_state.load(.monotonic); + while (true) { + const waiters = (state & waiter_mask) / one_waiter; + const signals = (state & signal_mask) / one_signal; + + // Reserves which waiters to wake up by incrementing the signals count. + // Therefore, the signals count is always less than or equal to the waiters count. + // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters. + const wakeable = waiters - signals; + if (wakeable == 0) { + return; + } + + const to_wake = switch (wake) { + .one => 1, + .all => wakeable, + }; + + // Reserve the amount of waiters to wake by incrementing the signals count. + // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. + const new_state = state + (one_signal * to_wake); + state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { + // Wake up the waiting threads we reserved above by changing the epoch value. + // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it. + // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption. + // + // Release barrier ensures the signal being added to the state happens before the epoch is changed. + // If not, the waiting thread could potentially deadlock from missing both the state and epoch change: + // + // - T2: UPDATE(&epoch, 1) (reordered before the state change) + // - T1: e = LOAD(&epoch) + // - T1: s = LOAD(&state) + // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) + // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) + _ = cond_epoch.fetchAdd(1, .release); + std.Thread.Futex.wake(cond_epoch, to_wake); + return; + }; + } +} + +fn createFile( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; + const fs_file = try fs_dir.createFile(sub_path, flags); + return .{ .handle = fs_file.handle }; +} + +fn openFile( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; + const fs_file = try fs_dir.openFile(sub_path, flags); + return .{ .handle = fs_file.handle }; +} + +fn closeFile(userdata: ?*anyopaque, file: Io.File) void { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + const fs_file: std.fs.File = .{ .handle = file.handle }; + return fs_file.close(); +} + +fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + const fs_file: std.fs.File = .{ .handle = file.handle }; + return switch (offset) { + -1 => fs_file.read(buffer), + else => fs_file.pread(buffer, @bitCast(offset)), + }; +} + +fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + const fs_file: std.fs.File = .{ .handle = file.handle }; + return switch (offset) { + -1 => fs_file.write(buffer), + else => fs_file.pwrite(buffer, @bitCast(offset)), + }; +} + +fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + const timespec = try std.posix.clock_gettime(clockid); + return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); +} + +fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + const deadline_nanoseconds: i96 = switch (deadline) { + .duration => |duration| duration.nanoseconds, + .timestamp => |timestamp| @intFromEnum(timestamp), + }; + var timespec: std.posix.timespec = .{ + .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), + .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), + }; + while (true) { + try pool.checkCancel(); + switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (deadline) { + .duration => false, + .timestamp => true, + } }, ×pec, ×pec))) { + .SUCCESS => return, + .FAULT => unreachable, + .INTR => {}, + .INVAL => return error.UnsupportedClock, + else => |err| return std.posix.unexpectedErrno(err), + } + } +} + +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + + var reset_event: std.Thread.ResetEvent = .{}; + + for (futures, 0..) |future, i| { + const closure: *AsyncClosure = @ptrCast(@alignCast(future)); + if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) { + for (futures[0..i]) |cleanup_future| { + const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future)); + if (@atomicRmw(?*std.Thread.ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + cleanup_closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. + } + } + return i; + } + } + + reset_event.wait(); + + var result: ?usize = null; + for (futures, 0..) |future, i| { + const closure: *AsyncClosure = @ptrCast(@alignCast(future)); + if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. + if (result == null) result = i; // In case multiple are ready, return first. + } + } + return result.?; +} diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig index b9d60c3568..e836665d70 100644 --- a/lib/std/Thread/Pool.zig +++ b/lib/std/Thread/Pool.zig @@ -1,34 +1,27 @@ -const builtin = @import("builtin"); const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const WaitGroup = @import("WaitGroup.zig"); -const Io = std.Io; +const builtin = @import("builtin"); const Pool = @This(); +const WaitGroup = @import("WaitGroup.zig"); -/// Must be a thread-safe allocator. -allocator: std.mem.Allocator, mutex: std.Thread.Mutex = .{}, cond: std.Thread.Condition = .{}, run_queue: std.SinglyLinkedList = .{}, is_running: bool = true, -threads: std.ArrayListUnmanaged(std.Thread), +allocator: std.mem.Allocator, +threads: if (builtin.single_threaded) [0]std.Thread else []std.Thread, ids: if (builtin.single_threaded) struct { inline fn deinit(_: @This(), _: std.mem.Allocator) void {} fn getIndex(_: @This(), _: std.Thread.Id) usize { return 0; } } else std.AutoArrayHashMapUnmanaged(std.Thread.Id, void), -stack_size: usize, -threadlocal var current_closure: ?*AsyncClosure = null; - -pub const Runnable = struct { +const Runnable = struct { runFn: RunProto, node: std.SinglyLinkedList.Node = .{}, }; -pub const RunProto = *const fn (*Runnable, id: ?usize) void; +const RunProto = *const fn (*Runnable, id: ?usize) void; pub const Options = struct { allocator: std.mem.Allocator, @@ -38,36 +31,48 @@ pub const Options = struct { }; pub fn init(pool: *Pool, options: Options) !void { - const gpa = options.allocator; - const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1); - const threads = try gpa.alloc(std.Thread, thread_count); - errdefer gpa.free(threads); + const allocator = options.allocator; pool.* = .{ - .allocator = gpa, - .threads = .initBuffer(threads), + .allocator = allocator, + .threads = if (builtin.single_threaded) .{} else &.{}, .ids = .{}, - .stack_size = options.stack_size, }; - if (builtin.single_threaded) return; + if (builtin.single_threaded) { + return; + } + const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1); if (options.track_ids) { - try pool.ids.ensureTotalCapacity(gpa, 1 + thread_count); + try pool.ids.ensureTotalCapacity(allocator, 1 + thread_count); pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {}); } + + // kill and join any threads we spawned and free memory on error. + pool.threads = try allocator.alloc(std.Thread, thread_count); + var spawned: usize = 0; + errdefer pool.join(spawned); + + for (pool.threads) |*thread| { + thread.* = try std.Thread.spawn(.{ + .stack_size = options.stack_size, + .allocator = allocator, + }, worker, .{pool}); + spawned += 1; + } } pub fn deinit(pool: *Pool) void { - const gpa = pool.allocator; - pool.join(); - pool.threads.deinit(gpa); - pool.ids.deinit(gpa); + pool.join(pool.threads.len); // kill and join all threads. + pool.ids.deinit(pool.allocator); pool.* = undefined; } -fn join(pool: *Pool) void { - if (builtin.single_threaded) return; +fn join(pool: *Pool, spawned: usize) void { + if (builtin.single_threaded) { + return; + } { pool.mutex.lock(); @@ -80,7 +85,11 @@ fn join(pool: *Pool) void { // wake up any sleeping threads (this can be done outside the mutex) // then wait for all the threads we know are spawned to complete. pool.cond.broadcast(); - for (pool.threads.items) |thread| thread.join(); + for (pool.threads[0..spawned]) |thread| { + thread.join(); + } + + pool.allocator.free(pool.threads); } /// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and @@ -108,38 +117,36 @@ pub fn spawnWg(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); @call(.auto, func, closure.arguments); closure.wait_group.finish(); + + // The thread pool's allocator is protected by the mutex. + const mutex = &closure.pool.mutex; + mutex.lock(); + defer mutex.unlock(); + closure.pool.allocator.destroy(closure); } }; - pool.mutex.lock(); + { + pool.mutex.lock(); - const gpa = pool.allocator; - const closure = gpa.create(Closure) catch { - pool.mutex.unlock(); - @call(.auto, func, args); - wait_group.finish(); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - .wait_group = wait_group, - }; - - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; + const closure = pool.allocator.create(Closure) catch { + pool.mutex.unlock(); + @call(.auto, func, args); + wait_group.finish(); + return; }; + closure.* = .{ + .arguments = args, + .pool = pool, + .wait_group = wait_group, + }; + + pool.run_queue.prepend(&closure.runnable.node); + pool.mutex.unlock(); } - pool.mutex.unlock(); + // Notify waiting threads outside the lock to try and keep the critical section small. pool.cond.signal(); } @@ -172,43 +179,41 @@ pub fn spawnWgId(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, ar const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); @call(.auto, func, .{id.?} ++ closure.arguments); closure.wait_group.finish(); + + // The thread pool's allocator is protected by the mutex. + const mutex = &closure.pool.mutex; + mutex.lock(); + defer mutex.unlock(); + closure.pool.allocator.destroy(closure); } }; - pool.mutex.lock(); + { + pool.mutex.lock(); - const gpa = pool.allocator; - const closure = gpa.create(Closure) catch { - const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId()); - pool.mutex.unlock(); - @call(.auto, func, .{id.?} ++ args); - wait_group.finish(); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - .wait_group = wait_group, - }; - - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; + const closure = pool.allocator.create(Closure) catch { + const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId()); + pool.mutex.unlock(); + @call(.auto, func, .{id.?} ++ args); + wait_group.finish(); + return; }; + closure.* = .{ + .arguments = args, + .pool = pool, + .wait_group = wait_group, + }; + + pool.run_queue.prepend(&closure.runnable.node); + pool.mutex.unlock(); } - pool.mutex.unlock(); + // Notify waiting threads outside the lock to try and keep the critical section small. pool.cond.signal(); } -pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) void { +pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) !void { if (builtin.single_threaded) { @call(.auto, func, args); return; @@ -223,36 +228,30 @@ pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) void { fn runFn(runnable: *Runnable, _: ?usize) void { const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); @call(.auto, func, closure.arguments); + + // The thread pool's allocator is protected by the mutex. + const mutex = &closure.pool.mutex; + mutex.lock(); + defer mutex.unlock(); + closure.pool.allocator.destroy(closure); } }; - pool.mutex.lock(); + { + pool.mutex.lock(); + defer pool.mutex.unlock(); - const gpa = pool.allocator; - const closure = gpa.create(Closure) catch { - pool.mutex.unlock(); - @call(.auto, func, args); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - }; - - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; + const closure = try pool.allocator.create(Closure); + closure.* = .{ + .arguments = args, + .pool = pool, }; + + pool.run_queue.prepend(&closure.runnable.node); } - pool.mutex.unlock(); + // Notify waiting threads outside the lock to try and keep the critical section small. pool.cond.signal(); } @@ -271,7 +270,7 @@ test spawn { .allocator = std.testing.allocator, }); defer pool.deinit(); - pool.spawn(TestFn.checkRun, .{&completed}); + try pool.spawn(TestFn.checkRun, .{&completed}); } try std.testing.expectEqual(true, completed); @@ -323,530 +322,5 @@ pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void { } pub fn getIdCount(pool: *Pool) usize { - return @intCast(1 + pool.threads.items.len); -} - -pub fn io(pool: *Pool) Io { - return .{ - .userdata = pool, - .vtable = &.{ - .async = async, - .await = await, - .go = go, - .cancel = cancel, - .cancelRequested = cancelRequested, - .select = select, - - .mutexLock = mutexLock, - .mutexUnlock = mutexUnlock, - - .conditionWait = conditionWait, - .conditionWake = conditionWake, - - .createFile = createFile, - .openFile = openFile, - .closeFile = closeFile, - .pread = pread, - .pwrite = pwrite, - - .now = now, - .sleep = sleep, - }, - }; -} - -const AsyncClosure = struct { - func: *const fn (context: *anyopaque, result: *anyopaque) void, - runnable: Runnable = .{ .runFn = runFn }, - reset_event: std.Thread.ResetEvent, - select_condition: ?*std.Thread.ResetEvent, - cancel_tid: std.Thread.Id, - context_offset: usize, - result_offset: usize, - - const done_reset_event: *std.Thread.ResetEvent = @ptrFromInt(@alignOf(std.Thread.ResetEvent)); - - const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { - .int => |int_info| switch (int_info.signedness) { - .signed => -1, - .unsigned => std.math.maxInt(std.Thread.Id), - }, - .pointer => @ptrFromInt(std.math.maxInt(usize)), - else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), - }; - - fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { - const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable)); - const tid = std.Thread.getCurrentId(); - if (@cmpxchgStrong( - std.Thread.Id, - &closure.cancel_tid, - 0, - tid, - .acq_rel, - .acquire, - )) |cancel_tid| { - assert(cancel_tid == canceling_tid); - return; - } - current_closure = closure; - closure.func(closure.contextPointer(), closure.resultPointer()); - current_closure = null; - if (@cmpxchgStrong( - std.Thread.Id, - &closure.cancel_tid, - tid, - 0, - .acq_rel, - .acquire, - )) |cancel_tid| assert(cancel_tid == canceling_tid); - - if (@atomicRmw( - ?*std.Thread.ResetEvent, - &closure.select_condition, - .Xchg, - done_reset_event, - .release, - )) |select_reset| { - assert(select_reset != done_reset_event); - select_reset.set(); - } - closure.reset_event.set(); - } - - fn contextOffset(context_alignment: std.mem.Alignment) usize { - return context_alignment.forward(@sizeOf(AsyncClosure)); - } - - fn resultOffset( - context_alignment: std.mem.Alignment, - context_len: usize, - result_alignment: std.mem.Alignment, - ) usize { - return result_alignment.forward(contextOffset(context_alignment) + context_len); - } - - fn resultPointer(closure: *AsyncClosure) [*]u8 { - const base: [*]u8 = @ptrCast(closure); - return base + closure.result_offset; - } - - fn contextPointer(closure: *AsyncClosure) [*]u8 { - const base: [*]u8 = @ptrCast(closure); - return base + closure.context_offset; - } - - fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void { - closure.reset_event.wait(); - const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); - @memcpy(result, closure.resultPointer()[0..result.len]); - gpa.free(base[0 .. closure.result_offset + result.len]); - } -}; - -fn async( - userdata: ?*anyopaque, - result: []u8, - result_alignment: std.mem.Alignment, - context: []const u8, - context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) ?*Io.AnyFuture { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - pool.mutex.lock(); - - const gpa = pool.allocator; - const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); - const result_offset = result_alignment.forward(context_offset + context.len); - const n = result_offset + result.len; - const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, @alignOf(AsyncClosure), n) catch { - pool.mutex.unlock(); - start(context.ptr, result.ptr); - return null; - })); - closure.* = .{ - .func = start, - .context_offset = context_offset, - .result_offset = result_offset, - .reset_event = .{}, - .cancel_tid = 0, - .select_condition = null, - }; - @memcpy(closure.contextPointer()[0..context.len], context); - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; - }; - } - - pool.mutex.unlock(); - pool.cond.signal(); - - return @ptrCast(closure); -} - -const DetachedClosure = struct { - pool: *Pool, - func: *const fn (context: *anyopaque) void, - run_node: std.Thread.Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, - context_alignment: std.mem.Alignment, - context_len: usize, - - fn runFn(runnable: *std.Thread.Pool.Runnable, _: ?usize) void { - const run_node: *std.Thread.Pool.RunQueue.Node = @fieldParentPtr("data", runnable); - const closure: *DetachedClosure = @alignCast(@fieldParentPtr("run_node", run_node)); - closure.func(closure.contextPointer()); - const gpa = closure.pool.allocator; - const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure); - gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]); - } - - fn contextOffset(context_alignment: std.mem.Alignment) usize { - return context_alignment.forward(@sizeOf(DetachedClosure)); - } - - fn contextEnd(context_alignment: std.mem.Alignment, context_len: usize) usize { - return contextOffset(context_alignment) + context_len; - } - - fn contextPointer(closure: *DetachedClosure) [*]u8 { - const base: [*]u8 = @ptrCast(closure); - return base + contextOffset(closure.context_alignment); - } -}; - -fn go( - userdata: ?*anyopaque, - context: []const u8, - context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) void, -) void { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - pool.mutex.lock(); - - const gpa = pool.allocator; - const n = DetachedClosure.contextEnd(context_alignment, context.len); - const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, @alignOf(DetachedClosure), n) catch { - pool.mutex.unlock(); - start(context.ptr); - return; - })); - closure.* = .{ - .pool = pool, - .func = start, - .context_alignment = context_alignment, - .context_len = context.len, - }; - @memcpy(closure.contextPointer()[0..context.len], context); - pool.run_queue.prepend(&closure.run_node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; - }; - } - - pool.mutex.unlock(); - pool.cond.signal(); -} - -fn await( - userdata: ?*anyopaque, - any_future: *std.Io.AnyFuture, - result: []u8, - result_alignment: std.mem.Alignment, -) void { - _ = result_alignment; - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - closure.waitAndFree(pool.allocator, result); -} - -fn cancel( - userdata: ?*anyopaque, - any_future: *Io.AnyFuture, - result: []u8, - result_alignment: std.mem.Alignment, -) void { - _ = result_alignment; - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - switch (@atomicRmw( - std.Thread.Id, - &closure.cancel_tid, - .Xchg, - AsyncClosure.canceling_tid, - .acq_rel, - )) { - 0, AsyncClosure.canceling_tid => {}, - else => |cancel_tid| switch (builtin.os.tag) { - .linux => _ = std.os.linux.tgkill( - std.os.linux.getpid(), - @bitCast(cancel_tid), - std.posix.SIG.IO, - ), - else => {}, - }, - } - closure.waitAndFree(pool.allocator, result); -} - -fn cancelRequested(userdata: ?*anyopaque) bool { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - _ = pool; - const closure = current_closure orelse return false; - return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; -} - -fn checkCancel(pool: *Pool) error{Canceled}!void { - if (cancelRequested(pool)) return error.Canceled; -} - -fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { - _ = userdata; - if (prev_state == .contended) { - std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); - } - while (@atomicRmw( - Io.Mutex.State, - &mutex.state, - .Xchg, - .contended, - .acquire, - ) != .unlocked) { - std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); - } -} -fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { - _ = userdata; - _ = prev_state; - if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { - std.Thread.Futex.wake(@ptrCast(&mutex.state), 1); - } -} - -fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - comptime assert(@TypeOf(cond.state) == u64); - const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); - const cond_state = &ints[0]; - const cond_epoch = &ints[1]; - const one_waiter = 1; - const waiter_mask = 0xffff; - const one_signal = 1 << 16; - const signal_mask = 0xffff << 16; - // Observe the epoch, then check the state again to see if we should wake up. - // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock: - // - // - T1: s = LOAD(&state) - // - T2: UPDATE(&s, signal) - // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch) - // - T1: e = LOAD(&epoch) (was reordered after the state load) - // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change) - // - // Acquire barrier to ensure the epoch load happens before the state load. - var epoch = cond_epoch.load(.acquire); - var state = cond_state.fetchAdd(one_waiter, .monotonic); - assert(state & waiter_mask != waiter_mask); - state += one_waiter; - - mutex.unlock(pool.io()); - defer mutex.lock(pool.io()) catch @panic("TODO"); - - var futex_deadline = std.Thread.Futex.Deadline.init(null); - - while (true) { - futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { - error.Timeout => unreachable, - }; - - epoch = cond_epoch.load(.acquire); - state = cond_state.load(.monotonic); - - // Try to wake up by consuming a signal and decremented the waiter we added previously. - // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. - while (state & signal_mask != 0) { - const new_state = state - one_waiter - one_signal; - state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; - } - } -} - -fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - _ = pool; - comptime assert(@TypeOf(cond.state) == u64); - const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); - const cond_state = &ints[0]; - const cond_epoch = &ints[1]; - const one_waiter = 1; - const waiter_mask = 0xffff; - const one_signal = 1 << 16; - const signal_mask = 0xffff << 16; - var state = cond_state.load(.monotonic); - while (true) { - const waiters = (state & waiter_mask) / one_waiter; - const signals = (state & signal_mask) / one_signal; - - // Reserves which waiters to wake up by incrementing the signals count. - // Therefore, the signals count is always less than or equal to the waiters count. - // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters. - const wakeable = waiters - signals; - if (wakeable == 0) { - return; - } - - const to_wake = switch (wake) { - .one => 1, - .all => wakeable, - }; - - // Reserve the amount of waiters to wake by incrementing the signals count. - // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. - const new_state = state + (one_signal * to_wake); - state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { - // Wake up the waiting threads we reserved above by changing the epoch value. - // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it. - // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption. - // - // Release barrier ensures the signal being added to the state happens before the epoch is changed. - // If not, the waiting thread could potentially deadlock from missing both the state and epoch change: - // - // - T2: UPDATE(&epoch, 1) (reordered before the state change) - // - T1: e = LOAD(&epoch) - // - T1: s = LOAD(&state) - // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) - // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) - _ = cond_epoch.fetchAdd(1, .release); - std.Thread.Futex.wake(cond_epoch, to_wake); - return; - }; - } -} - -fn createFile( - userdata: ?*anyopaque, - dir: Io.Dir, - sub_path: []const u8, - flags: Io.File.CreateFlags, -) Io.File.OpenError!Io.File { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - try pool.checkCancel(); - const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; - const fs_file = try fs_dir.createFile(sub_path, flags); - return .{ .handle = fs_file.handle }; -} - -fn openFile( - userdata: ?*anyopaque, - dir: Io.Dir, - sub_path: []const u8, - flags: Io.File.OpenFlags, -) Io.File.OpenError!Io.File { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - try pool.checkCancel(); - const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; - const fs_file = try fs_dir.openFile(sub_path, flags); - return .{ .handle = fs_file.handle }; -} - -fn closeFile(userdata: ?*anyopaque, file: Io.File) void { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - _ = pool; - const fs_file: std.fs.File = .{ .handle = file.handle }; - return fs_file.close(); -} - -fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - try pool.checkCancel(); - const fs_file: std.fs.File = .{ .handle = file.handle }; - return switch (offset) { - -1 => fs_file.read(buffer), - else => fs_file.pread(buffer, @bitCast(offset)), - }; -} - -fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - try pool.checkCancel(); - const fs_file: std.fs.File = .{ .handle = file.handle }; - return switch (offset) { - -1 => fs_file.write(buffer), - else => fs_file.pwrite(buffer, @bitCast(offset)), - }; -} - -fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - try pool.checkCancel(); - const timespec = try std.posix.clock_gettime(clockid); - return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); -} - -fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - const deadline_nanoseconds: i96 = switch (deadline) { - .duration => |duration| duration.nanoseconds, - .timestamp => |timestamp| @intFromEnum(timestamp), - }; - var timespec: std.posix.timespec = .{ - .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), - .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), - }; - while (true) { - try pool.checkCancel(); - switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (deadline) { - .duration => false, - .timestamp => true, - } }, ×pec, ×pec))) { - .SUCCESS => return, - .FAULT => unreachable, - .INTR => {}, - .INVAL => return error.UnsupportedClock, - else => |err| return std.posix.unexpectedErrno(err), - } - } -} - -fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { - const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); - _ = pool; - - var reset_event: std.Thread.ResetEvent = .{}; - - for (futures, 0..) |future, i| { - const closure: *AsyncClosure = @ptrCast(@alignCast(future)); - if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) { - for (futures[0..i]) |cleanup_future| { - const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future)); - if (@atomicRmw(?*std.Thread.ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { - cleanup_closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. - } - } - return i; - } - } - - reset_event.wait(); - - var result: ?usize = null; - for (futures, 0..) |future, i| { - const closure: *AsyncClosure = @ptrCast(@alignCast(future)); - if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { - closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. - if (result == null) result = i; // In case multiple are ready, return first. - } - } - return result.?; + return @intCast(1 + pool.threads.len); } From f5d8492b1f8ab51c5c7157e8de9610067cbaa6f9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 10 Jul 2025 09:20:01 -0700 Subject: [PATCH 040/244] std.Io: rename go to asyncDetached it's a better name because it's more descriptive, not a reference, and hints that it is less common than async --- lib/std/Io.zig | 6 +++--- lib/std/Io/EventLoop.zig | 4 ++-- lib/std/Io/ThreadPool.zig | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index effb0e9383..85644004ff 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -585,7 +585,7 @@ pub const VTable = struct { /// up. This mode does not support results, await, or cancel. /// /// Thread-safe. - go: *const fn ( + asyncDetached: *const fn ( /// Corresponds to `Io.userdata`. userdata: ?*anyopaque, /// Copied and then passed to `start`. @@ -1162,7 +1162,7 @@ pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(functio /// Calls `function` with `args` asynchronously. The resource cleans itself up /// when the function returns. Does not support await, cancel, or a return value. -pub fn go(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { +pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); const TypeErased = struct { fn start(context: *const anyopaque) void { @@ -1170,7 +1170,7 @@ pub fn go(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function)) @call(.auto, function, args_casted.*); } }; - io.vtable.go(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + io.vtable.asyncDetached(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); } pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 8f9cfd658d..bc760fcfd4 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -140,7 +140,7 @@ pub fn io(el: *EventLoop) Io { .vtable = &.{ .async = async, .await = await, - .go = go, + .asyncDetached = asyncDetached, .select = select, .cancel = cancel, .cancelRequested = cancelRequested, @@ -776,7 +776,7 @@ const DetachedClosure = struct { } }; -fn go( +fn asyncDetached( userdata: ?*anyopaque, context: []const u8, context_alignment: std.mem.Alignment, diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index f356a87054..92f97c37fd 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -332,7 +332,7 @@ pub fn io(pool: *Pool) Io { .vtable = &.{ .async = async, .await = await, - .go = go, + .asyncDetached = asyncDetached, .cancel = cancel, .cancelRequested = cancelRequested, .select = select, @@ -521,7 +521,7 @@ const DetachedClosure = struct { } }; -fn go( +fn asyncDetached( userdata: ?*anyopaque, context: []const u8, context_alignment: std.mem.Alignment, From 30be75ca4052dc027b21ec987444d16f2c22886d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 10 Jul 2025 16:44:05 -0700 Subject: [PATCH 041/244] std.Io.ThreadPool: fix asyncDetached --- lib/std/Io/ThreadPool.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index 92f97c37fd..f356187748 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -494,13 +494,12 @@ fn async( const DetachedClosure = struct { pool: *Pool, func: *const fn (context: *anyopaque) void, - run_node: Pool.RunQueue.Node = .{ .data = .{ .runFn = runFn } }, + runnable: Runnable = .{ .runFn = runFn }, context_alignment: std.mem.Alignment, context_len: usize, fn runFn(runnable: *Pool.Runnable, _: ?usize) void { - const run_node: *Pool.RunQueue.Node = @fieldParentPtr("data", runnable); - const closure: *DetachedClosure = @alignCast(@fieldParentPtr("run_node", run_node)); + const closure: *DetachedClosure = @alignCast(@fieldParentPtr("runnable", runnable)); closure.func(closure.contextPointer()); const gpa = closure.pool.allocator; const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure); @@ -544,7 +543,7 @@ fn asyncDetached( .context_len = context.len, }; @memcpy(closure.contextPointer()[0..context.len], context); - pool.run_queue.prepend(&closure.run_node); + pool.run_queue.prepend(&closure.runnable.node); if (pool.threads.items.len < pool.threads.capacity) { pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ From ec3e4c00c3a0b480cb9b6cfe6740052ce1084a94 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 11 Jul 2025 17:51:55 -0700 Subject: [PATCH 042/244] std.Io.EventLoop: add aarch64 support --- lib/std/Io/EventLoop.zig | 214 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 16 deletions(-) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index bc760fcfd4..c30736a885 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -192,14 +192,22 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { }; const main_thread = &el.threads.allocated[0]; Thread.self = main_thread; - const idle_stack_end: [*]usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); + const idle_stack_end: [*]align(16) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; main_thread.* = .{ .thread = undefined, - .idle_context = .{ - .rsp = @intFromPtr(idle_stack_end - 1), - .rbp = 0, - .rip = @intFromPtr(&mainIdleEntry), + .idle_context = switch (builtin.cpu.arch) { + .aarch64 => .{ + .sp = @intFromPtr(idle_stack_end), + .fp = 0, + .pc = @intFromPtr(&mainIdleEntry), + }, + .x86_64 => .{ + .rsp = @intFromPtr(idle_stack_end - 1), + .rbp = 0, + .rip = @intFromPtr(&mainIdleEntry), + }, + else => @compileError("unimplemented architecture"), }, .current_context = &main_fiber.context, .ready_queue = null, @@ -606,6 +614,11 @@ const SwitchMessage = struct { }; const Context = switch (builtin.cpu.arch) { + .aarch64 => extern struct { + sp: u64, + fp: u64, + pc: u64, + }, .x86_64 => extern struct { rsp: u64, rbp: u64, @@ -616,6 +629,102 @@ const Context = switch (builtin.cpu.arch) { inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { return @fieldParentPtr("contexts", switch (builtin.cpu.arch) { + .aarch64 => asm volatile ( + \\ ldp x0, x2, [x1] + \\ ldr x3, [x2, #16] + \\ mov x4, sp + \\ stp x4, fp, [x0] + \\ adr x5, 0f + \\ ldp x4, fp, [x2] + \\ str x5, [x0, #16] + \\ mov sp, x4 + \\ br x3 + \\0: + : [received_message] "={x1}" (-> *const @FieldType(SwitchMessage, "contexts")), + : [message_to_send] "{x1}" (&message.contexts), + : .{ + .x1 = true, + .x2 = true, + .x3 = true, + .x4 = true, + .x5 = true, + .x6 = true, + .x7 = true, + .x8 = true, + .x9 = true, + .x10 = true, + .x11 = true, + .x12 = true, + .x13 = true, + .x14 = true, + .x15 = true, + .x16 = true, + .x17 = true, + .x18 = true, + .x19 = true, + .x20 = true, + .x21 = true, + .x22 = true, + .x23 = true, + .x24 = true, + .x25 = true, + .x26 = true, + .x27 = true, + .x28 = true, + .x30 = true, + .z0 = true, + .z1 = true, + .z2 = true, + .z3 = true, + .z4 = true, + .z5 = true, + .z6 = true, + .z7 = true, + .z8 = true, + .z9 = true, + .z10 = true, + .z11 = true, + .z12 = true, + .z13 = true, + .z14 = true, + .z15 = true, + .z16 = true, + .z17 = true, + .z18 = true, + .z19 = true, + .z20 = true, + .z21 = true, + .z22 = true, + .z23 = true, + .z24 = true, + .z25 = true, + .z26 = true, + .z27 = true, + .z28 = true, + .z29 = true, + .z30 = true, + .z31 = true, + .p0 = true, + .p1 = true, + .p2 = true, + .p3 = true, + .p4 = true, + .p5 = true, + .p6 = true, + .p7 = true, + .p8 = true, + .p9 = true, + .p10 = true, + .p11 = true, + .p12 = true, + .p13 = true, + .p14 = true, + .p15 = true, + .fpcr = true, + .fpsr = true, + .ffr = true, + .memory = true, + }), .x86_64 => asm volatile ( \\ movq 0(%%rsi), %%rax \\ movq 8(%%rsi), %%rcx @@ -629,15 +738,67 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { \\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" - ), + : .{ + .rax = true, + .rcx = true, + .rdx = true, + .rbx = true, + .rsi = true, + .r8 = true, + .r9 = true, + .r10 = true, + .r11 = true, + .r12 = true, + .r13 = true, + .r14 = true, + .r15 = true, + .mm0 = true, + .mm1 = true, + .mm2 = true, + .mm3 = true, + .mm4 = true, + .mm5 = true, + .mm6 = true, + .mm7 = true, + .zmm0 = true, + .zmm1 = true, + .zmm2 = true, + .zmm3 = true, + .zmm4 = true, + .zmm5 = true, + .zmm6 = true, + .zmm7 = true, + .zmm8 = true, + .zmm9 = true, + .zmm10 = true, + .zmm11 = true, + .zmm12 = true, + .zmm13 = true, + .zmm14 = true, + .zmm15 = true, + .zmm16 = true, + .zmm17 = true, + .zmm18 = true, + .zmm19 = true, + .zmm20 = true, + .zmm21 = true, + .zmm22 = true, + .zmm23 = true, + .zmm24 = true, + .zmm25 = true, + .zmm26 = true, + .zmm27 = true, + .zmm28 = true, + .zmm29 = true, + .zmm30 = true, + .zmm31 = true, + .fpsr = true, + .fpcr = true, + .mxcsr = true, + .rflags = true, + .dirflag = true, + .memory = true, + }), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), }); } @@ -650,6 +811,12 @@ fn mainIdleEntry() callconv(.naked) void { : : [mainIdle] "X" (&mainIdle), ), + .aarch64 => asm volatile ( + \\ ldr x0, [sp, #-8] + \\ b %[mainIdle] + : + : [mainIdle] "X" (&mainIdle), + ), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), } } @@ -660,6 +827,11 @@ fn fiberEntry() callconv(.naked) void { \\ leaq 8(%%rsp), %%rdi \\ jmpq *(%%rsp) ), + .aarch64 => asm volatile ( + \\ mov x0, sp + \\ ldr x2, [sp, #-8] + \\ br x2 + ), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), } } @@ -718,7 +890,7 @@ fn async( std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = .fromFiber(fiber); - const stack_end: [*]usize = @alignCast(@ptrCast(closure)); + const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); (stack_end - 1)[0..1].* = .{@intFromPtr(&AsyncClosure.call)}; fiber.* = .{ .required_align = {}, @@ -728,6 +900,11 @@ fn async( .rbp = 0, .rip = @intFromPtr(&fiberEntry), }, + .aarch64 => .{ + .sp = @intFromPtr(stack_end), + .fp = 0, + .pc = @intFromPtr(&fiberEntry), + }, else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), }, .awaiter = null, @@ -796,7 +973,7 @@ fn asyncDetached( const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward( @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, ) - @sizeOf(DetachedClosure)); - const stack_end: [*]usize = @alignCast(@ptrCast(closure)); + const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); (stack_end - 1)[0..1].* = .{@intFromPtr(&DetachedClosure.call)}; fiber.* = .{ .required_align = {}, @@ -806,6 +983,11 @@ fn asyncDetached( .rbp = 0, .rip = @intFromPtr(&fiberEntry), }, + .aarch64 => .{ + .sp = @intFromPtr(stack_end), + .fp = 0, + .pc = @intFromPtr(&fiberEntry), + }, else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), }, .awaiter = null, From f76259772438a7fa4fafa59082cd5fe05dfbed44 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 17 Jul 2025 20:26:07 -0700 Subject: [PATCH 043/244] std.Io: add asyncConcurrent and asyncParallel --- lib/std/Io.zig | 121 +++++++++- lib/std/Io/EventLoop.zig | 44 +++- lib/std/Io/ThreadPool.zig | 474 ++++++++++++++------------------------ lib/std/Thread.zig | 2 + 4 files changed, 331 insertions(+), 310 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 85644004ff..df95da23cf 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -581,6 +581,32 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*AnyFuture, + /// Returning `null` indicates resource allocation failed. + /// + /// Thread-safe. + asyncConcurrent: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + ) ?*AnyFuture, + /// Returning `null` indicates resource allocation failed. + /// + /// Thread-safe. + asyncParallel: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + ) ?*AnyFuture, /// Executes `start` asynchronously in a manner such that it cleans itself /// up. This mode does not support results, await, or cancel. /// @@ -1138,7 +1164,18 @@ pub fn Queue(Elem: type) type { /// Calls `function` with `args`, such that the return value of the function is /// not guaranteed to be available until `await` is called. -pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { +/// +/// `function` *may* be called immediately, before `async` returns. This has +/// weaker guarantees than `asyncConcurrent` and `asyncParallel`, making it the +/// most portable and reusable among the async family functions. +/// +/// See also: +/// * `asyncDetached` +pub fn async( + io: Io, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; const Args = @TypeOf(args); const TypeErased = struct { @@ -1160,8 +1197,86 @@ pub fn async(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(functio return future; } +/// Calls `function` with `args`, such that the return value of the function is +/// not guaranteed to be available until `await` is called, passing control +/// flow back to the caller while waiting for any `Io` operations. +/// +/// This has a weaker guarantee than `asyncParallel`, making it more portable +/// and reusable, however it has stronger guarantee than `async`, placing +/// restrictions on what kind of `Io` implementations are supported. By calling +/// `async` instead, one allows, for example, stackful single-threaded blocking I/O. +pub fn asyncConcurrent( + io: Io, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) error{OutOfMemory}!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { + const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(context: *const anyopaque, result: *anyopaque) void { + const args_casted: *const Args = @alignCast(@ptrCast(context)); + const result_casted: *Result = @ptrCast(@alignCast(result)); + result_casted.* = @call(.auto, function, args_casted.*); + } + }; + var future: Future(Result) = undefined; + future.any_future = io.vtable.asyncConcurrent( + io.userdata, + @sizeOf(Result), + .of(Result), + @ptrCast((&args)[0..1]), + .of(Args), + TypeErased.start, + ); + return future; +} + +/// Calls `function` with `args`, such that the return value of the function is +/// not guaranteed to be available until `await` is called, while simultaneously +/// passing control flow back to the caller. +/// +/// This has the strongest guarantees of all async family functions, placing +/// the most restrictions on what kind of `Io` implementations are supported. +/// By calling `asyncConcurrent` instead, one allows, for example, +/// stackful single-threaded non-blocking I/O. +/// +/// See also: +/// * `asyncConcurrent` +/// * `async` +pub fn asyncParallel( + io: Io, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), +) error{OutOfMemory}!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { + const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(context: *const anyopaque, result: *anyopaque) void { + const args_casted: *const Args = @alignCast(@ptrCast(context)); + const result_casted: *Result = @ptrCast(@alignCast(result)); + result_casted.* = @call(.auto, function, args_casted.*); + } + }; + var future: Future(Result) = undefined; + future.any_future = io.vtable.asyncConcurrent( + io.userdata, + @ptrCast((&future.result)[0..1]), + .of(Result), + @ptrCast((&args)[0..1]), + .of(Args), + TypeErased.start, + ); + return future; +} + /// Calls `function` with `args` asynchronously. The resource cleans itself up /// when the function returns. Does not support await, cancel, or a return value. +/// +/// `function` *may* be called immediately, before `async` returns. +/// +/// See also: +/// * `async` +/// * `asyncConcurrent` pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); const TypeErased = struct { @@ -1173,6 +1288,10 @@ pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf io.vtable.asyncDetached(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); } +pub fn cancelRequested(io: Io) bool { + return io.vtable.cancelRequested(io.userdata); +} + pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { return io.vtable.now(io.userdata, clockid); } diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index c30736a885..3f070362e5 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -139,6 +139,8 @@ pub fn io(el: *EventLoop) Io { .userdata = el, .vtable = &.{ .async = async, + .asyncConcurrent = asyncConcurrent, + .asyncParallel = asyncParallel, .await = await, .asyncDetached = asyncDetached, .select = select, @@ -877,16 +879,27 @@ fn async( 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 { + return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) orelse { start(context.ptr, result.ptr); return null; }; +} + +fn asyncConcurrent( + userdata: ?*anyopaque, + result_len: usize, + 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 return null; std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = .fromFiber(fiber); @@ -925,6 +938,23 @@ fn async( return @ptrCast(fiber); } +fn asyncParallel( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: Alignment, + context: []const u8, + context_alignment: Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*std.Io.AnyFuture { + _ = userdata; + _ = result_len; + _ = result_alignment; + _ = context; + _ = context_alignment; + _ = start; + @panic("TODO"); +} + const DetachedClosure = struct { event_loop: *EventLoop, fiber: *Fiber, diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index f356187748..c5fc45ad91 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -6,331 +6,88 @@ const WaitGroup = std.Thread.WaitGroup; const Io = std.Io; const Pool = @This(); -/// Must be a thread-safe allocator. -allocator: std.mem.Allocator, +/// Thread-safe. +allocator: Allocator, mutex: std.Thread.Mutex = .{}, cond: std.Thread.Condition = .{}, run_queue: std.SinglyLinkedList = .{}, -is_running: bool = true, +join_requested: bool = false, threads: std.ArrayListUnmanaged(std.Thread), -ids: if (builtin.single_threaded) struct { - inline fn deinit(_: @This(), _: std.mem.Allocator) void {} - fn getIndex(_: @This(), _: std.Thread.Id) usize { - return 0; - } -} else std.AutoArrayHashMapUnmanaged(std.Thread.Id, void), stack_size: usize, +cpu_count: std.Thread.CpuCountError!usize, +parallel_count: usize, threadlocal var current_closure: ?*AsyncClosure = null; pub const Runnable = struct { - runFn: RunProto, + start: Start, node: std.SinglyLinkedList.Node = .{}, + is_parallel: bool, + + pub const Start = *const fn (*Runnable) void; }; -pub const RunProto = *const fn (*Runnable, id: ?usize) void; +pub const InitError = std.Thread.CpuCountError || Allocator.Error; -pub const Options = struct { - allocator: std.mem.Allocator, - n_jobs: ?usize = null, - track_ids: bool = false, - stack_size: usize = std.Thread.SpawnConfig.default_stack_size, -}; - -pub fn init(pool: *Pool, options: Options) !void { - const gpa = options.allocator; - const thread_count = options.n_jobs orelse @max(1, std.Thread.getCpuCount() catch 1); - const threads = try gpa.alloc(std.Thread, thread_count); - errdefer gpa.free(threads); - - pool.* = .{ +pub fn init(gpa: Allocator) Pool { + var pool: Pool = .{ .allocator = gpa, - .threads = .initBuffer(threads), - .ids = .{}, - .stack_size = options.stack_size, + .threads = .empty, + .stack_size = std.Thread.SpawnConfig.default_stack_size, + .cpu_count = std.Thread.getCpuCount(), + .parallel_count = 0, }; - - if (builtin.single_threaded) return; - - if (options.track_ids) { - try pool.ids.ensureTotalCapacity(gpa, 1 + thread_count); - pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {}); - } + if (pool.cpu_count) |n| { + pool.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {}; + } else |_| {} + return pool; } pub fn deinit(pool: *Pool) void { const gpa = pool.allocator; pool.join(); pool.threads.deinit(gpa); - pool.ids.deinit(gpa); pool.* = undefined; } fn join(pool: *Pool) void { if (builtin.single_threaded) return; - { pool.mutex.lock(); defer pool.mutex.unlock(); - - // ensure future worker threads exit the dequeue loop - pool.is_running = false; + pool.join_requested = true; } - - // wake up any sleeping threads (this can be done outside the mutex) - // then wait for all the threads we know are spawned to complete. pool.cond.broadcast(); for (pool.threads.items) |thread| thread.join(); } -/// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and -/// `WaitGroup.finish` after it returns. -/// -/// In the case that queuing the function call fails to allocate memory, or the -/// target is single-threaded, the function is called directly. -pub fn spawnWg(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args: anytype) void { - wait_group.start(); - - if (builtin.single_threaded) { - @call(.auto, func, args); - wait_group.finish(); - return; - } - - const Args = @TypeOf(args); - const Closure = struct { - arguments: Args, - pool: *Pool, - runnable: Runnable = .{ .runFn = runFn }, - wait_group: *WaitGroup, - - fn runFn(runnable: *Runnable, _: ?usize) void { - const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); - @call(.auto, func, closure.arguments); - closure.wait_group.finish(); - closure.pool.allocator.destroy(closure); - } - }; - - pool.mutex.lock(); - - const gpa = pool.allocator; - const closure = gpa.create(Closure) catch { - pool.mutex.unlock(); - @call(.auto, func, args); - wait_group.finish(); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - .wait_group = wait_group, - }; - - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; - }; - } - - pool.mutex.unlock(); - pool.cond.signal(); -} - -/// Runs `func` in the thread pool, calling `WaitGroup.start` beforehand, and -/// `WaitGroup.finish` after it returns. -/// -/// The first argument passed to `func` is a dense `usize` thread id, the rest -/// of the arguments are passed from `args`. Requires the pool to have been -/// initialized with `.track_ids = true`. -/// -/// In the case that queuing the function call fails to allocate memory, or the -/// target is single-threaded, the function is called directly. -pub fn spawnWgId(pool: *Pool, wait_group: *WaitGroup, comptime func: anytype, args: anytype) void { - wait_group.start(); - - if (builtin.single_threaded) { - @call(.auto, func, .{0} ++ args); - wait_group.finish(); - return; - } - - const Args = @TypeOf(args); - const Closure = struct { - arguments: Args, - pool: *Pool, - runnable: Runnable = .{ .runFn = runFn }, - wait_group: *WaitGroup, - - fn runFn(runnable: *Runnable, id: ?usize) void { - const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); - @call(.auto, func, .{id.?} ++ closure.arguments); - closure.wait_group.finish(); - closure.pool.allocator.destroy(closure); - } - }; - - pool.mutex.lock(); - - const gpa = pool.allocator; - const closure = gpa.create(Closure) catch { - const id: ?usize = pool.ids.getIndex(std.Thread.getCurrentId()); - pool.mutex.unlock(); - @call(.auto, func, .{id.?} ++ args); - wait_group.finish(); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - .wait_group = wait_group, - }; - - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; - }; - } - - pool.mutex.unlock(); - pool.cond.signal(); -} - -pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) void { - if (builtin.single_threaded) { - @call(.auto, func, args); - return; - } - - const Args = @TypeOf(args); - const Closure = struct { - arguments: Args, - pool: *Pool, - runnable: Runnable = .{ .runFn = runFn }, - - fn runFn(runnable: *Runnable, _: ?usize) void { - const closure: *@This() = @alignCast(@fieldParentPtr("runnable", runnable)); - @call(.auto, func, closure.arguments); - closure.pool.allocator.destroy(closure); - } - }; - - pool.mutex.lock(); - - const gpa = pool.allocator; - const closure = gpa.create(Closure) catch { - pool.mutex.unlock(); - @call(.auto, func, args); - return; - }; - closure.* = .{ - .arguments = args, - .pool = pool, - }; - - pool.run_queue.prepend(&closure.runnable.node); - - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; - }; - } - - pool.mutex.unlock(); - pool.cond.signal(); -} - -test spawn { - const TestFn = struct { - fn checkRun(completed: *bool) void { - completed.* = true; - } - }; - - var completed: bool = false; - - { - var pool: Pool = undefined; - try pool.init(.{ - .allocator = std.testing.allocator, - }); - defer pool.deinit(); - pool.spawn(TestFn.checkRun, .{&completed}); - } - - try std.testing.expectEqual(true, completed); -} - fn worker(pool: *Pool) void { pool.mutex.lock(); defer pool.mutex.unlock(); - const id: ?usize = if (pool.ids.count() > 0) @intCast(pool.ids.count()) else null; - if (id) |_| pool.ids.putAssumeCapacityNoClobber(std.Thread.getCurrentId(), {}); - while (true) { while (pool.run_queue.popFirst()) |run_node| { - // Temporarily unlock the mutex in order to execute the run_node - pool.mutex.unlock(); - defer pool.mutex.lock(); - - const runnable: *Runnable = @fieldParentPtr("node", run_node); - runnable.runFn(runnable, id); - } - - // Stop executing instead of waiting if the thread pool is no longer running. - if (pool.is_running) { - pool.cond.wait(&pool.mutex); - } else { - break; - } - } -} - -pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void { - var id: ?usize = null; - - while (!wait_group.isDone()) { - pool.mutex.lock(); - if (pool.run_queue.popFirst()) |run_node| { - id = id orelse pool.ids.getIndex(std.Thread.getCurrentId()); pool.mutex.unlock(); const runnable: *Runnable = @fieldParentPtr("node", run_node); - runnable.runFn(runnable, id); - continue; + runnable.start(runnable); + pool.mutex.lock(); + if (runnable.is_parallel) { + // TODO also pop thread and join sometimes + pool.parallel_count -= 1; + } } - - pool.mutex.unlock(); - wait_group.wait(); - return; + if (pool.join_requested) break; + pool.cond.wait(&pool.mutex); } } -pub fn getIdCount(pool: *Pool) usize { - return @intCast(1 + pool.threads.items.len); -} - pub fn io(pool: *Pool) Io { return .{ .userdata = pool, .vtable = &.{ .async = async, + .asyncConcurrent = asyncParallel, + .asyncParallel = asyncParallel, .await = await, .asyncDetached = asyncDetached, .cancel = cancel, @@ -357,7 +114,7 @@ pub fn io(pool: *Pool) Io { const AsyncClosure = struct { func: *const fn (context: *anyopaque, result: *anyopaque) void, - runnable: Runnable = .{ .runFn = runFn }, + runnable: Runnable, reset_event: std.Thread.ResetEvent, select_condition: ?*std.Thread.ResetEvent, cancel_tid: std.Thread.Id, @@ -375,7 +132,7 @@ const AsyncClosure = struct { else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), }; - fn runFn(runnable: *Pool.Runnable, _: ?usize) void { + fn start(runnable: *Runnable) void { const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable)); const tid = std.Thread.getCurrentId(); if (@cmpxchgStrong( @@ -387,6 +144,7 @@ const AsyncClosure = struct { .acquire, )) |cancel_tid| { assert(cancel_tid == canceling_tid); + closure.reset_event.set(); return; } current_closure = closure; @@ -438,9 +196,13 @@ const AsyncClosure = struct { fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void { closure.reset_event.wait(); - const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); @memcpy(result, closure.resultPointer()[0..result.len]); - gpa.free(base[0 .. closure.result_offset + result.len]); + free(closure, gpa, result.len); + } + + fn free(closure: *AsyncClosure, gpa: Allocator, result_len: usize) void { + const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); + gpa.free(base[0 .. closure.result_offset + result_len]); } }; @@ -452,18 +214,26 @@ fn async( context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*Io.AnyFuture { + if (builtin.single_threaded) { + start(context.ptr, result.ptr); + return null; + } const pool: *Pool = @alignCast(@ptrCast(userdata)); - pool.mutex.lock(); - + const cpu_count = pool.cpu_count catch { + return asyncParallel(userdata, result.len, result_alignment, context, context_alignment, start) orelse { + start(context.ptr, result.ptr); + return null; + }; + }; const gpa = pool.allocator; const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result.len; const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { - pool.mutex.unlock(); start(context.ptr, result.ptr); return null; })); + closure.* = .{ .func = start, .context_offset = context_offset, @@ -471,37 +241,124 @@ fn async( .reset_event = .{}, .cancel_tid = 0, .select_condition = null, + .runnable = .{ + .start = AsyncClosure.start, + .is_parallel = false, + }, }; + @memcpy(closure.contextPointer()[0..context.len], context); + + pool.mutex.lock(); + + const thread_capacity = cpu_count - 1 + pool.parallel_count; + + pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { + pool.mutex.unlock(); + closure.free(gpa, result.len); + start(context.ptr, result.ptr); + return null; + }; + pool.run_queue.prepend(&closure.runnable.node); - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; + if (pool.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { + if (pool.threads.items.len == 0) { + assert(pool.run_queue.popFirst() == &closure.runnable.node); + pool.mutex.unlock(); + closure.free(gpa, result.len); + start(context.ptr, result.ptr); + return null; + } + // Rely on other workers to do it. + pool.mutex.unlock(); + pool.cond.signal(); + return @ptrCast(closure); }; + pool.threads.appendAssumeCapacity(thread); } pool.mutex.unlock(); pool.cond.signal(); + return @ptrCast(closure); +} +fn asyncParallel( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*Io.AnyFuture { + if (builtin.single_threaded) return null; + + const pool: *Pool = @alignCast(@ptrCast(userdata)); + const cpu_count = pool.cpu_count catch 1; + const gpa = pool.allocator; + const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); + const result_offset = result_alignment.forward(context_offset + context.len); + const n = result_offset + result_len; + const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch return null)); + + closure.* = .{ + .func = start, + .context_offset = context_offset, + .result_offset = result_offset, + .reset_event = .{}, + .cancel_tid = 0, + .select_condition = null, + .runnable = .{ + .start = AsyncClosure.start, + .is_parallel = true, + }, + }; + @memcpy(closure.contextPointer()[0..context.len], context); + + pool.mutex.lock(); + + pool.parallel_count += 1; + const thread_capacity = cpu_count - 1 + pool.parallel_count; + + pool.threads.ensureTotalCapacity(gpa, thread_capacity) catch { + pool.mutex.unlock(); + closure.free(gpa, result_len); + return null; + }; + + pool.run_queue.prepend(&closure.runnable.node); + + if (pool.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { + assert(pool.run_queue.popFirst() == &closure.runnable.node); + pool.mutex.unlock(); + closure.free(gpa, result_len); + return null; + }; + pool.threads.appendAssumeCapacity(thread); + } + + pool.mutex.unlock(); + pool.cond.signal(); return @ptrCast(closure); } const DetachedClosure = struct { pool: *Pool, func: *const fn (context: *anyopaque) void, - runnable: Runnable = .{ .runFn = runFn }, + runnable: Runnable, context_alignment: std.mem.Alignment, context_len: usize, - fn runFn(runnable: *Pool.Runnable, _: ?usize) void { + fn start(runnable: *Runnable) void { const closure: *DetachedClosure = @alignCast(@fieldParentPtr("runnable", runnable)); closure.func(closure.contextPointer()); const gpa = closure.pool.allocator; + free(closure, gpa); + } + + fn free(closure: *DetachedClosure, gpa: Allocator) void { const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure); gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]); } @@ -526,33 +383,46 @@ fn asyncDetached( context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque) void, ) void { + if (builtin.single_threaded) return start(context.ptr); const pool: *Pool = @alignCast(@ptrCast(userdata)); - pool.mutex.lock(); - + const cpu_count = pool.cpu_count catch 1; const gpa = pool.allocator; const n = DetachedClosure.contextEnd(context_alignment, context.len); const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch { - pool.mutex.unlock(); - start(context.ptr); - return; + return start(context.ptr); })); closure.* = .{ .pool = pool, .func = start, .context_alignment = context_alignment, .context_len = context.len, + .runnable = .{ + .start = DetachedClosure.start, + .is_parallel = false, + }, }; @memcpy(closure.contextPointer()[0..context.len], context); + + pool.mutex.lock(); + + const thread_capacity = cpu_count - 1 + pool.parallel_count; + + pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { + pool.mutex.unlock(); + closure.free(gpa); + return start(context.ptr); + }; + pool.run_queue.prepend(&closure.runnable.node); - if (pool.threads.items.len < pool.threads.capacity) { - pool.threads.addOneAssumeCapacity().* = std.Thread.spawn(.{ - .stack_size = pool.stack_size, - .allocator = gpa, - }, worker, .{pool}) catch t: { - pool.threads.items.len -= 1; - break :t undefined; + if (pool.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { + assert(pool.run_queue.popFirst() == &closure.runnable.node); + pool.mutex.unlock(); + closure.free(gpa); + return start(context.ptr); }; + pool.threads.appendAssumeCapacity(thread); } pool.mutex.unlock(); diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index f587b15f86..3d78596f84 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -385,6 +385,8 @@ pub const CpuCountError = error{ }; /// Returns the platforms view on the number of logical CPU cores available. +/// +/// Returned value guaranteed to be >= 1. pub fn getCpuCount() CpuCountError!usize { return try Impl.getCpuCount(); } From 7ead86e339c0dca82473cab9bd22b682ac61817d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 17 Jul 2025 20:53:39 -0700 Subject: [PATCH 044/244] std.Io: fix error handling and asyncParallel docs --- lib/std/Io.zig | 20 ++++++++++---------- lib/std/Io/EventLoop.zig | 8 ++++---- lib/std/Io/ThreadPool.zig | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index df95da23cf..ee17d6b7ac 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -593,7 +593,7 @@ pub const VTable = struct { context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, - ) ?*AnyFuture, + ) error{OutOfMemory}!*AnyFuture, /// Returning `null` indicates resource allocation failed. /// /// Thread-safe. @@ -606,7 +606,7 @@ pub const VTable = struct { context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, - ) ?*AnyFuture, + ) error{OutOfMemory}!*AnyFuture, /// Executes `start` asynchronously in a manner such that it cleans itself /// up. This mode does not support results, await, or cancel. /// @@ -1220,7 +1220,7 @@ pub fn asyncConcurrent( } }; var future: Future(Result) = undefined; - future.any_future = io.vtable.asyncConcurrent( + future.any_future = try io.vtable.asyncConcurrent( io.userdata, @sizeOf(Result), .of(Result), @@ -1231,14 +1231,14 @@ pub fn asyncConcurrent( return future; } -/// Calls `function` with `args`, such that the return value of the function is -/// not guaranteed to be available until `await` is called, while simultaneously -/// passing control flow back to the caller. +/// Simultaneously calls `function` with `args` while passing control flow back +/// to the caller. The return value of the function is not guaranteed to be +/// available until `await` is called. /// /// This has the strongest guarantees of all async family functions, placing /// the most restrictions on what kind of `Io` implementations are supported. -/// By calling `asyncConcurrent` instead, one allows, for example, -/// stackful single-threaded non-blocking I/O. +/// By calling `asyncConcurrent` instead, one allows, for example, stackful +/// single-threaded non-blocking I/O. /// /// See also: /// * `asyncConcurrent` @@ -1258,9 +1258,9 @@ pub fn asyncParallel( } }; var future: Future(Result) = undefined; - future.any_future = io.vtable.asyncConcurrent( + future.any_future = try io.vtable.asyncParallel( io.userdata, - @ptrCast((&future.result)[0..1]), + @sizeOf(Result), .of(Result), @ptrCast((&args)[0..1]), .of(Args), diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 3f070362e5..8b8de73b23 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -879,7 +879,7 @@ fn async( context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*std.Io.AnyFuture { - return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) orelse { + return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; }; @@ -892,14 +892,14 @@ fn asyncConcurrent( context: []const u8, context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) ?*std.Io.AnyFuture { +) error{OutOfMemory}!*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 return null; + const fiber = try Fiber.allocate(event_loop); std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = .fromFiber(fiber); @@ -945,7 +945,7 @@ fn asyncParallel( context: []const u8, context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) ?*std.Io.AnyFuture { +) error{OutOfMemory}!*std.Io.AnyFuture { _ = userdata; _ = result_len; _ = result_alignment; diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index c5fc45ad91..2f10c6a8fa 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -220,7 +220,7 @@ fn async( } const pool: *Pool = @alignCast(@ptrCast(userdata)); const cpu_count = pool.cpu_count catch { - return asyncParallel(userdata, result.len, result_alignment, context, context_alignment, start) orelse { + return asyncParallel(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; }; @@ -291,8 +291,8 @@ fn asyncParallel( context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) ?*Io.AnyFuture { - if (builtin.single_threaded) return null; +) error{OutOfMemory}!*Io.AnyFuture { + if (builtin.single_threaded) unreachable; const pool: *Pool = @alignCast(@ptrCast(userdata)); const cpu_count = pool.cpu_count catch 1; @@ -300,7 +300,7 @@ fn asyncParallel( const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result_len; - const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch return null)); + const closure: *AsyncClosure = @alignCast(@ptrCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), n))); closure.* = .{ .func = start, @@ -324,7 +324,7 @@ fn asyncParallel( pool.threads.ensureTotalCapacity(gpa, thread_capacity) catch { pool.mutex.unlock(); closure.free(gpa, result_len); - return null; + return error.OutOfMemory; }; pool.run_queue.prepend(&closure.runnable.node); @@ -334,7 +334,7 @@ fn asyncParallel( assert(pool.run_queue.popFirst() == &closure.runnable.node); pool.mutex.unlock(); closure.free(gpa, result_len); - return null; + return error.OutOfMemory; }; pool.threads.appendAssumeCapacity(thread); } From 84d60404becbb66da3b025ab510e945b1bd8f5ff Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 18 Jul 2025 09:41:48 -0700 Subject: [PATCH 045/244] std.Io: delete asyncParallel --- lib/std/Io.zig | 68 ++++----------------------------------- lib/std/Io/EventLoop.zig | 18 ----------- lib/std/Io/ThreadPool.zig | 7 ++-- 3 files changed, 10 insertions(+), 83 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index ee17d6b7ac..42b80bb38d 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -581,8 +581,6 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*AnyFuture, - /// Returning `null` indicates resource allocation failed. - /// /// Thread-safe. asyncConcurrent: *const fn ( /// Corresponds to `Io.userdata`. @@ -594,19 +592,6 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) error{OutOfMemory}!*AnyFuture, - /// Returning `null` indicates resource allocation failed. - /// - /// Thread-safe. - asyncParallel: *const fn ( - /// Corresponds to `Io.userdata`. - userdata: ?*anyopaque, - result_len: usize, - result_alignment: std.mem.Alignment, - /// Copied and then passed to `start`. - context: []const u8, - context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque, result: *anyopaque) void, - ) error{OutOfMemory}!*AnyFuture, /// Executes `start` asynchronously in a manner such that it cleans itself /// up. This mode does not support results, await, or cancel. /// @@ -1166,8 +1151,8 @@ pub fn Queue(Elem: type) type { /// not guaranteed to be available until `await` is called. /// /// `function` *may* be called immediately, before `async` returns. This has -/// weaker guarantees than `asyncConcurrent` and `asyncParallel`, making it the -/// most portable and reusable among the async family functions. +/// weaker guarantees than `asyncConcurrent`, making more portable and +/// reusable. /// /// See also: /// * `asyncDetached` @@ -1198,13 +1183,12 @@ pub fn async( } /// Calls `function` with `args`, such that the return value of the function is -/// not guaranteed to be available until `await` is called, passing control -/// flow back to the caller while waiting for any `Io` operations. +/// not guaranteed to be available until `await` is called, allowing the caller +/// to progress while waiting for any `Io` operations. /// -/// This has a weaker guarantee than `asyncParallel`, making it more portable -/// and reusable, however it has stronger guarantee than `async`, placing -/// restrictions on what kind of `Io` implementations are supported. By calling -/// `async` instead, one allows, for example, stackful single-threaded blocking I/O. +/// This has stronger guarantee than `async`, placing restrictions on what kind +/// of `Io` implementations are supported. By calling `async` instead, one +/// allows, for example, stackful single-threaded blocking I/O. pub fn asyncConcurrent( io: Io, function: anytype, @@ -1231,44 +1215,6 @@ pub fn asyncConcurrent( return future; } -/// Simultaneously calls `function` with `args` while passing control flow back -/// to the caller. The return value of the function is not guaranteed to be -/// available until `await` is called. -/// -/// This has the strongest guarantees of all async family functions, placing -/// the most restrictions on what kind of `Io` implementations are supported. -/// By calling `asyncConcurrent` instead, one allows, for example, stackful -/// single-threaded non-blocking I/O. -/// -/// See also: -/// * `asyncConcurrent` -/// * `async` -pub fn asyncParallel( - io: Io, - function: anytype, - args: std.meta.ArgsTuple(@TypeOf(function)), -) error{OutOfMemory}!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { - const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; - const Args = @TypeOf(args); - const TypeErased = struct { - fn start(context: *const anyopaque, result: *anyopaque) void { - const args_casted: *const Args = @alignCast(@ptrCast(context)); - const result_casted: *Result = @ptrCast(@alignCast(result)); - result_casted.* = @call(.auto, function, args_casted.*); - } - }; - var future: Future(Result) = undefined; - future.any_future = try io.vtable.asyncParallel( - io.userdata, - @sizeOf(Result), - .of(Result), - @ptrCast((&args)[0..1]), - .of(Args), - TypeErased.start, - ); - return future; -} - /// Calls `function` with `args` asynchronously. The resource cleans itself up /// when the function returns. Does not support await, cancel, or a return value. /// diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 8b8de73b23..51406e7168 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -140,7 +140,6 @@ pub fn io(el: *EventLoop) Io { .vtable = &.{ .async = async, .asyncConcurrent = asyncConcurrent, - .asyncParallel = asyncParallel, .await = await, .asyncDetached = asyncDetached, .select = select, @@ -938,23 +937,6 @@ fn asyncConcurrent( return @ptrCast(fiber); } -fn asyncParallel( - userdata: ?*anyopaque, - result_len: usize, - result_alignment: Alignment, - context: []const u8, - context_alignment: Alignment, - start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) error{OutOfMemory}!*std.Io.AnyFuture { - _ = userdata; - _ = result_len; - _ = result_alignment; - _ = context; - _ = context_alignment; - _ = start; - @panic("TODO"); -} - const DetachedClosure = struct { event_loop: *EventLoop, fiber: *Fiber, diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index 2f10c6a8fa..0006840047 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -86,8 +86,7 @@ pub fn io(pool: *Pool) Io { .userdata = pool, .vtable = &.{ .async = async, - .asyncConcurrent = asyncParallel, - .asyncParallel = asyncParallel, + .asyncConcurrent = asyncConcurrent, .await = await, .asyncDetached = asyncDetached, .cancel = cancel, @@ -220,7 +219,7 @@ fn async( } const pool: *Pool = @alignCast(@ptrCast(userdata)); const cpu_count = pool.cpu_count catch { - return asyncParallel(userdata, result.len, result_alignment, context, context_alignment, start) catch { + return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; }; @@ -284,7 +283,7 @@ fn async( return @ptrCast(closure); } -fn asyncParallel( +fn asyncConcurrent( userdata: ?*anyopaque, result_len: usize, result_alignment: std.mem.Alignment, From d801a71d29938196f3e8f43b766f42450b27f235 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 19 Jul 2025 22:10:27 -0700 Subject: [PATCH 046/244] add std.testing.io --- lib/compiler/test_runner.zig | 16 +++++++--------- lib/std/testing.zig | 3 +++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 673ea2cb2d..40f52fbd39 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -12,7 +12,7 @@ pub const std_options: std.Options = .{ }; var log_err_count: usize = 0; -var fba = std.heap.FixedBufferAllocator.init(&fba_buffer); +var fba: std.heap.FixedBufferAllocator = .init(&fba_buffer); var fba_buffer: [8192]u8 = undefined; var stdin_buffer: [4096]u8 = undefined; var stdout_buffer: [4096]u8 = undefined; @@ -131,6 +131,7 @@ fn mainServer() !void { .run_test => { testing.allocator_instance = .{}; + testing.io_instance = .init(fba.allocator()); log_err_count = 0; const index = try server.receiveBody_u32(); const test_fn = builtin.test_functions[index]; @@ -152,6 +153,8 @@ fn mainServer() !void { break :s .fail; }, }; + testing.io_instance.deinit(); + fba.reset(); const leak_count = testing.allocator_instance.detectLeaks(); testing.allocator_instance.deinitWithoutLeakChecks(); try server.serveTestResults(.{ @@ -228,18 +231,13 @@ fn mainTerminal() void { }); const have_tty = std.fs.File.stderr().isTty(); - var async_frame_buffer: []align(builtin.target.stackAlignment()) u8 = undefined; - // TODO this is on the next line (using `undefined` above) because otherwise zig incorrectly - // ignores the alignment of the slice. - async_frame_buffer = &[_]u8{}; - var leaks: usize = 0; for (test_fn_list, 0..) |test_fn, i| { testing.allocator_instance = .{}; + testing.io_instance = .init(fba.allocator()); defer { - if (testing.allocator_instance.deinit() == .leak) { - leaks += 1; - } + if (testing.allocator_instance.deinit() == .leak) leaks += 1; + testing.io_instance.deinit(); } testing.log_level = .warn; diff --git a/lib/std/testing.zig b/lib/std/testing.zig index e1e78c3bec..5baaa0f77b 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -28,6 +28,9 @@ pub var allocator_instance: std.heap.GeneralPurposeAllocator(.{ break :b .init; }; +pub var io_instance: std.Io.ThreadPool = undefined; +pub const io = io_instance.io(); + /// TODO https://github.com/ziglang/zig/issues/5738 pub var log_level = std.log.Level.warn; From 668f90524387fedb015dce8e9728ac058f0e0b66 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 19 Jul 2025 23:36:20 -0700 Subject: [PATCH 047/244] add some networking --- lib/std/Io.zig | 7 + lib/std/Io/ThreadPool.zig | 235 +++++++++++++++++++- lib/std/Io/net.zig | 450 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 684 insertions(+), 8 deletions(-) create mode 100644 lib/std/Io/net.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 42b80bb38d..b11ee66cee 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -559,6 +559,7 @@ const Io = @This(); pub const EventLoop = @import("Io/EventLoop.zig"); pub const ThreadPool = @import("Io/ThreadPool.zig"); +pub const net = @import("Io/net.zig"); userdata: ?*anyopaque, vtable: *const VTable, @@ -656,6 +657,12 @@ pub const VTable = struct { now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp, sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, + + listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.ListenOptions) net.ListenError!net.Server, + accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Server.Connection, + netRead: *const fn (?*anyopaque, src: net.Stream, dest: *Io.Writer, limit: Io.Limit) net.Stream.Reader.Error!usize, + netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, + netClose: *const fn (?*anyopaque, stream: net.Stream) void, }; pub const Cancelable = error{ diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index 0006840047..13eb2e7695 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -3,6 +3,7 @@ const std = @import("../std.zig"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const WaitGroup = std.Thread.WaitGroup; +const posix = std.posix; const Io = std.Io; const Pool = @This(); @@ -19,6 +20,9 @@ parallel_count: usize, threadlocal var current_closure: ?*AsyncClosure = null; +const max_iovecs_len = 8; +const splat_buffer_size = 64; + pub const Runnable = struct { start: Start, node: std.SinglyLinkedList.Node = .{}, @@ -107,6 +111,18 @@ pub fn io(pool: *Pool) Io { .now = now, .sleep = sleep, + + .listen = listen, + .accept = accept, + .netRead = switch (builtin.os.tag) { + .windows => @panic("TODO"), + else => netReadPosix, + }, + .netWrite = switch (builtin.os.tag) { + .windows => @panic("TODO"), + else => netWritePosix, + }, + .netClose = netClose, }, }; } @@ -461,7 +477,7 @@ fn cancel( .linux => _ = std.os.linux.tgkill( std.os.linux.getpid(), @bitCast(cancel_tid), - std.posix.SIG.IO, + posix.SIG.IO, ), else => {}, }, @@ -635,7 +651,7 @@ fn closeFile(userdata: ?*anyopaque, file: Io.File) void { return fs_file.close(); } -fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { +fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: posix.off_t) Io.File.PReadError!usize { const pool: *Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); const fs_file: std.fs.File = .{ .handle = file.handle }; @@ -645,7 +661,7 @@ fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.o }; } -fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { +fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posix.off_t) Io.File.PWriteError!usize { const pool: *Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); const fs_file: std.fs.File = .{ .handle = file.handle }; @@ -655,20 +671,20 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std. }; } -fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { +fn now(userdata: ?*anyopaque, clockid: posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { const pool: *Pool = @alignCast(@ptrCast(userdata)); try pool.checkCancel(); - const timespec = try std.posix.clock_gettime(clockid); + const timespec = try posix.clock_gettime(clockid); return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); } -fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { +fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { const pool: *Pool = @alignCast(@ptrCast(userdata)); const deadline_nanoseconds: i96 = switch (deadline) { .duration => |duration| duration.nanoseconds, .timestamp => |timestamp| @intFromEnum(timestamp), }; - var timespec: std.posix.timespec = .{ + var timespec: posix.timespec = .{ .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), }; @@ -682,7 +698,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl .FAULT => unreachable, .INTR => {}, .INVAL => return error.UnsupportedClock, - else => |err| return std.posix.unexpectedErrno(err), + else => |err| return posix.unexpectedErrno(err), } } } @@ -718,3 +734,206 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { } return result.?; } + +fn listen(userdata: ?*anyopaque, address: Io.net.IpAddress, options: Io.net.ListenOptions) Io.net.ListenError!Io.net.Server { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + + const nonblock: u32 = if (options.force_nonblocking) posix.SOCK.NONBLOCK else 0; + const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | nonblock; + const proto: u32 = posix.IPPROTO.TCP; + const family = posixAddressFamily(address); + const sockfd = try posix.socket(family, sock_flags, proto); + const stream: std.net.Stream = .{ .handle = sockfd }; + errdefer stream.close(); + + if (options.reuse_address) { + try posix.setsockopt( + sockfd, + posix.SOL.SOCKET, + posix.SO.REUSEADDR, + &std.mem.toBytes(@as(c_int, 1)), + ); + if (@hasDecl(posix.SO, "REUSEPORT") and family != posix.AF.UNIX) { + try posix.setsockopt( + sockfd, + posix.SOL.SOCKET, + posix.SO.REUSEPORT, + &std.mem.toBytes(@as(c_int, 1)), + ); + } + } + + var storage: PosixAddress = undefined; + var socklen = addressToPosix(address, &storage); + try posix.bind(sockfd, &storage.any, socklen); + try posix.listen(sockfd, options.kernel_backlog); + try posix.getsockname(sockfd, &storage.any, &socklen); + return .{ + .listen_address = addressFromPosix(&storage), + .stream = .{ .handle = stream.handle }, + }; +} + +fn accept(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.AcceptError!Io.net.Server.Connection { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + + var storage: PosixAddress = undefined; + var addr_len: posix.socklen_t = @sizeOf(PosixAddress); + const fd = try posix.accept(server.stream.handle, &storage.any, &addr_len, posix.SOCK.CLOEXEC); + return .{ + .stream = .{ .handle = fd }, + .address = addressFromPosix(&storage), + }; +} + +fn netReadPosix( + userdata: ?*anyopaque, + stream: Io.net.Stream, + w: *Io.Writer, + limit: Io.Limit, +) Io.net.Stream.Reader.Error!usize { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + const dest = try w.writableVectorPosix(&iovecs_buffer, limit); + assert(dest[0].len > 0); + const n = try posix.readv(stream.handle, dest); + if (n == 0) return error.EndOfStream; + return n; +} + +fn netWritePosix( + userdata: ?*anyopaque, + stream: Io.net.Stream, + header: []const u8, + data: []const []const u8, + splat: usize, +) Io.net.Stream.Writer.Error!usize { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + try pool.checkCancel(); + + var iovecs: [max_iovecs_len]posix.iovec_const = undefined; + var msg: posix.msghdr_const = .{ + .name = null, + .namelen = 0, + .iov = &iovecs, + .iovlen = 0, + .control = null, + .controllen = 0, + .flags = 0, + }; + addBuf(&iovecs, &msg.iovlen, header); + for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &msg.iovlen, bytes); + const pattern = data[data.len - 1]; + if (iovecs.len - msg.iovlen != 0) switch (splat) { + 0 => {}, + 1 => addBuf(&iovecs, &msg.iovlen, pattern), + else => switch (pattern.len) { + 0 => {}, + 1 => { + var backup_buffer: [splat_buffer_size]u8 = undefined; + const splat_buffer = &backup_buffer; + const memset_len = @min(splat_buffer.len, splat); + const buf = splat_buffer[0..memset_len]; + @memset(buf, pattern[0]); + addBuf(&iovecs, &msg.iovlen, buf); + var remaining_splat = splat - buf.len; + while (remaining_splat > splat_buffer.len and iovecs.len - msg.iovlen != 0) { + assert(buf.len == splat_buffer.len); + addBuf(&iovecs, &msg.iovlen, splat_buffer); + remaining_splat -= splat_buffer.len; + } + addBuf(&iovecs, &msg.iovlen, splat_buffer[0..remaining_splat]); + }, + else => for (0..@min(splat, iovecs.len - msg.iovlen)) |_| { + addBuf(&iovecs, &msg.iovlen, pattern); + }, + }, + }; + const flags = posix.MSG.NOSIGNAL; + return posix.sendmsg(stream.handle, &msg, flags); +} + +fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { + // OS checks ptr addr before length so zero length vectors must be omitted. + if (bytes.len == 0) return; + if (v.len - i.* == 0) return; + v[i.*] = .{ .base = bytes.ptr, .len = bytes.len }; + i.* += 1; +} + +fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void { + const pool: *Pool = @alignCast(@ptrCast(userdata)); + _ = pool; + const net_stream: std.net.Stream = .{ .handle = stream.handle }; + return net_stream.close(); +} + +const PosixAddress = extern union { + any: posix.sockaddr, + in: posix.sockaddr.in, + in6: posix.sockaddr.in6, +}; + +fn posixAddressFamily(a: Io.net.IpAddress) posix.sa_family_t { + return switch (a) { + .ip4 => posix.AF.INET, + .ip6 => posix.AF.INET6, + }; +} + +fn addressFromPosix(posix_address: *PosixAddress) Io.net.IpAddress { + return switch (posix_address.any.family) { + posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, + posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, + else => unreachable, + }; +} + +fn addressToPosix(a: Io.net.IpAddress, storage: *PosixAddress) posix.socklen_t { + return switch (a) { + .ip4 => |ip4| { + storage.in = address4ToPosix(ip4); + return @sizeOf(posix.sockaddr.in); + }, + .ip6 => |ip6| { + storage.in6 = address6ToPosix(ip6); + return @sizeOf(posix.sockaddr.in6); + }, + }; +} + +fn address4FromPosix(in: *posix.sockaddr.in) Io.net.Ip4Address { + return .{ + .port = std.mem.bigToNative(u16, in.port), + .bytes = @bitCast(in.addr), + }; +} + +fn address6FromPosix(in6: *posix.sockaddr.in6) Io.net.Ip6Address { + return .{ + .port = std.mem.bigToNative(u16, in6.port), + .bytes = in6.addr, + .flowinfo = in6.flowinfo, + .scope_id = in6.scope_id, + }; +} + +fn address4ToPosix(a: Io.net.Ip4Address) posix.sockaddr.in { + return .{ + .port = std.mem.nativeToBig(u16, a.port), + .addr = @bitCast(a.bytes), + }; +} + +fn address6ToPosix(a: Io.net.Ip6Address) posix.sockaddr.in6 { + return .{ + .port = std.mem.nativeToBig(u16, a.port), + .flowinfo = a.flowinfo, + .addr = a.bytes, + .scope_id = a.scope_id, + }; +} diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig new file mode 100644 index 0000000000..24c310edde --- /dev/null +++ b/lib/std/Io/net.zig @@ -0,0 +1,450 @@ +const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const std = @import("../std.zig"); +const Io = std.Io; + +pub const ListenError = std.net.Address.ListenError || Io.Cancelable; + +pub const ListenOptions = struct { + /// How many connections the kernel will accept on the application's behalf. + /// If more than this many connections pool in the kernel, clients will start + /// seeing "Connection refused". + kernel_backlog: u31 = 128, + /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. + /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. + reuse_address: bool = false, + force_nonblocking: bool = false, +}; + +pub const IpAddress = union(enum) { + ip4: Ip4Address, + ip6: Ip6Address, + + /// Parse the given IP address string into an `IpAddress` value. + pub fn parse(name: []const u8, port: u16) !IpAddress { + if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.NonCanonical, + => {}, + } + + if (parseIp6(name, port)) |ip6| return ip6 else |err| switch (err) { + error.Overflow, + error.InvalidEnd, + error.InvalidCharacter, + error.Incomplete, + error.InvalidIpv4Mapping, + => {}, + } + + return error.InvalidIpAddressFormat; + } + + pub fn parseIp6(buffer: []const u8, port: u16) Ip6Address.ParseError!IpAddress { + return .{ .ip6 = try Ip6Address.parse(buffer, port) }; + } + + pub fn parseIp4(buffer: []const u8, port: u16) Ip4Address.ParseError!IpAddress { + return .{ .ip4 = try Ip4Address.parse(buffer, port) }; + } + + /// Returns the port in native endian. + pub fn getPort(a: IpAddress) u16 { + return switch (a) { + inline .ip4, .ip6 => |x| x.port, + }; + } + + /// `port` is native-endian. + pub fn setPort(a: *IpAddress, port: u16) void { + switch (a) { + inline .ip4, .ip6 => |*x| x.port = port, + } + } + + pub fn format(a: IpAddress, w: *std.io.Writer) std.io.Writer.Error!void { + switch (a) { + .ip4, .ip6 => |x| return x.format(w), + } + } + + pub fn eql(a: IpAddress, b: IpAddress) bool { + return switch (a) { + .ip4 => |a_ip4| switch (b) { + .ip4 => |b_ip4| a_ip4.eql(b_ip4), + else => false, + }, + .ip6 => |a_ip6| switch (b) { + .ip6 => |b_ip6| a_ip6.eql(b_ip6), + else => false, + }, + }; + } + + /// The returned `Server` has an open `stream`. + pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { + return io.vtable.listen(io.userdata, address, options); + } +}; + +pub const Ip4Address = struct { + bytes: [4]u8, + port: u16, + + pub const ParseError = error{ + Overflow, + InvalidEnd, + InvalidCharacter, + Incomplete, + NonCanonical, + }; + + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip4Address { + var bytes: [4]u8 = @splat(0); + var index: u8 = 0; + var saw_any_digits = false; + var has_zero_prefix = false; + for (buffer) |c| switch (c) { + '.' => { + if (!saw_any_digits) return error.InvalidCharacter; + if (index == 3) return error.InvalidEnd; + index += 1; + saw_any_digits = false; + has_zero_prefix = false; + }, + '0'...'9' => { + if (c == '0' and !saw_any_digits) { + has_zero_prefix = true; + } else if (has_zero_prefix) { + return error.NonCanonical; + } + saw_any_digits = true; + bytes[index] = try std.math.mul(u8, bytes[index], 10); + bytes[index] = try std.math.add(u8, bytes[index], c - '0'); + }, + else => return error.InvalidCharacter, + }; + if (index == 3 and saw_any_digits) return .{ + .bytes = bytes, + .port = port, + }; + return error.Incomplete; + } + + pub fn format(a: Ip4Address, w: *std.io.Writer) std.io.Writer.Error!void { + const bytes = &a.bytes; + try w.print("{d}.{d}.{d}.{d}:{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3], a.port }); + } + + pub fn eql(a: Ip4Address, b: Ip4Address) bool { + const a_int: u32 = @bitCast(a.bytes); + const b_int: u32 = @bitCast(b.bytes); + return a.port == b.port and a_int == b_int; + } +}; + +pub const Ip6Address = struct { + /// Native endian + port: u16, + /// Big endian + bytes: [16]u8, + flowinfo: u32 = 0, + scope_id: u32 = 0, + + pub const ParseError = error{ + Overflow, + InvalidCharacter, + InvalidEnd, + InvalidIpv4Mapping, + Incomplete, + }; + + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { + var result: Ip6Address = .{ + .port = port, + .bytes = undefined, + }; + var ip_slice: *[16]u8 = &result.bytes; + + var tail: [16]u8 = undefined; + + var x: u16 = 0; + var saw_any_digits = false; + var index: u8 = 0; + var scope_id = false; + var abbrv = false; + for (buffer, 0..) |c, i| { + if (scope_id) { + if (c >= '0' and c <= '9') { + const digit = c - '0'; + { + const ov = @mulWithOverflow(result.scope_id, 10); + if (ov[1] != 0) return error.Overflow; + result.scope_id = ov[0]; + } + { + const ov = @addWithOverflow(result.scope_id, digit); + if (ov[1] != 0) return error.Overflow; + result.scope_id = ov[0]; + } + } else { + return error.InvalidCharacter; + } + } else if (c == ':') { + if (!saw_any_digits) { + if (abbrv) return error.InvalidCharacter; // ':::' + if (i != 0) abbrv = true; + @memset(ip_slice[index..], 0); + ip_slice = tail[0..]; + index = 0; + continue; + } + if (index == 14) { + return error.InvalidEnd; + } + ip_slice[index] = @as(u8, @truncate(x >> 8)); + index += 1; + ip_slice[index] = @as(u8, @truncate(x)); + index += 1; + + x = 0; + saw_any_digits = false; + } else if (c == '%') { + if (!saw_any_digits) { + return error.InvalidCharacter; + } + scope_id = true; + saw_any_digits = false; + } else if (c == '.') { + if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) { + // must start with '::ffff:' + return error.InvalidIpv4Mapping; + } + const start_index = std.mem.lastIndexOfScalar(u8, buffer[0..i], ':').? + 1; + const addr = (Ip4Address.parse(buffer[start_index..], 0) catch { + return error.InvalidIpv4Mapping; + }).bytes; + ip_slice = result.bytes[0..]; + ip_slice[10] = 0xff; + ip_slice[11] = 0xff; + + ip_slice[12] = addr[0]; + ip_slice[13] = addr[1]; + ip_slice[14] = addr[2]; + ip_slice[15] = addr[3]; + return result; + } else { + const digit = try std.fmt.charToDigit(c, 16); + { + const ov = @mulWithOverflow(x, 16); + if (ov[1] != 0) return error.Overflow; + x = ov[0]; + } + { + const ov = @addWithOverflow(x, digit); + if (ov[1] != 0) return error.Overflow; + x = ov[0]; + } + saw_any_digits = true; + } + } + + if (!saw_any_digits and !abbrv) { + return error.Incomplete; + } + if (!abbrv and index < 14) { + return error.Incomplete; + } + + if (index == 14) { + ip_slice[14] = @as(u8, @truncate(x >> 8)); + ip_slice[15] = @as(u8, @truncate(x)); + return result; + } else { + ip_slice[index] = @as(u8, @truncate(x >> 8)); + index += 1; + ip_slice[index] = @as(u8, @truncate(x)); + index += 1; + @memcpy(result.bytes[16 - index ..][0..index], ip_slice[0..index]); + return result; + } + } + + pub fn format(a: Ip6Address, w: *std.io.Writer) std.io.Writer.Error!void { + const bytes = &a.bytes; + if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { + try w.print("[::ffff:{d}.{d}.{d}.{d}]:{d}", .{ + bytes[12], bytes[13], bytes[14], bytes[15], a.port, + }); + return; + } + const parts: [8]u16 = .{ + std.mem.readInt(u16, bytes[0..2], .big), + std.mem.readInt(u16, bytes[2..4], .big), + std.mem.readInt(u16, bytes[4..6], .big), + std.mem.readInt(u16, bytes[6..8], .big), + std.mem.readInt(u16, bytes[8..10], .big), + std.mem.readInt(u16, bytes[10..12], .big), + std.mem.readInt(u16, bytes[12..14], .big), + std.mem.readInt(u16, bytes[14..16], .big), + }; + + // Find the longest zero run + var longest_start: usize = 8; + var longest_len: usize = 0; + var current_start: usize = 0; + var current_len: usize = 0; + + for (parts, 0..) |part, i| { + if (part == 0) { + if (current_len == 0) { + current_start = i; + } + current_len += 1; + if (current_len > longest_len) { + longest_start = current_start; + longest_len = current_len; + } + } else { + current_len = 0; + } + } + + // Only compress if the longest zero run is 2 or more + if (longest_len < 2) { + longest_start = 8; + longest_len = 0; + } + + try w.writeAll("["); + var i: usize = 0; + var abbrv = false; + while (i < parts.len) : (i += 1) { + if (i == longest_start) { + // Emit "::" for the longest zero run + if (!abbrv) { + try w.writeAll(if (i == 0) "::" else ":"); + abbrv = true; + } + i += longest_len - 1; // Skip the compressed range + continue; + } + if (abbrv) { + abbrv = false; + } + try w.print("{x}", .{parts[i]}); + if (i != parts.len - 1) { + try w.writeAll(":"); + } + } + try w.print("]:{d}", .{a.port}); + } + + pub fn eql(a: Ip6Address, b: Ip6Address) bool { + return a.port == b.port and std.mem.eql(u8, &a.bytes, &b.bytes); + } +}; + +pub const Stream = struct { + /// Underlying platform-defined type which may or may not be + /// interchangeable with a file system file descriptor. + handle: Handle, + + pub const Handle = switch (native_os) { + .windows => std.windows.ws2_32.SOCKET, + else => std.posix.fd_t, + }; + + pub fn close(s: Stream, io: Io) void { + return io.vtable.close(io.userdata, s); + } + + pub const Reader = struct { + io: Io, + interface: Io.Reader, + stream: Stream, + err: ?Error, + + pub const Error = std.net.Stream.ReadError || Io.Cancelable || Io.Writer.Error || error{EndOfStream}; + + pub fn init(stream: Stream, buffer: []u8) Reader { + return .{ + .interface = .{ + .vtable = &.{ .stream = streamImpl }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + .stream = stream, + .err = null, + }; + } + + fn streamImpl(io_r: *Io.Reader, io_w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); + const io = r.io; + return io.vtable.netRead(io.vtable.userdata, r.stream, io_w, limit); + } + }; + + pub const Writer = struct { + io: Io, + interface: Io.Writer, + stream: Stream, + err: ?Error = null, + + pub const Error = std.net.Stream.WriteError || Io.Cancelable; + + pub fn init(stream: Stream, buffer: []u8) Writer { + return .{ + .stream = stream, + .interface = .{ + .vtable = &.{ .drain = drain }, + .buffer = buffer, + }, + }; + } + + fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { + const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); + const io = w.io; + const buffered = io_w.buffered(); + const n = try io.vtable.netWrite(io.vtable.userdata, w.stream, buffered, data, splat); + return io_w.consume(n); + } + }; + + pub fn reader(stream: Stream, buffer: []u8) Reader { + return .init(stream, buffer); + } + + pub fn writer(stream: Stream, buffer: []u8) Writer { + return .init(stream, buffer); + } +}; + +pub const Server = struct { + listen_address: IpAddress, + stream: Stream, + + pub const Connection = struct { + stream: Stream, + address: IpAddress, + }; + + pub fn deinit(s: *Server, io: Io) void { + s.stream.close(io); + s.* = undefined; + } + + pub const AcceptError = std.posix.AcceptError || Io.Cancelable; + + /// Blocks until a client connects to the server. The returned `Connection` has + /// an open stream. + pub fn accept(s: *Server, io: Io) AcceptError!Connection { + return io.vtable.accept(io, s); + } +}; From bd3c65f7526d8997ba9e585ae97b43e6a20bc609 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 4 Sep 2025 00:01:28 -0700 Subject: [PATCH 048/244] std.Io.net: partially implement HostName.lookup --- lib/std/Io.zig | 11 +- lib/std/Io/ThreadPool.zig | 65 +++++----- lib/std/Io/net.zig | 246 +++++++++++++++++++++++++++++++++++++- lib/std/http/Client.zig | 25 ++-- lib/std/net.zig | 11 +- 5 files changed, 302 insertions(+), 56 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b11ee66cee..ae4634c812 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -660,7 +660,7 @@ pub const VTable = struct { listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.ListenOptions) net.ListenError!net.Server, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Server.Connection, - netRead: *const fn (?*anyopaque, src: net.Stream, dest: *Io.Writer, limit: Io.Limit) net.Stream.Reader.Error!usize, + netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, stream: net.Stream) void, }; @@ -760,6 +760,11 @@ pub const File = struct { } return index; } + + pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError { + assert(std.fs.path.isAbsolute(absolute_path)); + return Dir.cwd().openFile(io, absolute_path, flags); + } }; pub const Timestamp = enum(i96) { @@ -1205,7 +1210,7 @@ pub fn asyncConcurrent( const Args = @TypeOf(args); const TypeErased = struct { fn start(context: *const anyopaque, result: *anyopaque) void { - const args_casted: *const Args = @alignCast(@ptrCast(context)); + const args_casted: *const Args = @ptrCast(@alignCast(context)); const result_casted: *Result = @ptrCast(@alignCast(result)); result_casted.* = @call(.auto, function, args_casted.*); } @@ -1234,7 +1239,7 @@ pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf const Args = @TypeOf(args); const TypeErased = struct { fn start(context: *const anyopaque) void { - const args_casted: *const Args = @alignCast(@ptrCast(context)); + const args_casted: *const Args = @ptrCast(@alignCast(context)); @call(.auto, function, args_casted.*); } }; diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index 13eb2e7695..2affcdafb3 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -233,7 +233,7 @@ fn async( start(context.ptr, result.ptr); return null; } - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); const cpu_count = pool.cpu_count catch { return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); @@ -244,7 +244,7 @@ fn async( const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result.len; - const closure: *AsyncClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { + const closure: *AsyncClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { start(context.ptr, result.ptr); return null; })); @@ -309,13 +309,13 @@ fn asyncConcurrent( ) error{OutOfMemory}!*Io.AnyFuture { if (builtin.single_threaded) unreachable; - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); const cpu_count = pool.cpu_count catch 1; const gpa = pool.allocator; const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result_len; - const closure: *AsyncClosure = @alignCast(@ptrCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), n))); + const closure: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), n))); closure.* = .{ .func = start, @@ -399,11 +399,11 @@ fn asyncDetached( start: *const fn (context: *const anyopaque) void, ) void { if (builtin.single_threaded) return start(context.ptr); - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); const cpu_count = pool.cpu_count catch 1; const gpa = pool.allocator; const n = DetachedClosure.contextEnd(context_alignment, context.len); - const closure: *DetachedClosure = @alignCast(@ptrCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch { + const closure: *DetachedClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch { return start(context.ptr); })); closure.* = .{ @@ -451,7 +451,7 @@ fn await( result_alignment: std.mem.Alignment, ) void { _ = result_alignment; - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); closure.waitAndFree(pool.allocator, result); } @@ -463,7 +463,7 @@ fn cancel( result_alignment: std.mem.Alignment, ) void { _ = result_alignment; - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); switch (@atomicRmw( std.Thread.Id, @@ -486,7 +486,7 @@ fn cancel( } fn cancelRequested(userdata: ?*anyopaque) bool { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const closure = current_closure orelse return false; return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; @@ -520,7 +520,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut } fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); const cond_state = &ints[0]; @@ -567,7 +567,7 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I } fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); @@ -624,7 +624,7 @@ fn createFile( sub_path: []const u8, flags: Io.File.CreateFlags, ) Io.File.OpenError!Io.File { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; const fs_file = try fs_dir.createFile(sub_path, flags); @@ -637,7 +637,7 @@ fn openFile( sub_path: []const u8, flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; const fs_file = try fs_dir.openFile(sub_path, flags); @@ -645,14 +645,14 @@ fn openFile( } fn closeFile(userdata: ?*anyopaque, file: Io.File) void { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const fs_file: std.fs.File = .{ .handle = file.handle }; return fs_file.close(); } fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: posix.off_t) Io.File.PReadError!usize { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const fs_file: std.fs.File = .{ .handle = file.handle }; return switch (offset) { @@ -662,7 +662,7 @@ fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: posix.off_t } fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posix.off_t) Io.File.PWriteError!usize { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const fs_file: std.fs.File = .{ .handle = file.handle }; return switch (offset) { @@ -672,14 +672,14 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posi } fn now(userdata: ?*anyopaque, clockid: posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const timespec = try posix.clock_gettime(clockid); return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); } fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); const deadline_nanoseconds: i96 = switch (deadline) { .duration => |duration| duration.nanoseconds, .timestamp => |timestamp| @intFromEnum(timestamp), @@ -704,7 +704,7 @@ fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, deadline: Io.Deadline) } fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; var reset_event: std.Thread.ResetEvent = .{}; @@ -736,7 +736,7 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { } fn listen(userdata: ?*anyopaque, address: Io.net.IpAddress, options: Io.net.ListenOptions) Io.net.ListenError!Io.net.Server { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const nonblock: u32 = if (options.force_nonblocking) posix.SOCK.NONBLOCK else 0; @@ -776,7 +776,7 @@ fn listen(userdata: ?*anyopaque, address: Io.net.IpAddress, options: Io.net.List } fn accept(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.AcceptError!Io.net.Server.Connection { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); var storage: PosixAddress = undefined; @@ -788,17 +788,20 @@ fn accept(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.AcceptErr }; } -fn netReadPosix( - userdata: ?*anyopaque, - stream: Io.net.Stream, - w: *Io.Writer, - limit: Io.Limit, -) Io.net.Stream.Reader.Error!usize { - const pool: *Pool = @alignCast(@ptrCast(userdata)); +fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.net.Stream.Reader.Error!usize { + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; - const dest = try w.writableVectorPosix(&iovecs_buffer, limit); + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; assert(dest[0].len > 0); const n = try posix.readv(stream.handle, dest); if (n == 0) return error.EndOfStream; @@ -812,7 +815,7 @@ fn netWritePosix( data: []const []const u8, splat: usize, ) Io.net.Stream.Writer.Error!usize { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); var iovecs: [max_iovecs_len]posix.iovec_const = undefined; @@ -866,7 +869,7 @@ fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), } fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void { - const pool: *Pool = @alignCast(@ptrCast(userdata)); + const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const net_stream: std.net.Stream = .{ .handle = stream.handle }; return net_stream.close(); diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 24c310edde..6f198fc449 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -2,6 +2,7 @@ const builtin = @import("builtin"); const native_os = builtin.os.tag; const std = @import("../std.zig"); const Io = std.Io; +const assert = std.debug.assert; pub const ListenError = std.net.Address.ListenError || Io.Cancelable; @@ -16,10 +17,233 @@ pub const ListenOptions = struct { force_nonblocking: bool = false, }; +/// An already-validated host name. +pub const HostName = struct { + /// Externally managed memory. Already checked to be within `max_len`. + bytes: []const u8, + + pub const max_len = 255; + + pub const InitError = error{ + NameTooLong, + InvalidHostName, + }; + + pub fn init(bytes: []const u8) InitError!HostName { + if (bytes.len > max_len) return error.NameTooLong; + if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; + for (bytes) |byte| { + if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { + continue; + } + return error.InvalidHostName; + } + return .{ .bytes = bytes }; + } + + pub const LookupOptions = struct { + port: u16, + /// Must have at least length 2. + addresses_buffer: []IpAddress, + /// If a buffer of at least `max_len` is not provided, `lookup` may + /// return successfully with zero-length `LookupResult.canonical_name_len`. + /// + /// Suggestion: if not interested in canonical name, pass an empty buffer; + /// otherwise pass a buffer of size `max_len`. + canonical_name_buffer: []u8, + /// `null` means either. + family: ?IpAddress.Tag = null, + }; + + pub const LookupError = Io.Cancelable || error{}; + + pub const LookupResult = struct { + /// How many `LookupOptions.addresses_buffer` elements are populated. + addresses_len: usize, + /// Length zero means no canonical name returned. + canonical_name_len: usize, + }; + + pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { + const name = host_name.bytes; + assert(name.len <= max_len); + assert(options.addresses_buffer.len >= 2); + + if (native_os == .windows) @compileError("TODO"); + if (builtin.link_libc) @compileError("TODO"); + if (native_os == .linux) { + if (options.family != .ip6) { + if (IpAddress.parseIp4(name, options.port)) |addr| { + options.addresses_buffer[0] = addr; + return .{ .addresses_len = 1, .canonical_name_len = 0 }; + } else |_| {} + } + if (options.family != .ip4) { + if (IpAddress.parseIp6(name, options.port)) |addr| { + options.addresses_buffer[0] = addr; + return .{ .addresses_len = 1, .canonical_name_len = 0 }; + } else |_| {} + } + { + const result = try lookupHosts(io, options); + if (result.addresses_len > 0) return sortLookupResults(options, result); + } + { + // RFC 6761 Section 6.3.3 + // Name resolution APIs and libraries SHOULD recognize + // localhost names as special and SHOULD always return the IP + // loopback address for address queries and negative responses + // for all other query types. + + // Check for equal to "localhost(.)" or ends in ".localhost(.)" + const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; + if (std.mem.endsWith(u8, name, localhost) and + (name.len == localhost.len or name[name.len - localhost.len] == '.')) + { + var i: usize = 0; + if (options.family != .ip6) { + options.addresses_buffer[i] = .{ .ip4 = .localhost(options.port) }; + i += 1; + } + if (options.family != .ip4) { + options.addresses_buffer[i] = .{ .ip6 = .localhost(options.port) }; + i += 1; + } + const canon_name = "localhost"; + options.canonical_name_buffer[0..canon_name.len].* = canon_name.*; + return sortLookupResults(options, .{ .addresses_len = i, .canonical_name_len = canon_name.len }); + } + } + { + const result = try lookupDns(io, options); + if (result.addresses_len > 0) return sortLookupResults(options, result); + } + return error.UnknownHostName; + } + @compileError("unimplemented"); + } + + fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult { + _ = options; + _ = result; + @panic("TODO"); + } + + fn lookupDns(io: Io, options: LookupOptions) !LookupResult { + _ = io; + _ = options; + @panic("TODO"); + } + + fn lookupHosts(io: Io, options: LookupOptions) !LookupResult { + const file = Io.File.openFileAbsoluteZ(io, "/etc/hosts", .{}) catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => return, + else => |e| return e, + }; + defer file.close(); + + var line_buf: [512]u8 = undefined; + var file_reader = file.reader(io, &line_buf); + return lookupHostsReader(options, &file_reader.interface) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.ReadFailed => return file_reader.err.?, + }; + } + + fn lookupHostsReader(options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult { + var addresses_len: usize = 0; + var canonical_name_len: usize = 0; + while (true) { + const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { + error.StreamTooLong => { + // Skip lines that are too long. + _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) { + error.EndOfStream => break, + error.ReadFailed => return error.ReadFailed, + }; + continue; + }, + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => break, + }; + var split_it = std.mem.splitScalar(u8, line, '#'); + const no_comment_line = split_it.first(); + + var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t"); + const ip_text = line_it.next() orelse continue; + var first_name_text: ?[]const u8 = null; + while (line_it.next()) |name_text| { + if (std.mem.eql(u8, name_text, options.name)) { + if (first_name_text == null) first_name_text = name_text; + break; + } + } else continue; + + if (canonical_name_len == 0) { + if (HostName.init(first_name_text)) |name_text| { + if (name_text.len <= options.canonical_name_buffer.len) { + @memcpy(options.canonical_name_buffer[0..name_text.len], name_text); + canonical_name_len = name_text.len; + } + } + } + + if (options.family != .ip6) { + if (IpAddress.parseIp4(ip_text, options.port)) |addr| { + options.addresses_buffer[addresses_len] = addr; + addresses_len += 1; + if (options.addresses_buffer.len - addresses_len == 0) return .{ + .addresses_len = addresses_len, + .canonical_name_len = canonical_name_len, + }; + } else |_| {} + } + if (options.family != .ip4) { + if (IpAddress.parseIp6(ip_text, options.port)) |addr| { + options.addresses_buffer[addresses_len] = addr; + addresses_len += 1; + if (options.addresses_buffer.len - addresses_len == 0) return .{ + .addresses_len = addresses_len, + .canonical_name_len = canonical_name_len, + }; + } else |_| {} + } + } + } + + pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError; + + pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream { + var addresses_buffer: [32]IpAddress = undefined; + + const results = try lookup(host_name, .{ + .port = port, + .addresses_buffer = &addresses_buffer, + .canonical_name_buffer = &.{}, + }); + const addresses = addresses_buffer[0..results.addresses_len]; + + if (addresses.len == 0) return error.UnknownHostName; + + for (addresses) |addr| { + return addr.connectTcp(io) catch |err| switch (err) { + error.ConnectionRefused => continue, + else => |e| return e, + }; + } + return error.ConnectionRefused; + } +}; + pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, + pub const Tag = @typeInfo(IpAddress).@"union".tag_type.?; + /// Parse the given IP address string into an `IpAddress` value. pub fn parse(name: []const u8, port: u16) !IpAddress { if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { @@ -94,6 +318,13 @@ pub const Ip4Address = struct { bytes: [4]u8, port: u16, + pub fn localhost(port: u16) Ip4Address { + return .{ + .bytes = .{ 127, 0, 0, 1 }, + .port = port, + }; + } + pub const ParseError = error{ Overflow, InvalidEnd, @@ -373,7 +604,10 @@ pub const Stream = struct { pub fn init(stream: Stream, buffer: []u8) Reader { return .{ .interface = .{ - .vtable = &.{ .stream = streamImpl }, + .vtable = &.{ + .stream = streamImpl, + .readVec = readVec, + }, .buffer = buffer, .seek = 0, .end = 0, @@ -384,9 +618,17 @@ pub const Stream = struct { } fn streamImpl(io_r: *Io.Reader, io_w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const dest = limit.slice(try io_w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVec(io_r, &data); + io_w.advance(n); + return n; + } + + fn readVec(io_r: *Reader, data: [][]u8) Io.Reader.Error!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); const io = r.io; - return io.vtable.netRead(io.vtable.userdata, r.stream, io_w, limit); + return io.vtable.netReadVec(io.vtable.userdata, r.stream, io_r, data); } }; diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 5d6a75cb28..81bfbdcc2e 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -9,10 +9,10 @@ const builtin = @import("builtin"); const testing = std.testing; const http = std.http; const mem = std.mem; -const net = std.net; const Uri = std.Uri; const Allocator = mem.Allocator; const assert = std.debug.assert; +const Io = std.Io; const Writer = std.Io.Writer; const Reader = std.Io.Reader; @@ -22,6 +22,8 @@ pub const disable_tls = std.options.http_disable_tls; /// Used for all client allocations. Must be thread-safe. allocator: Allocator, +/// Used for opening TCP connections. +io: Io, ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, ca_bundle_mutex: std.Thread.Mutex = .{}, @@ -225,8 +227,8 @@ pub const Protocol = enum { pub const Connection = struct { client: *Client, - stream_writer: net.Stream.Writer, - stream_reader: net.Stream.Reader, + stream_writer: Io.net.Stream.Writer, + stream_reader: Io.net.Stream.Reader, /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. pool_node: std.DoublyLinkedList.Node, port: u16, @@ -242,7 +244,7 @@ pub const Connection = struct { client: *Client, remote_host: []const u8, port: u16, - stream: net.Stream, + stream: Io.net.Stream, ) error{OutOfMemory}!*Plain { const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.len); @@ -295,7 +297,7 @@ pub const Connection = struct { client: *Client, remote_host: []const u8, port: u16, - stream: net.Stream, + stream: Io.net.Stream, ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.len); @@ -363,7 +365,7 @@ pub const Connection = struct { } }; - pub const ReadError = std.crypto.tls.Client.ReadError || std.net.Stream.ReadError; + pub const ReadError = std.crypto.tls.Client.ReadError || Io.net.Stream.ReadError; pub fn getReadError(c: *const Connection) ?ReadError { return switch (c.protocol) { @@ -378,8 +380,8 @@ pub const Connection = struct { }; } - fn getStream(c: *Connection) net.Stream { - return c.stream_reader.getStream(); + fn getStream(c: *Connection) Io.net.Stream { + return c.stream_reader.stream; } pub fn host(c: *Connection) []u8 { @@ -1409,7 +1411,7 @@ pub fn connectTcp( } pub const ConnectTcpOptions = struct { - host: []const u8, + host: Io.net.HostName, port: u16, protocol: Protocol, @@ -1418,7 +1420,7 @@ pub const ConnectTcpOptions = struct { }; pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { - const host = options.host; + const host = options.host_name; const port = options.port; const protocol = options.protocol; @@ -1431,7 +1433,7 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp .protocol = protocol, })) |conn| return conn; - const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { + const stream = host.connectTcp(client.io, port) catch |err| switch (err) { error.ConnectionRefused => return error.ConnectionRefused, error.NetworkUnreachable => return error.NetworkUnreachable, error.ConnectionTimedOut => return error.ConnectionTimedOut, @@ -1440,6 +1442,7 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp error.NameServerFailure => return error.NameServerFailure, error.UnknownHostName => return error.UnknownHostName, error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses, + error.Canceled => return error.Canceled, else => return error.UnexpectedConnectFailure, }; errdefer stream.close(); diff --git a/lib/std/net.zig b/lib/std/net.zig index 9d70515c2e..7348be424a 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -1462,15 +1462,8 @@ test parseHosts { try std.testing.expectFmt("127.0.0.2:1234", "{f}", .{addrs.items[0].addr}); } -pub fn isValidHostName(hostname: []const u8) bool { - if (hostname.len >= 254) return false; - if (!std.unicode.utf8ValidateSlice(hostname)) return false; - for (hostname) |byte| { - if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { - continue; - } - return false; - } +pub fn isValidHostName(bytes: []const u8) bool { + _ = std.Io.net.HostName.init(bytes) catch return false; return true; } From e7729a7b89bf5fdde286dcb5767d856334090be6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 5 Sep 2025 17:30:07 -0700 Subject: [PATCH 049/244] std: start moving fs.File to Io --- lib/std/Io.zig | 87 ++---- lib/std/Io/EventLoop.zig | 56 ++-- lib/std/Io/File.zig | 550 ++++++++++++++++++++++++++++++++++++++ lib/std/Io/ThreadPool.zig | 271 ++++++++++++++++++- lib/std/Io/Writer.zig | 2 +- lib/std/Io/net.zig | 84 +++--- lib/std/fs/File.zig | 131 +-------- lib/std/posix.zig | 56 +--- 8 files changed, 924 insertions(+), 313 deletions(-) create mode 100644 lib/std/Io/File.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index ae4634c812..7db81ae76b 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -6,7 +6,6 @@ const windows = std.os.windows; const posix = std.posix; const math = std.math; const assert = std.debug.assert; -const fs = std.fs; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; @@ -650,10 +649,15 @@ pub const VTable = struct { conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, createFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File, - openFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File, - closeFile: *const fn (?*anyopaque, File) void, - pread: *const fn (?*anyopaque, file: File, buffer: []u8, offset: std.posix.off_t) File.PReadError!usize, + fileOpen: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File, + fileClose: *const fn (?*anyopaque, File) void, pwrite: *const fn (?*anyopaque, file: File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, + /// Returns 0 on end of stream. + fileReadStreaming: *const fn (?*anyopaque, file: File, data: [][]u8) File.ReadStreamingError!usize, + /// Returns 0 on end of stream. + fileReadPositional: *const fn (?*anyopaque, file: File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, + fileSeekBy: *const fn (?*anyopaque, file: File, offset: i64) File.SeekError!void, + fileSeekTo: *const fn (?*anyopaque, file: File, offset: u64) File.SeekError!void, now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp, sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, @@ -670,6 +674,18 @@ pub const Cancelable = error{ Canceled, }; +pub const UnexpectedError = error{ + /// The Operating System returned an undocumented error code. + /// + /// This error is in theory not possible, but it would be better + /// to handle this error than to invoke undefined behavior. + /// + /// When this error code is observed, it usually means the Zig Standard + /// Library needs a small patch to add the error code to the error set for + /// the respective function. + Unexpected, +}; + pub const Dir = struct { handle: Handle, @@ -680,7 +696,7 @@ pub const Dir = struct { pub const Handle = std.posix.fd_t; pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - return io.vtable.openFile(io.userdata, dir, sub_path, flags); + return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); } pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { @@ -706,66 +722,7 @@ pub const Dir = struct { } }; -pub const File = struct { - handle: Handle, - - pub const Handle = std.posix.fd_t; - - pub const OpenFlags = fs.File.OpenFlags; - pub const CreateFlags = fs.File.CreateFlags; - - pub const OpenError = fs.File.OpenError || Cancelable; - - pub fn close(file: File, io: Io) void { - return io.vtable.closeFile(io.userdata, file); - } - - pub const ReadError = fs.File.ReadError || Cancelable; - - pub fn read(file: File, io: Io, buffer: []u8) ReadError!usize { - return @errorCast(file.pread(io, buffer, -1)); - } - - pub const PReadError = fs.File.PReadError || Cancelable; - - pub fn pread(file: File, io: Io, buffer: []u8, offset: std.posix.off_t) PReadError!usize { - return io.vtable.pread(io.userdata, file, buffer, offset); - } - - pub const WriteError = fs.File.WriteError || Cancelable; - - pub fn write(file: File, io: Io, buffer: []const u8) WriteError!usize { - return @errorCast(file.pwrite(io, buffer, -1)); - } - - pub const PWriteError = fs.File.PWriteError || Cancelable; - - pub fn pwrite(file: File, io: Io, buffer: []const u8, offset: std.posix.off_t) PWriteError!usize { - return io.vtable.pwrite(io.userdata, file, buffer, offset); - } - - pub fn writeAll(file: File, io: Io, bytes: []const u8) WriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try file.write(io, bytes[index..]); - } - } - - pub fn readAll(file: File, io: Io, buffer: []u8) ReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try file.read(io, buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; - } - - pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError { - assert(std.fs.path.isAbsolute(absolute_path)); - return Dir.cwd().openFile(io, absolute_path, flags); - } -}; +pub const File = @import("Io/File.zig"); pub const Timestamp = enum(i96) { _, diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 51406e7168..e24b6b445e 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -93,7 +93,7 @@ const Fiber = struct { } fn resultPointer(f: *Fiber, comptime Result: type) *Result { - return @alignCast(@ptrCast(f.resultBytes(.of(Result)))); + return @ptrCast(@alignCast(f.resultBytes(.of(Result)))); } fn resultBytes(f: *Fiber, alignment: Alignment) [*]u8 { @@ -153,8 +153,8 @@ pub fn io(el: *EventLoop) Io { .conditionWake = conditionWake, .createFile = createFile, - .openFile = openFile, - .closeFile = closeFile, + .fileOpen = fileOpen, + .fileClose = fileClose, .pread = pread, .pwrite = pwrite, @@ -193,7 +193,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { }; const main_thread = &el.threads.allocated[0]; Thread.self = main_thread; - const idle_stack_end: [*]align(16) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); + const idle_stack_end: [*]align(16) usize = @ptrCast(@alignCast(allocated_slice[idle_stack_end_offset..].ptr)); (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; main_thread.* = .{ .thread = undefined, @@ -244,7 +244,7 @@ pub fn deinit(el: *EventLoop) void { assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async } el.yield(null, .exit); - const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @alignCast(@ptrCast(el.threads.allocated.ptr)); + const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @ptrCast(@alignCast(el.threads.allocated.ptr)); const idle_stack_end_offset = std.mem.alignForward(usize, el.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); for (el.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); el.gpa.free(allocated_ptr[0..idle_stack_end_offset]); @@ -530,7 +530,7 @@ const SwitchMessage = struct { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); assert(prev_fiber.queue_next == null); for (futures) |any_future| { - const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); if (@atomicRmw(?*Fiber, &future_fiber.awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) { const closure: *AsyncClosure = .fromFiber(future_fiber); if (!@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .seq_cst)) { @@ -897,12 +897,12 @@ fn asyncConcurrent( assert(result_len <= Fiber.max_result_size); // TODO assert(context.len <= Fiber.max_context_size); // TODO - const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); + const event_loop: *EventLoop = @ptrCast(@alignCast(userdata)); const fiber = try Fiber.allocate(event_loop); std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = .fromFiber(fiber); - const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); + const stack_end: [*]align(16) usize = @ptrCast(@alignCast(closure)); (stack_end - 1)[0..1].* = .{@intFromPtr(&AsyncClosure.call)}; fiber.* = .{ .required_align = {}, @@ -974,7 +974,7 @@ fn asyncDetached( assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO assert(context.len <= Fiber.max_context_size); // TODO - const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); + const event_loop: *EventLoop = @ptrCast(@alignCast(userdata)); const fiber = Fiber.allocate(event_loop) catch { start(context.ptr); return; @@ -985,7 +985,7 @@ fn asyncDetached( const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward( @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, ) - @sizeOf(DetachedClosure)); - const stack_end: [*]align(16) usize = @alignCast(@ptrCast(closure)); + const stack_end: [*]align(16) usize = @ptrCast(@alignCast(closure)); (stack_end - 1)[0..1].* = .{@intFromPtr(&DetachedClosure.call)}; fiber.* = .{ .required_align = {}, @@ -1035,8 +1035,8 @@ fn await( result: []u8, result_alignment: Alignment, ) void { - const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); - const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + const event_loop: *EventLoop = @ptrCast(@alignCast(userdata)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, .{ .register_awaiter = &future_fiber.awaiter }); @memcpy(result, future_fiber.resultBytes(result_alignment)); @@ -1044,11 +1044,11 @@ fn await( } fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); // Optimization to avoid the yield below. for (futures, 0..) |any_future, i| { - const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) == Fiber.finished) return i; } @@ -1062,7 +1062,7 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { var result: ?usize = null; for (futures, 0..) |any_future, i| { - const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); if (@cmpxchgStrong(?*Fiber, &future_fiber.awaiter, my_fiber, null, .seq_cst, .seq_cst)) |awaiter| { if (awaiter == Fiber.finished) { if (result == null) result = i; @@ -1085,7 +1085,7 @@ fn cancel( result: []u8, result_alignment: Alignment, ) void { - const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); if (@atomicRmw( ?*Thread, &future_fiber.cancel_thread, @@ -1124,7 +1124,7 @@ fn createFile( sub_path: []const u8, flags: Io.File.CreateFlags, ) Io.File.OpenError!Io.File { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); @@ -1220,13 +1220,13 @@ fn createFile( } } -fn openFile( +fn fileOpen( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); @@ -1328,8 +1328,8 @@ fn openFile( } } -fn closeFile(userdata: ?*anyopaque, file: Io.File) void { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); +fn fileClose(userdata: ?*anyopaque, file: Io.File) void { + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); @@ -1365,7 +1365,7 @@ fn closeFile(userdata: ?*anyopaque, file: Io.File) void { } fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.off_t) Io.File.PReadError!usize { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); @@ -1417,7 +1417,7 @@ fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.o } fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std.posix.off_t) Io.File.PWriteError!usize { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); @@ -1479,7 +1479,7 @@ fn now(userdata: ?*anyopaque, clockid: std.posix.clockid_t) Io.ClockGetTimeError } fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const thread: *Thread = .current(); const iou = &thread.io_uring; const fiber = thread.currentFiber(); @@ -1532,7 +1532,7 @@ fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.Deadl } fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); el.yield(null, .{ .mutex_lock = .{ .prev_state = prev_state, .mutex = mutex } }); } fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { @@ -1553,7 +1553,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut .acquire, ) orelse return) |next_state| maybe_waiting_fiber = @ptrFromInt(@intFromEnum(next_state)); maybe_waiting_fiber.?.queue_next = null; - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); el.yield(maybe_waiting_fiber.?, .reschedule); } @@ -1566,7 +1566,7 @@ const ConditionImpl = struct { }; fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); el.yield(null, .{ .condition_wait = .{ .cond = cond, .mutex = mutex } }); const thread = Thread.current(); const fiber = thread.currentFiber(); @@ -1595,7 +1595,7 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I } fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { - const el: *EventLoop = @alignCast(@ptrCast(userdata)); + const el: *EventLoop = @ptrCast(@alignCast(userdata)); const waiting_fiber = @atomicRmw(?*Fiber, @as(*?*Fiber, @ptrCast(&cond.state)), .Xchg, null, .acquire) orelse return; waiting_fiber.resultPointer(ConditionImpl).event = .{ .wake = wake }; el.yield(waiting_fiber, .reschedule); diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig new file mode 100644 index 0000000000..b8db947f58 --- /dev/null +++ b/lib/std/Io/File.zig @@ -0,0 +1,550 @@ +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const Io = std.Io; +const File = @This(); +const assert = std.debug.assert; + +handle: Handle, + +pub const Handle = std.posix.fd_t; +pub const Mode = std.posix.mode_t; +pub const INode = std.posix.ino_t; + +pub const Kind = enum { + block_device, + character_device, + directory, + named_pipe, + sym_link, + file, + unix_domain_socket, + whiteout, + door, + event_port, + unknown, +}; + +pub const Stat = struct { + /// A number that the system uses to point to the file metadata. This + /// number is not guaranteed to be unique across time, as some file + /// systems may reuse an inode after its file has been deleted. Some + /// systems may change the inode of a file over time. + /// + /// On Linux, the inode is a structure that stores the metadata, and + /// the inode _number_ is what you see here: the index number of the + /// inode. + /// + /// The FileIndex on Windows is similar. It is a number for a file that + /// is unique to each filesystem. + inode: INode, + size: u64, + /// This is available on POSIX systems and is always 0 otherwise. + mode: Mode, + kind: Kind, + + /// Last access time in nanoseconds, relative to UTC 1970-01-01. + atime: i128, + /// Last modification time in nanoseconds, relative to UTC 1970-01-01. + mtime: i128, + /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. + ctime: i128, + + pub fn fromPosix(st: std.posix.Stat) Stat { + const atime = st.atime(); + const mtime = st.mtime(); + const ctime = st.ctime(); + return .{ + .inode = st.ino, + .size = @bitCast(st.size), + .mode = st.mode, + .kind = k: { + const m = st.mode & std.posix.S.IFMT; + switch (m) { + std.posix.S.IFBLK => break :k .block_device, + std.posix.S.IFCHR => break :k .character_device, + std.posix.S.IFDIR => break :k .directory, + std.posix.S.IFIFO => break :k .named_pipe, + std.posix.S.IFLNK => break :k .sym_link, + std.posix.S.IFREG => break :k .file, + std.posix.S.IFSOCK => break :k .unix_domain_socket, + else => {}, + } + if (builtin.os.tag == .illumos) switch (m) { + std.posix.S.IFDOOR => break :k .door, + std.posix.S.IFPORT => break :k .event_port, + else => {}, + }; + + break :k .unknown; + }, + .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, + .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, + .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, + }; + } + + pub fn fromLinux(stx: std.os.linux.Statx) Stat { + const atime = stx.atime; + const mtime = stx.mtime; + const ctime = stx.ctime; + + return .{ + .inode = stx.ino, + .size = stx.size, + .mode = stx.mode, + .kind = switch (stx.mode & std.os.linux.S.IFMT) { + std.os.linux.S.IFDIR => .directory, + std.os.linux.S.IFCHR => .character_device, + std.os.linux.S.IFBLK => .block_device, + std.os.linux.S.IFREG => .file, + std.os.linux.S.IFIFO => .named_pipe, + std.os.linux.S.IFLNK => .sym_link, + std.os.linux.S.IFSOCK => .unix_domain_socket, + else => .unknown, + }, + .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, + .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, + .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, + }; + } + + pub fn fromWasi(st: std.os.wasi.filestat_t) Stat { + return .{ + .inode = st.ino, + .size = @bitCast(st.size), + .mode = 0, + .kind = switch (st.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + }, + .atime = st.atim, + .mtime = st.mtim, + .ctime = st.ctim, + }; + } +}; + +pub const StatError = std.posix.FStatError || Io.Cancelable; + +/// Returns `Stat` containing basic information about the `File`. +pub fn stat(file: File, io: Io) StatError!Stat { + _ = file; + _ = io; + @panic("TODO"); +} + +pub const OpenFlags = std.fs.File.OpenFlags; +pub const CreateFlags = std.fs.File.CreateFlags; + +pub const OpenError = std.fs.File.OpenError || Io.Cancelable; + +pub fn close(file: File, io: Io) void { + return io.vtable.fileClose(io.userdata, file); +} + +pub const ReadStreamingError = error{ + InputOutput, + SystemResources, + IsDir, + BrokenPipe, + ConnectionResetByPeer, + ConnectionTimedOut, + NotOpenForReading, + SocketNotConnected, + /// This error occurs when no global event loop is configured, + /// and reading from the file descriptor would block. + WouldBlock, + /// In WASI, this error occurs when the file descriptor does + /// not hold the required rights to read from it. + AccessDenied, + /// This error occurs in Linux if the process to be read from + /// no longer exists. + ProcessNotFound, + /// Unable to read file due to lock. + LockViolation, +} || Io.Cancelable || Io.UnexpectedError; + +pub const ReadPositionalError = ReadStreamingError || error{Unseekable}; + +pub fn readPositional(file: File, io: Io, buffer: []u8, offset: u64) ReadPositionalError!usize { + return io.vtable.pread(io.userdata, file, buffer, offset); +} + +pub const WriteError = std.fs.File.WriteError || Io.Cancelable; + +pub fn write(file: File, io: Io, buffer: []const u8) WriteError!usize { + return @errorCast(file.pwrite(io, buffer, -1)); +} + +pub const PWriteError = std.fs.File.PWriteError || Io.Cancelable; + +pub fn pwrite(file: File, io: Io, buffer: []const u8, offset: std.posix.off_t) PWriteError!usize { + return io.vtable.pwrite(io.userdata, file, buffer, offset); +} + +pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError!File { + assert(std.fs.path.isAbsolute(absolute_path)); + return Io.Dir.cwd().openFile(io, absolute_path, flags); +} + +/// Defaults to positional reading; falls back to streaming. +/// +/// Positional is more threadsafe, since the global seek position is not +/// affected. +pub fn reader(file: File, io: Io, buffer: []u8) Reader { + return .init(file, io, buffer); +} + +/// Positional is more threadsafe, since the global seek position is not +/// affected, but when such syscalls are not available, preemptively +/// initializing in streaming mode skips a failed syscall. +pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader { + return .initStreaming(file, io, buffer); +} + +pub const SeekError = error{ + Unseekable, + /// The file descriptor does not hold the required rights to seek on it. + AccessDenied, +} || Io.Cancelable || Io.UnexpectedError; + +/// Memoizes key information about a file handle such as: +/// * The size from calling stat, or the error that occurred therein. +/// * The current seek position. +/// * The error that occurred when trying to seek. +/// * Whether reading should be done positionally or streaming. +/// * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`) +/// versus plain variants (e.g. `read`). +/// +/// Fulfills the `Io.Reader` interface. +pub const Reader = struct { + io: Io, + file: File, + err: ?Error = null, + mode: Reader.Mode = .positional, + /// Tracks the true seek position in the file. To obtain the logical + /// position, use `logicalPos`. + pos: u64 = 0, + size: ?u64 = null, + size_err: ?SizeError = null, + seek_err: ?Reader.SeekError = null, + interface: Io.Reader, + + pub const Error = std.posix.ReadError || Io.Cancelable; + + pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{ + /// Occurs if, for example, the file handle is a network socket and therefore does not have a size. + Streaming, + }; + + pub const SeekError = File.SeekError || error{ + /// Seeking fell back to reading, and reached the end before the requested seek position. + /// `pos` remains at the end of the file. + EndOfStream, + /// Seeking fell back to reading, which failed. + ReadFailed, + }; + + pub const Mode = enum { + streaming, + positional, + /// Avoid syscalls other than `read` and `readv`. + streaming_reading, + /// Avoid syscalls other than `pread` and `preadv`. + positional_reading, + /// Indicates reading cannot continue because of a seek failure. + failure, + + pub fn toStreaming(m: @This()) @This() { + return switch (m) { + .positional, .streaming => .streaming, + .positional_reading, .streaming_reading => .streaming_reading, + .failure => .failure, + }; + } + + pub fn toReading(m: @This()) @This() { + return switch (m) { + .positional, .positional_reading => .positional_reading, + .streaming, .streaming_reading => .streaming_reading, + .failure => .failure, + }; + } + }; + + pub fn initInterface(buffer: []u8) Io.Reader { + return .{ + .vtable = &.{ + .stream = Reader.stream, + .discard = Reader.discard, + .readVec = Reader.readVec, + }, + .buffer = buffer, + .seek = 0, + .end = 0, + }; + } + + pub fn init(file: File, io: Io, buffer: []u8) Reader { + return .{ + .io = io, + .file = file, + .interface = initInterface(buffer), + }; + } + + pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader { + return .{ + .io = io, + .file = file, + .interface = initInterface(buffer), + .size = size, + }; + } + + /// Positional is more threadsafe, since the global seek position is not + /// affected, but when such syscalls are not available, preemptively + /// initializing in streaming mode skips a failed syscall. + pub fn initStreaming(file: File, io: Io, buffer: []u8) Reader { + return .{ + .io = io, + .file = file, + .interface = Reader.initInterface(buffer), + .mode = .streaming, + .seek_err = error.Unseekable, + .size_err = error.Streaming, + }; + } + + pub fn getSize(r: *Reader) SizeError!u64 { + return r.size orelse { + if (r.size_err) |err| return err; + if (std.posix.Stat == void) { + r.size_err = error.Streaming; + return error.Streaming; + } + if (stat(r.file, r.io)) |st| { + if (st.kind == .file) { + r.size = st.size; + return st.size; + } else { + r.mode = r.mode.toStreaming(); + r.size_err = error.Streaming; + return error.Streaming; + } + } else |err| { + r.size_err = err; + return err; + } + }; + } + + pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void { + const io = r.io; + switch (r.mode) { + .positional, .positional_reading => { + setPosAdjustingBuffer(r, @intCast(@as(i64, @intCast(r.pos)) + offset)); + }, + .streaming, .streaming_reading => { + if (std.posix.SEEK == void) { + r.seek_err = error.Unseekable; + return error.Unseekable; + } + const seek_err = r.seek_err orelse e: { + if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| { + setPosAdjustingBuffer(r, @intCast(@as(i64, @intCast(r.pos)) + offset)); + return; + } else |err| { + r.seek_err = err; + break :e err; + } + }; + var remaining = std.math.cast(u64, offset) orelse return seek_err; + while (remaining > 0) { + remaining -= discard(&r.interface, .limited64(remaining)) catch |err| { + r.seek_err = err; + return err; + }; + } + r.interface.seek = 0; + r.interface.end = 0; + }, + .failure => return r.seek_err.?, + } + } + + pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void { + const io = r.io; + switch (r.mode) { + .positional, .positional_reading => { + setPosAdjustingBuffer(r, offset); + }, + .streaming, .streaming_reading => { + if (offset >= r.pos) return Reader.seekBy(r, @intCast(offset - r.pos)); + if (r.seek_err) |err| return err; + io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| { + r.seek_err = err; + return err; + }; + setPosAdjustingBuffer(r, offset); + }, + .failure => return r.seek_err.?, + } + } + + pub fn logicalPos(r: *const Reader) u64 { + return r.pos - r.interface.bufferedLen(); + } + + fn setPosAdjustingBuffer(r: *Reader, offset: u64) void { + const logical_pos = logicalPos(r); + if (offset < logical_pos or offset >= r.pos) { + r.interface.seek = 0; + r.interface.end = 0; + r.pos = offset; + } else { + const logical_delta: usize = @intCast(offset - logical_pos); + r.interface.seek += logical_delta; + } + } + + /// Number of slices to store on the stack, when trying to send as many byte + /// vectors through the underlying read calls as possible. + const max_buffers_len = 16; + + fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); + switch (r.mode) { + .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { + error.Unimplemented => { + r.mode = r.mode.toReading(); + return 0; + }, + else => |e| return e, + }, + .positional_reading => { + const dest = limit.slice(try w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVecPositional(r, &data); + w.advance(n); + return n; + }, + .streaming_reading => { + const dest = limit.slice(try w.writableSliceGreedy(1)); + var data: [1][]u8 = .{dest}; + const n = try readVecStreaming(r, &data); + w.advance(n); + return n; + }, + .failure => return error.ReadFailed, + } + } + + fn readVec(io_reader: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); + switch (r.mode) { + .positional, .positional_reading => return readVecPositional(r, data), + .streaming, .streaming_reading => return readVecStreaming(r, data), + .failure => return error.ReadFailed, + } + } + + fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize { + const io = r.io; + assert(r.interface.bufferedLen() == 0); + var iovecs_buffer: [max_buffers_len][]u8 = undefined; + const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.fileReadPositional(io.userdata, r.file, dest, r.pos) catch |err| switch (err) { + error.Unseekable => { + r.mode = r.mode.toStreaming(); + const pos = r.pos; + if (pos != 0) { + r.pos = 0; + r.seekBy(@intCast(pos)) catch { + r.mode = .failure; + return error.ReadFailed; + }; + } + return 0; + }, + else => |e| { + r.err = e; + return error.ReadFailed; + }, + }; + if (n == 0) { + r.size = r.pos; + return error.EndOfStream; + } + r.pos += n; + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; + } + + fn readVecStreaming(r: *Reader, data: [][]u8) Io.Reader.Error!usize { + const io = r.io; + var iovecs_buffer: [max_buffers_len][]u8 = undefined; + const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.fileReadStreaming(io.userdata, r.file, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + r.size = r.pos; + return error.EndOfStream; + } + r.pos += n; + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; + } + + fn discard(io_reader: *Io.Reader, limit: Io.Limit) Io.Reader.Error!usize { + const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); + const io = r.io; + const file = r.file; + const pos = r.pos; + switch (r.mode) { + .positional, .positional_reading => { + const size = r.getSize() catch { + r.mode = r.mode.toStreaming(); + return 0; + }; + const delta = @min(@intFromEnum(limit), size - pos); + r.pos = pos + delta; + return delta; + }, + .streaming, .streaming_reading => { + const size = r.getSize() catch return 0; + const n = @min(size - pos, std.math.maxInt(i64), @intFromEnum(limit)); + io.vtable.fileSeekBy(io.userdata, file, n) catch |err| { + r.seek_err = err; + return 0; + }; + r.pos = pos + n; + return n; + }, + .failure => return error.ReadFailed, + } + } + + pub fn atEnd(r: *Reader) bool { + // Even if stat fails, size is set when end is encountered. + const size = r.size orelse return false; + return size - r.pos == 0; + } +}; diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index 2affcdafb3..6ba1249669 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -1,11 +1,16 @@ +const Pool = @This(); + const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const is_windows = native_os == .windows; +const windows = std.os.windows; + const std = @import("../std.zig"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const WaitGroup = std.Thread.WaitGroup; const posix = std.posix; const Io = std.Io; -const Pool = @This(); /// Thread-safe. allocator: Allocator, @@ -23,6 +28,10 @@ threadlocal var current_closure: ?*AsyncClosure = null; const max_iovecs_len = 8; const splat_buffer_size = 64; +comptime { + assert(max_iovecs_len <= posix.IOV_MAX); +} + pub const Runnable = struct { start: Start, node: std.SinglyLinkedList.Node = .{}, @@ -104,10 +113,13 @@ pub fn io(pool: *Pool) Io { .conditionWake = conditionWake, .createFile = createFile, - .openFile = openFile, - .closeFile = closeFile, - .pread = pread, + .fileOpen = fileOpen, + .fileClose = fileClose, .pwrite = pwrite, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, .now = now, .sleep = sleep, @@ -631,7 +643,7 @@ fn createFile( return .{ .handle = fs_file.handle }; } -fn openFile( +fn fileOpen( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, @@ -644,21 +656,256 @@ fn openFile( return .{ .handle = fs_file.handle }; } -fn closeFile(userdata: ?*anyopaque, file: Io.File) void { +fn fileClose(userdata: ?*anyopaque, file: Io.File) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const fs_file: std.fs.File = .{ .handle = file.handle }; return fs_file.close(); } -fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: posix.off_t) Io.File.PReadError!usize { +fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + + if (is_windows) { + const DWORD = windows.DWORD; + var index: usize = 0; + var truncate: usize = 0; + var total: usize = 0; + while (index < data.len) { + try pool.checkCancel(); + { + const untruncated = data[index]; + data[index] = untruncated[truncate..]; + defer data[index] = untruncated; + const buffer = data[index..]; + const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); + var n: DWORD = undefined; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) == 0) { + switch (windows.GetLastError()) { + .IO_PENDING => unreachable, + .OPERATION_ABORTED => continue, + .BROKEN_PIPE => return 0, + .HANDLE_EOF => return 0, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + .LOCK_VIOLATION => return error.LockViolation, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.NotOpenForReading, + else => |err| return windows.unexpectedError(err), + } + } + total += n; + truncate += n; + } + while (index < data.len and truncate >= data[index].len) { + truncate -= data[index].len; + index += 1; + } + } + return total; + } + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + if (native_os == .wasi and !builtin.link_libc) { + try pool.checkCancel(); + var nread: usize = undefined; + switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) { + .SUCCESS => return nread, + .INTR => unreachable, + .INVAL => unreachable, + .FAULT => unreachable, + .AGAIN => unreachable, // currently not support in WASI + .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, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + } + + while (true) { + try pool.checkCancel(); + const rc = posix.system.readv(file.handle, dest.ptr, dest.len); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .INVAL => unreachable, + .FAULT => unreachable, + .SRCH => 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), + } + } +} + +fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + + const have_pread_but_not_preadv = switch (native_os) { + .windows, .macos, .ios, .watchos, .tvos, .visionos, .haiku, .serenity => true, + else => false, + }; + if (have_pread_but_not_preadv) { + @compileError("TODO"); + } + + if (is_windows) { + const DWORD = windows.DWORD; + const OVERLAPPED = windows.OVERLAPPED; + var index: usize = 0; + var truncate: usize = 0; + var total: usize = 0; + while (true) { + try pool.checkCancel(); + { + const untruncated = data[index]; + data[index] = untruncated[truncate..]; + defer data[index] = untruncated; + const buffer = data[index..]; + const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); + var n: DWORD = undefined; + var overlapped_data: OVERLAPPED = undefined; + const overlapped: ?*OVERLAPPED = if (offset) |off| blk: { + overlapped_data = .{ + .Internal = 0, + .InternalHigh = 0, + .DUMMYUNIONNAME = .{ + .DUMMYSTRUCTNAME = .{ + .Offset = @as(u32, @truncate(off)), + .OffsetHigh = @as(u32, @truncate(off >> 32)), + }, + }, + .hEvent = null, + }; + break :blk &overlapped_data; + } else null; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, overlapped) == 0) { + switch (windows.GetLastError()) { + .IO_PENDING => unreachable, + .OPERATION_ABORTED => continue, + .BROKEN_PIPE => return 0, + .HANDLE_EOF => return 0, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + .LOCK_VIOLATION => return error.LockViolation, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.NotOpenForReading, + else => |err| return windows.unexpectedError(err), + } + } + total += n; + truncate += n; + } + while (index < data.len and truncate >= data[index].len) { + truncate -= data[index].len; + index += 1; + } + } + return total; + } + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + if (native_os == .wasi and !builtin.link_libc) { + try pool.checkCancel(); + var nread: usize = undefined; + switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) { + .SUCCESS => return nread, + .INTR => unreachable, + .INVAL => unreachable, + .FAULT => unreachable, + .AGAIN => unreachable, + .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, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + } + + const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; + while (true) { + try pool.checkCancel(); + const rc = preadv_sym(file.handle, dest.ptr, dest.len, @bitCast(offset)); + switch (posix.errno(rc)) { + .SUCCESS => return @bitCast(rc), + .INTR => continue, + .INVAL => unreachable, + .FAULT => unreachable, + .SRCH => 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, + .NXIO => return error.Unseekable, + .SPIPE => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); - const fs_file: std.fs.File = .{ .handle = file.handle }; - return switch (offset) { - -1 => fs_file.read(buffer), - else => fs_file.pread(buffer, @bitCast(offset)), - }; + + _ = file; + _ = offset; + @panic("TODO"); +} + +fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = file; + _ = offset; + @panic("TODO"); } fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posix.off_t) Io.File.PWriteError!usize { diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index a525a028d7..2bb3c12b15 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -5,7 +5,7 @@ const Writer = @This(); const std = @import("../std.zig"); const assert = std.debug.assert; const Limit = std.Io.Limit; -const File = std.fs.File; +const File = std.Io.File; const testing = std.testing; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 6f198fc449..b9ddccff3e 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -17,9 +17,12 @@ pub const ListenOptions = struct { force_nonblocking: bool = false, }; -/// An already-validated host name. +/// An already-validated host name. A valid host name: +/// * Has length less than or equal to `max_len`. +/// * Is valid UTF-8. +/// * Lacks ASCII characters other than alphanumeric, '-', and '.'. pub const HostName = struct { - /// Externally managed memory. Already checked to be within `max_len`. + /// Externally managed memory. Already checked to be valid. bytes: []const u8, pub const max_len = 255; @@ -55,13 +58,14 @@ pub const HostName = struct { family: ?IpAddress.Tag = null, }; - pub const LookupError = Io.Cancelable || error{}; + pub const LookupError = Io.Cancelable || Io.File.OpenError || Io.File.Reader.Error || error{ + UnknownHostName, + }; pub const LookupResult = struct { /// How many `LookupOptions.addresses_buffer` elements are populated. - addresses_len: usize, - /// Length zero means no canonical name returned. - canonical_name_len: usize, + addresses_len: usize = 0, + canonical_name: ?HostName = null, }; pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { @@ -75,17 +79,17 @@ pub const HostName = struct { if (options.family != .ip6) { if (IpAddress.parseIp4(name, options.port)) |addr| { options.addresses_buffer[0] = addr; - return .{ .addresses_len = 1, .canonical_name_len = 0 }; + return .{ .addresses_len = 1 }; } else |_| {} } if (options.family != .ip4) { if (IpAddress.parseIp6(name, options.port)) |addr| { options.addresses_buffer[0] = addr; - return .{ .addresses_len = 1, .canonical_name_len = 0 }; + return .{ .addresses_len = 1 }; } else |_| {} } { - const result = try lookupHosts(io, options); + const result = try lookupHosts(host_name, io, options); if (result.addresses_len > 0) return sortLookupResults(options, result); } { @@ -110,8 +114,12 @@ pub const HostName = struct { i += 1; } const canon_name = "localhost"; - options.canonical_name_buffer[0..canon_name.len].* = canon_name.*; - return sortLookupResults(options, .{ .addresses_len = i, .canonical_name_len = canon_name.len }); + const canon_name_dest = options.canonical_name_buffer[0..canon_name.len]; + canon_name_dest.* = canon_name.*; + return sortLookupResults(options, .{ + .addresses_len = i, + .canonical_name = .{ .bytes = canon_name_dest }, + }); } } { @@ -135,27 +143,27 @@ pub const HostName = struct { @panic("TODO"); } - fn lookupHosts(io: Io, options: LookupOptions) !LookupResult { - const file = Io.File.openFileAbsoluteZ(io, "/etc/hosts", .{}) catch |err| switch (err) { + fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { + const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) { error.FileNotFound, error.NotDir, error.AccessDenied, - => return, + => return .{}, + else => |e| return e, }; - defer file.close(); + defer file.close(io); var line_buf: [512]u8 = undefined; var file_reader = file.reader(io, &line_buf); - return lookupHostsReader(options, &file_reader.interface) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, + return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, }; } - fn lookupHostsReader(options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult { + fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult { var addresses_len: usize = 0; - var canonical_name_len: usize = 0; + var canonical_name: ?HostName = null; while (true) { const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { error.StreamTooLong => { @@ -176,19 +184,20 @@ pub const HostName = struct { const ip_text = line_it.next() orelse continue; var first_name_text: ?[]const u8 = null; while (line_it.next()) |name_text| { - if (std.mem.eql(u8, name_text, options.name)) { + if (std.mem.eql(u8, name_text, host_name.bytes)) { if (first_name_text == null) first_name_text = name_text; break; } } else continue; - if (canonical_name_len == 0) { - if (HostName.init(first_name_text)) |name_text| { - if (name_text.len <= options.canonical_name_buffer.len) { - @memcpy(options.canonical_name_buffer[0..name_text.len], name_text); - canonical_name_len = name_text.len; + if (canonical_name == null) { + if (HostName.init(first_name_text.?)) |name_text| { + if (name_text.bytes.len <= options.canonical_name_buffer.len) { + const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len]; + @memcpy(canonical_name_dest, name_text.bytes); + canonical_name = .{ .bytes = canonical_name_dest }; } - } + } else |_| {} } if (options.family != .ip6) { @@ -197,7 +206,7 @@ pub const HostName = struct { addresses_len += 1; if (options.addresses_buffer.len - addresses_len == 0) return .{ .addresses_len = addresses_len, - .canonical_name_len = canonical_name_len, + .canonical_name = canonical_name, }; } else |_| {} } @@ -207,11 +216,15 @@ pub const HostName = struct { addresses_len += 1; if (options.addresses_buffer.len - addresses_len == 0) return .{ .addresses_len = addresses_len, - .canonical_name_len = canonical_name_len, + .canonical_name = canonical_name, }; } else |_| {} } } + return .{ + .addresses_len = addresses_len, + .canonical_name = canonical_name, + }; } pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError; @@ -289,9 +302,9 @@ pub const IpAddress = union(enum) { } } - pub fn format(a: IpAddress, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(a: IpAddress, w: *Io.Writer) Io.Writer.Error!void { switch (a) { - .ip4, .ip6 => |x| return x.format(w), + inline .ip4, .ip6 => |x| return x.format(w), } } @@ -365,7 +378,7 @@ pub const Ip4Address = struct { return error.Incomplete; } - pub fn format(a: Ip4Address, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(a: Ip4Address, w: *Io.Writer) Io.Writer.Error!void { const bytes = &a.bytes; try w.print("{d}.{d}.{d}.{d}:{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3], a.port }); } @@ -393,6 +406,13 @@ pub const Ip6Address = struct { Incomplete, }; + pub fn localhost(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, + .port = port, + }; + } + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { var result: Ip6Address = .{ .port = port, @@ -504,7 +524,7 @@ pub const Ip6Address = struct { } } - pub fn format(a: Ip6Address, w: *std.io.Writer) std.io.Writer.Error!void { + pub fn format(a: Ip6Address, w: *Io.Writer) Io.Writer.Error!void { const bytes = &a.bytes; if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { try w.print("[::ffff:{d}.{d}.{d}.{d}]:{d}", .{ diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index ebc3809878..e83e5a6251 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -17,25 +17,12 @@ const Alignment = std.mem.Alignment; /// The OS-specific file descriptor or file handle. handle: Handle, -pub const Handle = posix.fd_t; -pub const Mode = posix.mode_t; -pub const INode = posix.ino_t; +pub const Handle = std.Io.File.Handle; +pub const Mode = std.Io.File.Mode; +pub const INode = std.Io.File.INode; pub const Uid = posix.uid_t; pub const Gid = posix.gid_t; - -pub const Kind = enum { - block_device, - character_device, - directory, - named_pipe, - sym_link, - file, - unix_domain_socket, - whiteout, - door, - event_port, - unknown, -}; +pub const Kind = std.Io.File.Kind; /// This is the default mode given to POSIX operating systems for creating /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, @@ -399,115 +386,11 @@ pub fn mode(self: File) ModeError!Mode { return (try self.stat()).mode; } -pub const Stat = struct { - /// A number that the system uses to point to the file metadata. This - /// number is not guaranteed to be unique across time, as some file - /// systems may reuse an inode after its file has been deleted. Some - /// systems may change the inode of a file over time. - /// - /// On Linux, the inode is a structure that stores the metadata, and - /// the inode _number_ is what you see here: the index number of the - /// inode. - /// - /// The FileIndex on Windows is similar. It is a number for a file that - /// is unique to each filesystem. - inode: INode, - size: u64, - /// This is available on POSIX systems and is always 0 otherwise. - mode: Mode, - kind: Kind, - - /// Last access time in nanoseconds, relative to UTC 1970-01-01. - atime: i128, - /// Last modification time in nanoseconds, relative to UTC 1970-01-01. - mtime: i128, - /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. - ctime: i128, - - pub fn fromPosix(st: posix.Stat) Stat { - const atime = st.atime(); - const mtime = st.mtime(); - const ctime = st.ctime(); - return .{ - .inode = st.ino, - .size = @bitCast(st.size), - .mode = st.mode, - .kind = k: { - const m = st.mode & posix.S.IFMT; - switch (m) { - posix.S.IFBLK => break :k .block_device, - posix.S.IFCHR => break :k .character_device, - posix.S.IFDIR => break :k .directory, - posix.S.IFIFO => break :k .named_pipe, - posix.S.IFLNK => break :k .sym_link, - posix.S.IFREG => break :k .file, - posix.S.IFSOCK => break :k .unix_domain_socket, - else => {}, - } - if (builtin.os.tag == .illumos) switch (m) { - posix.S.IFDOOR => break :k .door, - posix.S.IFPORT => break :k .event_port, - else => {}, - }; - - break :k .unknown; - }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, - }; - } - - pub fn fromLinux(stx: linux.Statx) Stat { - const atime = stx.atime; - const mtime = stx.mtime; - const ctime = stx.ctime; - - return .{ - .inode = stx.ino, - .size = stx.size, - .mode = stx.mode, - .kind = switch (stx.mode & linux.S.IFMT) { - linux.S.IFDIR => .directory, - linux.S.IFCHR => .character_device, - linux.S.IFBLK => .block_device, - linux.S.IFREG => .file, - linux.S.IFIFO => .named_pipe, - linux.S.IFLNK => .sym_link, - linux.S.IFSOCK => .unix_domain_socket, - else => .unknown, - }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, - }; - } - - pub fn fromWasi(st: std.os.wasi.filestat_t) Stat { - return .{ - .inode = st.ino, - .size = @bitCast(st.size), - .mode = 0, - .kind = switch (st.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - }, - .atime = st.atim, - .mtime = st.mtim, - .ctime = st.ctim, - }; - } -}; +pub const Stat = std.Io.File.Stat; pub const StatError = posix.FStatError; /// Returns `Stat` containing basic information about the `File`. -/// TODO: integrate with async I/O pub fn stat(self: File) StatError!Stat { if (builtin.os.tag == .windows) { var io_status_block: windows.IO_STATUS_BLOCK = undefined; @@ -1728,7 +1611,7 @@ pub const Writer = struct { pub fn sendFile( io_w: *std.Io.Writer, - file_reader: *Reader, + file_reader: *std.Io.File.Reader, limit: std.Io.Limit, ) std.Io.Writer.FileError!usize { const reader_buffered = file_reader.interface.buffered(); @@ -1995,7 +1878,7 @@ pub const Writer = struct { fn sendFileBuffered( io_w: *std.Io.Writer, - file_reader: *Reader, + file_reader: *std.Io.File.Reader, reader_buffered: []const u8, ) std.Io.Writer.FileError!usize { const n = try drain(io_w, &.{reader_buffered}, 1); diff --git a/lib/std/posix.zig b/lib/std/posix.zig index e602b47c7b..5498769d3b 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -805,36 +805,7 @@ pub fn exit(status: u8) noreturn { system.exit(status); } -pub const ReadError = error{ - InputOutput, - SystemResources, - IsDir, - OperationAborted, - BrokenPipe, - ConnectionResetByPeer, - ConnectionTimedOut, - NotOpenForReading, - SocketNotConnected, - - /// This error occurs when no global event loop is configured, - /// and reading from the file descriptor would block. - WouldBlock, - - /// reading a timerfd with CANCEL_ON_SET will lead to this error - /// when the clock goes through a discontinuous change - Canceled, - - /// In WASI, this error occurs when the file descriptor does - /// not hold the required rights to read from it. - AccessDenied, - - /// This error occurs in Linux if the process to be read from - /// no longer exists. - ProcessNotFound, - - /// Unable to read file due to lock. - LockViolation, -} || UnexpectedError; +pub const ReadError = std.Io.File.ReadStreamingError; /// Returns the number of bytes that were read, which can be less than /// buf.len. If 0 bytes were read, that means EOF. @@ -921,7 +892,6 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { /// a pointer within the address space of the application. pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { if (native_os == .windows) { - // TODO improve this to use ReadFileScatter if (iov.len == 0) return 0; const first = iov[0]; return read(fd, first.base[0..first.len]); @@ -969,7 +939,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { } } -pub const PReadError = ReadError || error{Unseekable}; +pub const PReadError = std.Io.ReadPositionalError; /// Number of bytes read is returned. Upon reading end-of-file, zero is returned. /// @@ -5393,13 +5363,7 @@ pub fn gettimeofday(tv: ?*timeval, tz: ?*timezone) void { } } -pub const SeekError = error{ - Unseekable, - - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to seek on it. - AccessDenied, -} || UnexpectedError; +pub const SeekError = std.Io.File.SeekError; /// Repositions read/write file offset relative to the beginning. pub fn lseek_SET(fd: fd_t, offset: u64) SeekError!void { @@ -7572,7 +7536,7 @@ pub fn ioctl_SIOCGIFINDEX(fd: fd_t, ifr: *ifreq) IoCtl_SIOCGIFINDEX_Error!void { } } -const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid()); +pub const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid()); /// Whether or not `error.Unexpected` will print its value and a stack trace. /// @@ -7584,17 +7548,7 @@ pub const unexpected_error_tracing = builtin.mode == .Debug and switch (builtin. else => false, }; -pub const UnexpectedError = error{ - /// The Operating System returned an undocumented error code. - /// - /// This error is in theory not possible, but it would be better - /// to handle this error than to invoke undefined behavior. - /// - /// When this error code is observed, it usually means the Zig Standard - /// Library needs a small patch to add the error code to the error set for - /// the respective function. - Unexpected, -}; +pub const UnexpectedError = std.Io.UnexpectedError; /// Call this when you made a syscall or something that sets errno /// and you get an unexpected error. From b4e5e9d48f4df3849edf768cea2910a976c916a7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 7 Sep 2025 00:20:30 -0700 Subject: [PATCH 050/244] Io.net: implement sortLookupResults --- lib/std/Io/net.zig | 155 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index b9ddccff3e..6f351780a1 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -132,9 +132,58 @@ pub const HostName = struct { } fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult { - _ = options; - _ = result; - @panic("TODO"); + const addresses = options.addresses_buffer[0..result.addresses_len]; + // No further processing is needed if there are fewer than 2 results or + // if there are only IPv4 results. + if (addresses.len < 2) return result; + const all_ip4 = for (addresses) |a| switch (a) { + .ip4 => continue, + .ip6 => break false, + } else true; + if (all_ip4) return result; + + // RFC 3484/6724 describes how destination address selection is + // supposed to work. However, to implement it requires making a bunch + // of networking syscalls, which is unnecessarily high latency, + // especially if implemented serially. Furthermore, rules 3, 4, and 7 + // have excessive runtime and code size cost and dubious benefit. + // + // Therefore, this logic sorts only using values available without + // doing any syscalls, relying on the calling code to have a + // meta-strategy such as attempting connection to multiple results at + // once and keeping the fastest response while canceling the others. + + const S = struct { + pub fn lessThan(s: @This(), lhs: IpAddress, rhs: IpAddress) bool { + return sortKey(s, lhs) < sortKey(s, rhs); + } + + fn sortKey(s: @This(), a: IpAddress) i32 { + _ = s; + var da6: Ip6Address = .{ + .port = 65535, + .bytes = undefined, + }; + switch (a) { + .ip6 => |ip6| { + da6.bytes = ip6.bytes; + da6.scope_id = ip6.scope_id; + }, + .ip4 => |ip4| { + da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; + da6.bytes[12..].* = ip4.bytes; + }, + } + const da6_scope: i32 = da6.scope(); + const da6_prec: i32 = da6.policy().prec; + var key: i32 = 0; + key |= da6_prec << 20; + key |= (15 - da6_scope) << 16; + return key; + } + }; + std.mem.sort(IpAddress, addresses, @as(S, .{}), S.lessThan); + return result; } fn lookupDns(io: Io, options: LookupOptions) !LookupResult { @@ -406,6 +455,14 @@ pub const Ip6Address = struct { Incomplete, }; + pub const Policy = struct { + addr: [16]u8, + len: u8, + mask: u8, + prec: u8, + label: u8, + }; + pub fn localhost(port: u16) Ip6Address { return .{ .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, @@ -597,6 +654,98 @@ pub const Ip6Address = struct { pub fn eql(a: Ip6Address, b: Ip6Address) bool { return a.port == b.port and std.mem.eql(u8, &a.bytes, &b.bytes); } + + pub fn isMultiCast(a: Ip6Address) bool { + return a.bytes[0] == 0xff; + } + + pub fn isLinkLocal(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0xfe and (b[1] & 0xc0) == 0x80; + } + + pub fn isLoopBack(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0 and b[1] == 0 and + b[2] == 0 and + b[12] == 0 and b[13] == 0 and + b[14] == 0 and b[15] == 1; + } + + pub fn isSiteLocal(a: Ip6Address) bool { + const b = &a.bytes; + return b[0] == 0xfe and (b[1] & 0xc0) == 0xc0; + } + + pub fn policy(a: Ip6Address) *const Policy { + const b = &a.bytes; + for (&defined_policies) |*p| { + if (!std.mem.eql(u8, b[0..p.len], p.addr[0..p.len])) continue; + if ((b[p.len] & p.mask) != p.addr[p.len]) continue; + return p; + } + unreachable; + } + + pub fn scope(a: Ip6Address) u8 { + if (isMultiCast(a)) return a.bytes[1] & 15; + if (isLinkLocal(a)) return 2; + if (isLoopBack(a)) return 2; + if (isSiteLocal(a)) return 5; + return 14; + } + + const defined_policies = [_]Policy{ + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01".*, + .len = 15, + .mask = 0xff, + .prec = 50, + .label = 0, + }, + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00".*, + .len = 11, + .mask = 0xff, + .prec = 35, + .label = 4, + }, + .{ + .addr = "\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 1, + .mask = 0xff, + .prec = 30, + .label = 2, + }, + .{ + .addr = "\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 3, + .mask = 0xff, + .prec = 5, + .label = 5, + }, + .{ + .addr = "\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 0, + .mask = 0xfe, + .prec = 3, + .label = 13, + }, + // These are deprecated and/or returned to the address + // pool, so despite the RFC, treating them as special + // is probably wrong. + // { "", 11, 0xff, 1, 3 }, + // { "\xfe\xc0", 1, 0xc0, 1, 11 }, + // { "\x3f\xfe", 1, 0xff, 1, 12 }, + // Last rule must match all addresses to stop loop. + .{ + .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, + .len = 0, + .mask = 0, + .prec = 40, + .label = 1, + }, + }; }; pub const Stream = struct { From fc1e3d5bc9f4279ae6cd19577bb443aff8d4ccb6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 8 Sep 2025 01:06:00 -0700 Subject: [PATCH 051/244] Io.net: partial implementation of dns lookup --- lib/std/Io.zig | 2 + lib/std/Io/ThreadPool.zig | 71 ++++ lib/std/Io/net.zig | 306 ++--------------- lib/std/Io/net/HostName.zig | 631 ++++++++++++++++++++++++++++++++++++ lib/std/mem.zig | 16 +- lib/std/net.zig | 10 +- lib/std/net/test.zig | 8 +- lib/std/posix.zig | 99 ------ 8 files changed, 742 insertions(+), 401 deletions(-) create mode 100644 lib/std/Io/net/HostName.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 7db81ae76b..0d27af1075 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -667,6 +667,8 @@ pub const VTable = struct { netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, stream: net.Stream) void, + /// Equivalent to libc "if_nametoindex". + netInterfaceIndex: *const fn (?*anyopaque, name: []const u8) net.InterfaceIndexError!u32, }; pub const Cancelable = error{ diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/ThreadPool.zig index 6ba1249669..619525435f 100644 --- a/lib/std/Io/ThreadPool.zig +++ b/lib/std/Io/ThreadPool.zig @@ -135,6 +135,7 @@ pub fn io(pool: *Pool) Io { else => netWritePosix, }, .netClose = netClose, + .netInterfaceIndex = netInterfaceIndex, }, }; } @@ -1122,6 +1123,69 @@ fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void { return net_stream.close(); } +fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIndexError!u32 { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + if (native_os == .linux) { + if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound; + var ifr: posix.ifreq = undefined; + @memcpy(ifr.ifrn.name[0..name.len], name); + ifr.ifrn.name[name.len] = 0; + + const rc = posix.system.socket(posix.AF.UNIX, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0); + const sock_fd: posix.fd_t = switch (posix.errno(rc)) { + .SUCCESS => @intCast(rc), + .ACCES => return error.AccessDenied, + .MFILE => return error.SystemResources, + .NFILE => return error.SystemResources, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + }; + defer posix.close(sock_fd); + + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { + .SUCCESS => return @bitCast(ifr.ifru.ivalue), + .INVAL => |err| return badErrno(err), // Bad parameters. + .NOTTY => |err| return badErrno(err), + .NXIO => |err| return badErrno(err), + .BADF => |err| return badErrno(err), // Always a race condition. + .FAULT => |err| return badErrno(err), // Bad pointer parameter. + .INTR => continue, + .IO => |err| return badErrno(err), // sock_fd is not a file descriptor + .NODEV => return error.InterfaceNotFound, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (native_os.isDarwin()) { + if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound; + var if_name: [posix.IFNAMESIZE:0]u8 = undefined; + @memcpy(if_name[0..name.len], name); + if_name[name.len] = 0; + const if_slice = if_name[0..name.len :0]; + const index = std.c.if_nametoindex(if_slice); + if (index == 0) return error.InterfaceNotFound; + return @bitCast(index); + } + + if (native_os == .windows) { + if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound; + var interface_name: [posix.IFNAMESIZE:0]u8 = undefined; + @memcpy(interface_name[0..name.len], name); + interface_name[name.len] = 0; + const index = std.os.windows.ws2_32.if_nametoindex(@as([*:0]const u8, &interface_name)); + if (index == 0) return error.InterfaceNotFound; + return index; + } + + @compileError("std.net.if_nametoindex unimplemented for this OS"); +} + const PosixAddress = extern union { any: posix.sockaddr, in: posix.sockaddr.in, @@ -1187,3 +1251,10 @@ fn address6ToPosix(a: Io.net.Ip6Address) posix.sockaddr.in6 { .scope_id = a.scope_id, }; } + +fn badErrno(err: posix.E) Io.UnexpectedError { + switch (builtin.mode) { + .Debug => std.debug.panic("programmer bug caused syscall error: {t}", .{err}), + else => return error.Unexpected, + } +} diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 6f351780a1..aa5032ada8 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -4,6 +4,8 @@ const std = @import("../std.zig"); const Io = std.Io; const assert = std.debug.assert; +pub const HostName = @import("net/HostName.zig"); + pub const ListenError = std.net.Address.ListenError || Io.Cancelable; pub const ListenOptions = struct { @@ -17,298 +19,17 @@ pub const ListenOptions = struct { force_nonblocking: bool = false, }; -/// An already-validated host name. A valid host name: -/// * Has length less than or equal to `max_len`. -/// * Is valid UTF-8. -/// * Lacks ASCII characters other than alphanumeric, '-', and '.'. -pub const HostName = struct { - /// Externally managed memory. Already checked to be valid. - bytes: []const u8, - - pub const max_len = 255; - - pub const InitError = error{ - NameTooLong, - InvalidHostName, - }; - - pub fn init(bytes: []const u8) InitError!HostName { - if (bytes.len > max_len) return error.NameTooLong; - if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; - for (bytes) |byte| { - if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { - continue; - } - return error.InvalidHostName; - } - return .{ .bytes = bytes }; - } - - pub const LookupOptions = struct { - port: u16, - /// Must have at least length 2. - addresses_buffer: []IpAddress, - /// If a buffer of at least `max_len` is not provided, `lookup` may - /// return successfully with zero-length `LookupResult.canonical_name_len`. - /// - /// Suggestion: if not interested in canonical name, pass an empty buffer; - /// otherwise pass a buffer of size `max_len`. - canonical_name_buffer: []u8, - /// `null` means either. - family: ?IpAddress.Tag = null, - }; - - pub const LookupError = Io.Cancelable || Io.File.OpenError || Io.File.Reader.Error || error{ - UnknownHostName, - }; - - pub const LookupResult = struct { - /// How many `LookupOptions.addresses_buffer` elements are populated. - addresses_len: usize = 0, - canonical_name: ?HostName = null, - }; - - pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { - const name = host_name.bytes; - assert(name.len <= max_len); - assert(options.addresses_buffer.len >= 2); - - if (native_os == .windows) @compileError("TODO"); - if (builtin.link_libc) @compileError("TODO"); - if (native_os == .linux) { - if (options.family != .ip6) { - if (IpAddress.parseIp4(name, options.port)) |addr| { - options.addresses_buffer[0] = addr; - return .{ .addresses_len = 1 }; - } else |_| {} - } - if (options.family != .ip4) { - if (IpAddress.parseIp6(name, options.port)) |addr| { - options.addresses_buffer[0] = addr; - return .{ .addresses_len = 1 }; - } else |_| {} - } - { - const result = try lookupHosts(host_name, io, options); - if (result.addresses_len > 0) return sortLookupResults(options, result); - } - { - // RFC 6761 Section 6.3.3 - // Name resolution APIs and libraries SHOULD recognize - // localhost names as special and SHOULD always return the IP - // loopback address for address queries and negative responses - // for all other query types. - - // Check for equal to "localhost(.)" or ends in ".localhost(.)" - const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; - if (std.mem.endsWith(u8, name, localhost) and - (name.len == localhost.len or name[name.len - localhost.len] == '.')) - { - var i: usize = 0; - if (options.family != .ip6) { - options.addresses_buffer[i] = .{ .ip4 = .localhost(options.port) }; - i += 1; - } - if (options.family != .ip4) { - options.addresses_buffer[i] = .{ .ip6 = .localhost(options.port) }; - i += 1; - } - const canon_name = "localhost"; - const canon_name_dest = options.canonical_name_buffer[0..canon_name.len]; - canon_name_dest.* = canon_name.*; - return sortLookupResults(options, .{ - .addresses_len = i, - .canonical_name = .{ .bytes = canon_name_dest }, - }); - } - } - { - const result = try lookupDns(io, options); - if (result.addresses_len > 0) return sortLookupResults(options, result); - } - return error.UnknownHostName; - } - @compileError("unimplemented"); - } - - fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult { - const addresses = options.addresses_buffer[0..result.addresses_len]; - // No further processing is needed if there are fewer than 2 results or - // if there are only IPv4 results. - if (addresses.len < 2) return result; - const all_ip4 = for (addresses) |a| switch (a) { - .ip4 => continue, - .ip6 => break false, - } else true; - if (all_ip4) return result; - - // RFC 3484/6724 describes how destination address selection is - // supposed to work. However, to implement it requires making a bunch - // of networking syscalls, which is unnecessarily high latency, - // especially if implemented serially. Furthermore, rules 3, 4, and 7 - // have excessive runtime and code size cost and dubious benefit. - // - // Therefore, this logic sorts only using values available without - // doing any syscalls, relying on the calling code to have a - // meta-strategy such as attempting connection to multiple results at - // once and keeping the fastest response while canceling the others. - - const S = struct { - pub fn lessThan(s: @This(), lhs: IpAddress, rhs: IpAddress) bool { - return sortKey(s, lhs) < sortKey(s, rhs); - } - - fn sortKey(s: @This(), a: IpAddress) i32 { - _ = s; - var da6: Ip6Address = .{ - .port = 65535, - .bytes = undefined, - }; - switch (a) { - .ip6 => |ip6| { - da6.bytes = ip6.bytes; - da6.scope_id = ip6.scope_id; - }, - .ip4 => |ip4| { - da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - da6.bytes[12..].* = ip4.bytes; - }, - } - const da6_scope: i32 = da6.scope(); - const da6_prec: i32 = da6.policy().prec; - var key: i32 = 0; - key |= da6_prec << 20; - key |= (15 - da6_scope) << 16; - return key; - } - }; - std.mem.sort(IpAddress, addresses, @as(S, .{}), S.lessThan); - return result; - } - - fn lookupDns(io: Io, options: LookupOptions) !LookupResult { - _ = io; - _ = options; - @panic("TODO"); - } - - fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { - const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) { - error.FileNotFound, - error.NotDir, - error.AccessDenied, - => return .{}, - - else => |e| return e, - }; - defer file.close(io); - - var line_buf: [512]u8 = undefined; - var file_reader = file.reader(io, &line_buf); - return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) { - error.ReadFailed => return file_reader.err.?, - }; - } - - fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult { - var addresses_len: usize = 0; - var canonical_name: ?HostName = null; - while (true) { - const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { - error.StreamTooLong => { - // Skip lines that are too long. - _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) { - error.EndOfStream => break, - error.ReadFailed => return error.ReadFailed, - }; - continue; - }, - error.ReadFailed => return error.ReadFailed, - error.EndOfStream => break, - }; - var split_it = std.mem.splitScalar(u8, line, '#'); - const no_comment_line = split_it.first(); - - var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t"); - const ip_text = line_it.next() orelse continue; - var first_name_text: ?[]const u8 = null; - while (line_it.next()) |name_text| { - if (std.mem.eql(u8, name_text, host_name.bytes)) { - if (first_name_text == null) first_name_text = name_text; - break; - } - } else continue; - - if (canonical_name == null) { - if (HostName.init(first_name_text.?)) |name_text| { - if (name_text.bytes.len <= options.canonical_name_buffer.len) { - const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len]; - @memcpy(canonical_name_dest, name_text.bytes); - canonical_name = .{ .bytes = canonical_name_dest }; - } - } else |_| {} - } - - if (options.family != .ip6) { - if (IpAddress.parseIp4(ip_text, options.port)) |addr| { - options.addresses_buffer[addresses_len] = addr; - addresses_len += 1; - if (options.addresses_buffer.len - addresses_len == 0) return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name, - }; - } else |_| {} - } - if (options.family != .ip4) { - if (IpAddress.parseIp6(ip_text, options.port)) |addr| { - options.addresses_buffer[addresses_len] = addr; - addresses_len += 1; - if (options.addresses_buffer.len - addresses_len == 0) return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name, - }; - } else |_| {} - } - } - return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name, - }; - } - - pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError; - - pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream { - var addresses_buffer: [32]IpAddress = undefined; - - const results = try lookup(host_name, .{ - .port = port, - .addresses_buffer = &addresses_buffer, - .canonical_name_buffer = &.{}, - }); - const addresses = addresses_buffer[0..results.addresses_len]; - - if (addresses.len == 0) return error.UnknownHostName; - - for (addresses) |addr| { - return addr.connectTcp(io) catch |err| switch (err) { - error.ConnectionRefused => continue, - else => |e| return e, - }; - } - return error.ConnectionRefused; - } -}; - pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, - pub const Tag = @typeInfo(IpAddress).@"union".tag_type.?; + pub const Family = @typeInfo(IpAddress).@"union".tag_type.?; /// Parse the given IP address string into an `IpAddress` value. pub fn parse(name: []const u8, port: u16) !IpAddress { - if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { + if (Ip4Address.parse(name, port)) |ip4| { + return .{ .ip4 = ip4 }; + } else |err| switch (err) { error.Overflow, error.InvalidEnd, error.InvalidCharacter, @@ -317,7 +38,9 @@ pub const IpAddress = union(enum) { => {}, } - if (parseIp6(name, port)) |ip6| return ip6 else |err| switch (err) { + if (Ip6Address.parse(name, port)) |ip6| { + return .{ .ip6 = ip6 }; + } else |err| switch (err) { error.Overflow, error.InvalidEnd, error.InvalidCharacter, @@ -859,3 +582,14 @@ pub const Server = struct { return io.vtable.accept(io, s); } }; + +pub const InterfaceIndexError = error{ + InterfaceNotFound, + AccessDenied, + SystemResources, +} || Io.UnexpectedError || Io.Cancelable; + +/// Otherwise known as "if_nametoindex". +pub fn interfaceIndex(io: Io, name: []const u8) InterfaceIndexError!u32 { + return io.vtable.netInterfaceIndex(io.userdata, name); +} diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig new file mode 100644 index 0000000000..a1a8b48d8e --- /dev/null +++ b/lib/std/Io/net/HostName.zig @@ -0,0 +1,631 @@ +//! An already-validated host name. A valid host name: +//! * Has length less than or equal to `max_len`. +//! * Is valid UTF-8. +//! * Lacks ASCII characters other than alphanumeric, '-', and '.'. +const HostName = @This(); + +const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../../std.zig"); +const Io = std.Io; +const IpAddress = Io.net.IpAddress; +const Ip6Address = Io.net.Ip6Address; +const assert = std.debug.assert; +const Stream = Io.net.Stream; + +/// Externally managed memory. Already checked to be valid. +bytes: []const u8, + +pub const max_len = 255; + +pub const InitError = error{ + NameTooLong, + InvalidHostName, +}; + +pub fn init(bytes: []const u8) InitError!HostName { + if (bytes.len > max_len) return error.NameTooLong; + if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; + for (bytes) |byte| { + if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { + continue; + } + return error.InvalidHostName; + } + return .{ .bytes = bytes }; +} + +/// TODO add a retry field here +pub const LookupOptions = struct { + port: u16, + /// Must have at least length 2. + addresses_buffer: []IpAddress, + canonical_name_buffer: *[max_len]u8, + /// `null` means either. + family: ?IpAddress.Family = null, +}; + +pub const LookupError = Io.Cancelable || Io.File.OpenError || Io.File.Reader.Error || error{ + UnknownHostName, + ResolvConfParseFailed, + // TODO remove from error set; retry a few times then report a different error + TemporaryNameServerFailure, + InvalidDnsARecord, + InvalidDnsAAAARecord, + NameServerFailure, +}; + +pub const LookupResult = struct { + /// How many `LookupOptions.addresses_buffer` elements are populated. + addresses_len: usize, + canonical_name: HostName, + + pub const empty: LookupResult = .{ + .addresses_len = 0, + .canonical_name = undefined, + }; +}; + +pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { + const name = host_name.bytes; + assert(name.len <= max_len); + assert(options.addresses_buffer.len >= 2); + + if (native_os == .windows) @compileError("TODO"); + if (builtin.link_libc) @compileError("TODO"); + if (native_os == .linux) { + if (options.family != .ip6) { + if (IpAddress.parseIp4(name, options.port)) |addr| { + options.addresses_buffer[0] = addr; + return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) }; + } else |_| {} + } + if (options.family != .ip4) { + if (IpAddress.parseIp6(name, options.port)) |addr| { + options.addresses_buffer[0] = addr; + return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) }; + } else |_| {} + } + { + const result = try lookupHosts(host_name, io, options); + if (result.addresses_len > 0) return sortLookupResults(options, result); + } + { + // RFC 6761 Section 6.3.3 + // Name resolution APIs and libraries SHOULD recognize + // localhost names as special and SHOULD always return the IP + // loopback address for address queries and negative responses + // for all other query types. + + // Check for equal to "localhost(.)" or ends in ".localhost(.)" + const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; + if (std.mem.endsWith(u8, name, localhost) and + (name.len == localhost.len or name[name.len - localhost.len] == '.')) + { + var i: usize = 0; + if (options.family != .ip6) { + options.addresses_buffer[i] = .{ .ip4 = .localhost(options.port) }; + i += 1; + } + if (options.family != .ip4) { + options.addresses_buffer[i] = .{ .ip6 = .localhost(options.port) }; + i += 1; + } + const canon_name = "localhost"; + const canon_name_dest = options.canonical_name_buffer[0..canon_name.len]; + canon_name_dest.* = canon_name.*; + return sortLookupResults(options, .{ + .addresses_len = i, + .canonical_name = .{ .bytes = canon_name_dest }, + }); + } + } + { + const result = try lookupDnsSearch(host_name, io, options); + if (result.addresses_len > 0) return sortLookupResults(options, result); + } + return error.UnknownHostName; + } + @compileError("unimplemented"); +} + +fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult { + const addresses = options.addresses_buffer[0..result.addresses_len]; + // No further processing is needed if there are fewer than 2 results or + // if there are only IPv4 results. + if (addresses.len < 2) return result; + const all_ip4 = for (addresses) |a| switch (a) { + .ip4 => continue, + .ip6 => break false, + } else true; + if (all_ip4) return result; + + // RFC 3484/6724 describes how destination address selection is + // supposed to work. However, to implement it requires making a bunch + // of networking syscalls, which is unnecessarily high latency, + // especially if implemented serially. Furthermore, rules 3, 4, and 7 + // have excessive runtime and code size cost and dubious benefit. + // + // Therefore, this logic sorts only using values available without + // doing any syscalls, relying on the calling code to have a + // meta-strategy such as attempting connection to multiple results at + // once and keeping the fastest response while canceling the others. + + const S = struct { + pub fn lessThan(s: @This(), lhs: IpAddress, rhs: IpAddress) bool { + return sortKey(s, lhs) < sortKey(s, rhs); + } + + fn sortKey(s: @This(), a: IpAddress) i32 { + _ = s; + var da6: Ip6Address = .{ + .port = 65535, + .bytes = undefined, + }; + switch (a) { + .ip6 => |ip6| { + da6.bytes = ip6.bytes; + da6.scope_id = ip6.scope_id; + }, + .ip4 => |ip4| { + da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; + da6.bytes[12..].* = ip4.bytes; + }, + } + const da6_scope: i32 = da6.scope(); + const da6_prec: i32 = da6.policy().prec; + var key: i32 = 0; + key |= da6_prec << 20; + key |= (15 - da6_scope) << 16; + return key; + } + }; + std.mem.sort(IpAddress, addresses, @as(S, .{}), S.lessThan); + return result; +} + +fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { + const rc = ResolvConf.init(io) catch return error.ResolvConfParseFailed; + + // Count dots, suppress search when >=ndots or name ends in + // a dot, which is an explicit request for global scope. + const dots = std.mem.countScalar(u8, host_name.bytes, '.'); + const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len; + const search = rc.search_buffer[0..search_len]; + + var canon_name = host_name.bytes; + + // Strip final dot for canon, fail if multiple trailing dots. + if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; + if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; + + // Name with search domain appended is set up in `canon_name`. This + // both provides the desired default canonical name (if the requested + // name is not a CNAME record) and serves as a buffer for passing the + // full requested name to `lookupDns`. + @memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name); + options.canonical_name_buffer[canon_name.len] = '.'; + var it = std.mem.tokenizeAny(u8, search, " \t"); + while (it.next()) |token| { + @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token); + const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len]; + const result = try lookupDns(io, lookup_canon_name, &rc, options); + if (result.addresses_len > 0) return sortLookupResults(options, result); + } + + const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len]; + return lookupDns(io, lookup_canon_name, &rc, options); +} + +fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) !LookupResult { + const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ + .{ .af = .ip6, .rr = std.posix.RR.A }, + .{ .af = .ip4, .rr = std.posix.RR.AAAA }, + }; + var query_buffers: [2][280]u8 = undefined; + var queries_buffer: [2][]const u8 = undefined; + var answer_buffers: [2][512]u8 = undefined; + var answers_buffer: [2][]u8 = .{ &answer_buffers[0], &answer_buffers[1] }; + var nq: usize = 0; + + for (family_records) |fr| { + if (options.family != fr.af) { + const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr); + queries_buffer[nq] = query_buffers[nq][0..len]; + nq += 1; + } + } + + const queries = queries_buffer[0..nq]; + const replies = answers_buffer[0..nq]; + try rc.sendMessage(io, queries, replies); + + for (replies) |reply| { + if (reply.len < 4 or (reply[3] & 15) == 2) return error.TemporaryNameServerFailure; + if ((reply[3] & 15) == 3) return .empty; + if ((reply[3] & 15) != 0) return error.UnknownHostName; + } + + var addresses_len: usize = 0; + var canonical_name: ?HostName = null; + + for (replies) |reply| { + var it = DnsResponse.init(reply) catch { + // TODO accept a diagnostics struct and append warnings + continue; + }; + while (it.next() catch { + // TODO accept a diagnostics struct and append warnings + continue; + }) |answer| switch (answer.rr) { + std.posix.RR.A => { + if (answer.data.len != 4) return error.InvalidDnsARecord; + options.addresses_buffer[addresses_len] = .{ .ip4 = .{ + .bytes = answer.data[0..4].*, + .port = options.port, + } }; + addresses_len += 1; + }, + std.posix.RR.AAAA => { + if (answer.data.len != 16) return error.InvalidDnsAAAARecord; + options.addresses_buffer[addresses_len] = .{ .ip6 = .{ + .bytes = answer.data[0..16].*, + .port = options.port, + } }; + addresses_len += 1; + }, + std.posix.RR.CNAME => { + _ = &canonical_name; + @panic("TODO"); + //var tmp: [256]u8 = undefined; + //// Returns len of compressed name. strlen to get canon name. + //_ = try posix.dn_expand(packet, answer.data, &tmp); + //const canon_name = mem.sliceTo(&tmp, 0); + //if (isValidHostName(canon_name)) { + // ctx.canon.items.len = 0; + // try ctx.canon.appendSlice(gpa, canon_name); + //} + }, + else => continue, + }; + } + + if (addresses_len != 0) return .{ + .addresses_len = addresses_len, + .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name }, + }; + + return error.NameServerFailure; +} + +fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { + const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => return .empty, + + else => |e| return e, + }; + defer file.close(io); + + var line_buf: [512]u8 = undefined; + var file_reader = file.reader(io, &line_buf); + return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; +} + +fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult { + var addresses_len: usize = 0; + var canonical_name: ?HostName = null; + while (true) { + const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { + error.StreamTooLong => { + // Skip lines that are too long. + _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) { + error.EndOfStream => break, + error.ReadFailed => return error.ReadFailed, + }; + continue; + }, + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => break, + }; + var split_it = std.mem.splitScalar(u8, line, '#'); + const no_comment_line = split_it.first(); + + var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t"); + const ip_text = line_it.next() orelse continue; + var first_name_text: ?[]const u8 = null; + while (line_it.next()) |name_text| { + if (std.mem.eql(u8, name_text, host_name.bytes)) { + if (first_name_text == null) first_name_text = name_text; + break; + } + } else continue; + + if (canonical_name == null) { + if (HostName.init(first_name_text.?)) |name_text| { + if (name_text.bytes.len <= options.canonical_name_buffer.len) { + const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len]; + @memcpy(canonical_name_dest, name_text.bytes); + canonical_name = .{ .bytes = canonical_name_dest }; + } + } else |_| {} + } + + if (options.family != .ip6) { + if (IpAddress.parseIp4(ip_text, options.port)) |addr| { + options.addresses_buffer[addresses_len] = addr; + addresses_len += 1; + if (options.addresses_buffer.len - addresses_len == 0) return .{ + .addresses_len = addresses_len, + .canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text), + }; + } else |_| {} + } + if (options.family != .ip4) { + if (IpAddress.parseIp6(ip_text, options.port)) |addr| { + options.addresses_buffer[addresses_len] = addr; + addresses_len += 1; + if (options.addresses_buffer.len - addresses_len == 0) return .{ + .addresses_len = addresses_len, + .canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text), + }; + } else |_| {} + } + } + if (canonical_name == null) assert(addresses_len == 0); + return .{ + .addresses_len = addresses_len, + .canonical_name = canonical_name orelse undefined, + }; +} + +fn copyCanon(canonical_name_buffer: *[max_len]u8, name: []const u8) HostName { + const dest = canonical_name_buffer[0..name.len]; + @memcpy(dest, name); + return .{ .bytes = dest }; +} + +/// Writes DNS resolution query packet data to `w`; at most 280 bytes. +fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8) usize { + // This implementation is ported from musl libc. + // A more idiomatic "ziggy" implementation would be welcome. + var name = dname; + if (std.mem.endsWith(u8, name, ".")) name.len -= 1; + assert(name.len <= 253); + const n = 17 + name.len + @intFromBool(name.len != 0); + + // Construct query template - ID will be filled later + @memset(q[0..n], 0); + q[2] = @as(u8, op) * 8 + 1; + q[5] = 1; + @memcpy(q[13..][0..name.len], name); + var i: usize = 13; + var j: usize = undefined; + while (q[i] != 0) : (i = j + 1) { + j = i; + while (q[j] != 0 and q[j] != '.') : (j += 1) {} + // TODO determine the circumstances for this and whether or + // not this should be an error. + if (j - i - 1 > 62) unreachable; + q[i - 1] = @intCast(j - i); + } + q[i + 1] = ty; + q[i + 3] = class; + + std.crypto.random.bytes(q[0..2]); + return n; +} + +pub const ExpandDomainNameError = error{InvalidDnsPacket}; + +pub fn expandDomainName( + msg: []const u8, + comp_dn: []const u8, + exp_dn: []u8, +) ExpandDomainNameError!usize { + // This implementation is ported from musl libc. + // A more idiomatic "ziggy" implementation would be welcome. + var p = comp_dn.ptr; + var len: usize = std.math.maxInt(usize); + const end = msg.ptr + msg.len; + if (p == end or exp_dn.len == 0) return error.InvalidDnsPacket; + var dest = exp_dn.ptr; + const dend = dest + @min(exp_dn.len, 254); + // detect reference loop using an iteration counter + var i: usize = 0; + while (i < msg.len) : (i += 2) { + // loop invariants: p= msg.len) return error.InvalidDnsPacket; + p = msg.ptr + j; + } else if (p[0] != 0) { + if (dest != exp_dn.ptr) { + dest[0] = '.'; + dest += 1; + } + var j = p[0]; + p += 1; + if (j >= @intFromPtr(end) - @intFromPtr(p) or j >= @intFromPtr(dend) - @intFromPtr(dest)) { + return error.InvalidDnsPacket; + } + while (j != 0) { + j -= 1; + dest[0] = p[0]; + dest += 1; + p += 1; + } + } else { + dest[0] = 0; + if (len == std.math.maxInt(usize)) len = @intFromPtr(p) + 1 - @intFromPtr(comp_dn.ptr); + return len; + } + } + return error.InvalidDnsPacket; +} + +pub const DnsResponse = struct { + bytes: []const u8, + + pub const Answer = struct { + rr: u8, + data: []const u8, + packet: []const u8, + }; + + pub const Error = error{InvalidDnsPacket}; + + pub fn init(r: []const u8) Error!DnsResponse { + if (r.len < 12) return error.InvalidDnsPacket; + return .{ .bytes = r }; + } + + pub fn next(dr: *DnsResponse) Error!?Answer { + _ = dr; + @panic("TODO"); + } +}; + +pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError; + +pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream { + var addresses_buffer: [32]IpAddress = undefined; + + const results = try lookup(host_name, .{ + .port = port, + .addresses_buffer = &addresses_buffer, + .canonical_name_buffer = &.{}, + }); + const addresses = addresses_buffer[0..results.addresses_len]; + + if (addresses.len == 0) return error.UnknownHostName; + + for (addresses) |addr| { + return addr.connectTcp(io) catch |err| switch (err) { + error.ConnectionRefused => continue, + else => |e| return e, + }; + } + return error.ConnectionRefused; +} + +pub const ResolvConf = struct { + attempts: u32, + ndots: u32, + timeout: u32, + nameservers_buffer: [3]IpAddress, + nameservers_len: usize, + search_buffer: [max_len]u8, + search_len: usize, + + /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. + fn init(io: Io) !ResolvConf { + var rc: ResolvConf = .{ + .nameservers_buffer = undefined, + .nameservers_len = 0, + .search_buffer = undefined, + .search_len = 0, + .ndots = 1, + .timeout = 5, + .attempts = 2, + }; + + const file = Io.File.openAbsolute(io, "/etc/resolv.conf", .{}) catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => { + try addNumeric(&rc, "127.0.0.1", 53); + return rc; + }, + + else => |e| return e, + }; + defer file.close(io); + + var line_buf: [512]u8 = undefined; + var file_reader = file.reader(io, &line_buf); + parse(&rc, &file_reader.interface) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + else => |e| return e, + }; + return rc; + } + + const Directive = enum { options, nameserver, domain, search }; + const Option = enum { ndots, attempts, timeout }; + + fn parse(rc: *ResolvConf, reader: *Io.Reader) !void { + while (reader.takeSentinel('\n')) |line_with_comment| { + const line = line: { + var split = std.mem.splitScalar(u8, line_with_comment, '#'); + break :line split.first(); + }; + var line_it = std.mem.tokenizeAny(u8, line, " \t"); + + const token = line_it.next() orelse continue; + switch (std.meta.stringToEnum(Directive, token) orelse continue) { + .options => while (line_it.next()) |sub_tok| { + var colon_it = std.mem.splitScalar(u8, sub_tok, ':'); + const name = colon_it.first(); + const value_txt = colon_it.next() orelse continue; + const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) { + error.Overflow => 255, + error.InvalidCharacter => continue, + }; + switch (std.meta.stringToEnum(Option, name) orelse continue) { + .ndots => rc.ndots = @min(value, 15), + .attempts => rc.attempts = @min(value, 10), + .timeout => rc.timeout = @min(value, 60), + } + }, + .nameserver => { + const ip_txt = line_it.next() orelse continue; + try addNumeric(rc, ip_txt, 53); + }, + .domain, .search => { + const rest = line_it.rest(); + @memcpy(rc.search_buffer[0..rest.len], rest); + rc.search_len = rest.len; + }, + } + } else |err| switch (err) { + error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream, + else => |e| return e, + } + + if (rc.nameservers_len == 0) { + try addNumeric(rc, "127.0.0.1", 53); + } + } + + fn addNumeric(rc: *ResolvConf, name: []const u8, port: u16) !void { + assert(rc.nameservers_len < rc.nameservers_buffer.len); + rc.nameservers_buffer[rc.nameservers_len] = try .parse(name, port); + rc.nameservers_len += 1; + } + + fn nameservers(rc: *const ResolvConf) []IpAddress { + return rc.nameservers_buffer[0..rc.nameservers_len]; + } + + fn sendMessage( + rc: *const ResolvConf, + io: Io, + queries: []const []const u8, + answers: [][]u8, + ) !void { + _ = rc; + _ = io; + _ = queries; + _ = answers; + @panic("TODO"); + } +}; diff --git a/lib/std/mem.zig b/lib/std/mem.zig index ed48132d9a..5042356db9 100644 --- a/lib/std/mem.zig +++ b/lib/std/mem.zig @@ -1678,6 +1678,7 @@ test "indexOfPos empty needle" { /// needle.len must be > 0 /// does not count overlapping needles pub fn count(comptime T: type, haystack: []const T, needle: []const T) usize { + if (needle.len == 1) return countScalar(T, haystack, needle[0]); assert(needle.len > 0); var i: usize = 0; var found: usize = 0; @@ -1704,9 +1705,9 @@ test count { try testing.expect(count(u8, "owowowu", "owowu") == 1); } -/// Returns the number of needles inside the haystack -pub fn countScalar(comptime T: type, haystack: []const T, needle: T) usize { - const n = haystack.len; +/// Returns the number of times `element` appears in a slice of memory. +pub fn countScalar(comptime T: type, list: []const T, element: T) usize { + const n = list.len; var i: usize = 0; var found: usize = 0; @@ -1716,16 +1717,16 @@ pub fn countScalar(comptime T: type, haystack: []const T, needle: T) usize { if (std.simd.suggestVectorLength(T)) |block_size| { const Block = @Vector(block_size, T); - const letter_mask: Block = @splat(needle); + const letter_mask: Block = @splat(element); while (n - i >= block_size) : (i += block_size) { - const haystack_block: Block = haystack[i..][0..block_size].*; + const haystack_block: Block = list[i..][0..block_size].*; found += std.simd.countTrues(letter_mask == haystack_block); } } } - for (haystack[i..n]) |item| { - found += @intFromBool(item == needle); + for (list[i..n]) |item| { + found += @intFromBool(item == element); } return found; @@ -1735,6 +1736,7 @@ test countScalar { try testing.expectEqual(0, countScalar(u8, "", 'h')); try testing.expectEqual(1, countScalar(u8, "h", 'h')); try testing.expectEqual(2, countScalar(u8, "hh", 'h')); + try testing.expectEqual(2, countScalar(u8, "ahhb", 'h')); try testing.expectEqual(3, countScalar(u8, " abcabc abc", 'b')); } diff --git a/lib/std/net.zig b/lib/std/net.zig index 7348be424a..5814334dd6 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -105,7 +105,7 @@ pub const Address = extern union { => {}, } - return error.InvalidIPAddressFormat; + return error.InvalidIpAddressFormat; } pub fn resolveIp(name: []const u8, port: u16) !Address { @@ -128,7 +128,7 @@ pub const Address = extern union { else => return err, } - return error.InvalidIPAddressFormat; + return error.InvalidIpAddressFormat; } pub fn parseExpectingFamily(name: []const u8, family: posix.sa_family_t, port: u16) !Address { @@ -360,7 +360,7 @@ pub const Ip4Address = extern struct { error.NonCanonical, => {}, } - return error.InvalidIPAddressFormat; + return error.InvalidIpAddressFormat; } pub fn init(addr: [4]u8, port: u16) Ip4Address { @@ -885,7 +885,7 @@ const GetAddressListError = Allocator.Error || File.OpenError || File.ReadError Overflow, Incomplete, InvalidIpv4Mapping, - InvalidIPAddressFormat, + InvalidIpAddressFormat, InterfaceNotFound, FileSystem, @@ -1427,7 +1427,7 @@ fn parseHosts( error.InvalidEnd, error.InvalidCharacter, error.Incomplete, - error.InvalidIPAddressFormat, + error.InvalidIpAddressFormat, error.InvalidIpv4Mapping, error.NonCanonical, => continue, diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig index bfafbe6044..cf736591fc 100644 --- a/lib/std/net/test.zig +++ b/lib/std/net/test.zig @@ -12,10 +12,10 @@ test "parse and render IP addresses at comptime" { const ipv4addr = net.Address.parseIp("127.0.0.1", 0) catch unreachable; try std.testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.parseIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.parseIp("127.01.0.1", 0)); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.resolveIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIPAddressFormat, net.Address.resolveIp("127.01.0.1", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.Address.parseIp("::123.123.123.123", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.Address.parseIp("127.01.0.1", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.Address.resolveIp("::123.123.123.123", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.Address.resolveIp("127.01.0.1", 0)); } } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 5498769d3b..431aded9fe 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -6116,55 +6116,6 @@ pub fn uname() utsname { } } -pub fn res_mkquery( - op: u4, - dname: []const u8, - class: u8, - ty: u8, - data: []const u8, - newrr: ?[*]const u8, - buf: []u8, -) usize { - _ = data; - _ = newrr; - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - var name = dname; - if (mem.endsWith(u8, name, ".")) name.len -= 1; - assert(name.len <= 253); - const n = 17 + name.len + @intFromBool(name.len != 0); - - // Construct query template - ID will be filled later - var q: [280]u8 = undefined; - @memset(q[0..n], 0); - q[2] = @as(u8, op) * 8 + 1; - q[5] = 1; - @memcpy(q[13..][0..name.len], name); - var i: usize = 13; - var j: usize = undefined; - while (q[i] != 0) : (i = j + 1) { - j = i; - while (q[j] != 0 and q[j] != '.') : (j += 1) {} - // TODO determine the circumstances for this and whether or - // not this should be an error. - if (j - i - 1 > 62) unreachable; - q[i - 1] = @intCast(j - i); - } - q[i + 1] = ty; - q[i + 3] = class; - - // Make a reasonably unpredictable id - const ts = clock_gettime(.REALTIME) catch unreachable; - const UInt = std.meta.Int(.unsigned, @bitSizeOf(@TypeOf(ts.nsec))); - const unsec: UInt = @bitCast(ts.nsec); - const id: u32 = @truncate(unsec + unsec / 65536); - q[0] = @truncate(id / 256); - q[1] = @truncate(id); - - @memcpy(buf[0..n], q[0..n]); - return n; -} - pub const SendError = error{ /// (For UNIX domain sockets, which are identified by pathname) Write permission is denied /// on the destination socket file, or search permission is denied for one of the @@ -6736,56 +6687,6 @@ pub fn recvmsg( } } -pub const DnExpandError = error{InvalidDnsPacket}; - -pub fn dn_expand( - msg: []const u8, - comp_dn: []const u8, - exp_dn: []u8, -) DnExpandError!usize { - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - var p = comp_dn.ptr; - var len: usize = maxInt(usize); - const end = msg.ptr + msg.len; - if (p == end or exp_dn.len == 0) return error.InvalidDnsPacket; - var dest = exp_dn.ptr; - const dend = dest + @min(exp_dn.len, 254); - // detect reference loop using an iteration counter - var i: usize = 0; - while (i < msg.len) : (i += 2) { - // loop invariants: p= msg.len) return error.InvalidDnsPacket; - p = msg.ptr + j; - } else if (p[0] != 0) { - if (dest != exp_dn.ptr) { - dest[0] = '.'; - dest += 1; - } - var j = p[0]; - p += 1; - if (j >= @intFromPtr(end) - @intFromPtr(p) or j >= @intFromPtr(dend) - @intFromPtr(dest)) { - return error.InvalidDnsPacket; - } - while (j != 0) { - j -= 1; - dest[0] = p[0]; - dest += 1; - p += 1; - } - } else { - dest[0] = 0; - if (len == maxInt(usize)) len = @intFromPtr(p) + 1 - @intFromPtr(comp_dn.ptr); - return len; - } - } - return error.InvalidDnsPacket; -} - pub const SetSockOptError = error{ /// The socket is already connected, and a specified option cannot be set while the socket is connected. AlreadyConnected, From 0e9280ef1a80e8fdeaec40a41b9cb6f2c2d4a490 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 10 Sep 2025 22:34:19 -0700 Subject: [PATCH 052/244] std.Io: extract Dir to separate file --- lib/std/Io.zig | 38 +----------- lib/std/Io/Dir.zig | 113 ++++++++++++++++++++++++++++++++++++ lib/std/Io/File.zig | 4 +- lib/std/Io/net.zig | 4 ++ lib/std/Io/net/HostName.zig | 25 ++++++++ lib/std/fs.zig | 24 ++++---- lib/std/fs/Dir.zig | 68 ---------------------- lib/std/fs/File.zig | 8 +-- lib/std/posix.zig | 2 +- 9 files changed, 162 insertions(+), 124 deletions(-) create mode 100644 lib/std/Io/Dir.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 0d27af1075..0d71727f3f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -548,6 +548,7 @@ pub fn PollFiles(comptime StreamEnum: type) type { } test { + _ = net; _ = Reader; _ = Writer; _ = tty; @@ -688,42 +689,7 @@ pub const UnexpectedError = error{ Unexpected, }; -pub const Dir = struct { - handle: Handle, - - pub fn cwd() Dir { - return .{ .handle = std.fs.cwd().fd }; - } - - pub const Handle = std.posix.fd_t; - - pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); - } - - pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - return io.vtable.createFile(io.userdata, dir, sub_path, flags); - } - - pub const WriteFileOptions = struct { - /// On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). - /// On WASI, `sub_path` should be encoded as valid UTF-8. - /// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. - sub_path: []const u8, - data: []const u8, - flags: File.CreateFlags = .{}, - }; - - pub const WriteFileError = File.WriteError || File.OpenError || Cancelable; - - /// Writes content to the file system, using the file creation flags provided. - pub fn writeFile(dir: Dir, io: Io, options: WriteFileOptions) WriteFileError!void { - var file = try dir.createFile(io, options.sub_path, options.flags); - defer file.close(io); - try file.writeAll(io, options.data); - } -}; - +pub const Dir = @import("Io/Dir.zig"); pub const File = @import("Io/File.zig"); pub const Timestamp = enum(i96) { diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig new file mode 100644 index 0000000000..a2ae96210e --- /dev/null +++ b/lib/std/Io/Dir.zig @@ -0,0 +1,113 @@ +const Dir = @This(); + +const std = @import("../std.zig"); +const Io = std.Io; +const File = Io.File; + +handle: Handle, + +pub fn cwd() Dir { + return .{ .handle = std.fs.cwd().fd }; +} + +pub const Handle = std.posix.fd_t; + +pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); +} + +pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + return io.vtable.createFile(io.userdata, dir, sub_path, flags); +} + +pub const WriteFileOptions = struct { + /// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). + /// On WASI, `sub_path` should be encoded as valid UTF-8. + /// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. + sub_path: []const u8, + data: []const u8, + flags: File.CreateFlags = .{}, +}; + +pub const WriteFileError = File.WriteError || File.OpenError || Io.Cancelable; + +/// Writes content to the file system, using the file creation flags provided. +pub fn writeFile(dir: Dir, io: Io, options: WriteFileOptions) WriteFileError!void { + var file = try dir.createFile(io, options.sub_path, options.flags); + defer file.close(io); + try file.writeAll(io, options.data); +} + +pub const PrevStatus = enum { + stale, + fresh, +}; + +pub const UpdateFileError = File.OpenError; + +/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If +/// they are equal, does nothing. Otherwise, atomically copies `source_path` to +/// `dest_path`. The destination file gains the mtime, atime, and mode of the +/// source file so that the next call to `updateFile` will not need a copy. +/// +/// Returns the previous status of the file before updating. +/// +/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// * On WASI, both paths should be encoded as valid UTF-8. +/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding. +pub fn updateFile( + source_dir: Dir, + io: Io, + source_path: []const u8, + dest_dir: Dir, + /// If directories in this path do not exist, they are created. + dest_path: []const u8, + options: std.fs.Dir.CopyFileOptions, +) !PrevStatus { + var src_file = try source_dir.openFile(io, source_path, .{}); + defer src_file.close(); + + const src_stat = try src_file.stat(io); + const actual_mode = options.override_mode orelse src_stat.mode; + check_dest_stat: { + const dest_stat = blk: { + var dest_file = dest_dir.openFile(io, dest_path, .{}) catch |err| switch (err) { + error.FileNotFound => break :check_dest_stat, + else => |e| return e, + }; + defer dest_file.close(io); + + break :blk try dest_file.stat(io); + }; + + if (src_stat.size == dest_stat.size and + src_stat.mtime == dest_stat.mtime and + actual_mode == dest_stat.mode) + { + return .fresh; + } + } + + if (std.fs.path.dirname(dest_path)) |dirname| { + try dest_dir.makePath(io, dirname); + } + + var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available. + var atomic_file = try dest_dir.atomicFile(io, dest_path, .{ + .mode = actual_mode, + .write_buffer = &buffer, + }); + defer atomic_file.deinit(); + + var src_reader: File.Reader = .initSize(io, src_file, &.{}, src_stat.size); + const dest_writer = &atomic_file.file_writer.interface; + + _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) { + error.ReadFailed => return src_reader.err.?, + error.WriteFailed => return atomic_file.file_writer.err.?, + }; + try atomic_file.flush(); + try atomic_file.file_writer.file.updateTimes(src_stat.atime, src_stat.mtime); + try atomic_file.renameIntoPlace(); + return .stale; +} diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index b8db947f58..63381fe99c 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -1,7 +1,9 @@ +const File = @This(); + const builtin = @import("builtin"); + const std = @import("../std.zig"); const Io = std.Io; -const File = @This(); const assert = std.debug.assert; handle: Handle, diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index aa5032ada8..0881fdc30c 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -593,3 +593,7 @@ pub const InterfaceIndexError = error{ pub fn interfaceIndex(io: Io, name: []const u8) InterfaceIndexError!u32 { return io.vtable.netInterfaceIndex(io.userdata, name); } + +test { + _ = HostName; +} diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index a1a8b48d8e..06cf4219b3 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -629,3 +629,28 @@ pub const ResolvConf = struct { @panic("TODO"); } }; + +test ResolvConf { + const input = + \\# Generated by resolvconf + \\nameserver 1.0.0.1 + \\nameserver 1.1.1.1 + \\nameserver fe80::e0e:76ff:fed4:cf22%eno1 + \\options edns0 + \\ + ; + var reader: Io.Reader = .fixed(input); + + var rc: ResolvConf = .{ + .nameservers_buffer = undefined, + .nameservers_len = 0, + .search_buffer = undefined, + .search_len = 0, + .ndots = 1, + .timeout = 5, + .attempts = 2, + }; + + try rc.parse(&reader); + try std.testing.expect(false); +} diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 652480914f..0470ee4e2a 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -107,13 +107,15 @@ pub fn updateFileAbsolute( source_path: []const u8, dest_path: []const u8, args: Dir.CopyFileOptions, -) !Dir.PrevStatus { +) !std.Io.Dir.PrevStatus { assert(path.isAbsolute(source_path)); assert(path.isAbsolute(dest_path)); const my_cwd = cwd(); return Dir.updateFile(my_cwd, source_path, my_cwd, dest_path, args); } +test updateFileAbsolute {} + /// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` /// are absolute. See `Dir.copyFile` for a function that operates on both /// absolute and relative paths. @@ -131,6 +133,8 @@ pub fn copyFileAbsolute( return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args); } +test copyFileAbsolute {} + /// Create a new directory, based on an absolute path. /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates /// on both absolute and relative paths. @@ -142,12 +146,16 @@ pub fn makeDirAbsolute(absolute_path: []const u8) !void { return posix.mkdir(absolute_path, Dir.default_mode); } +test makeDirAbsolute {} + /// Same as `makeDirAbsolute` except the parameter is null-terminated. pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { assert(path.isAbsoluteZ(absolute_path_z)); return posix.mkdirZ(absolute_path_z, Dir.default_mode); } +test makeDirAbsoluteZ {} + /// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16 LE-encoded string. pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { assert(path.isAbsoluteWindowsW(absolute_path_w)); @@ -702,16 +710,10 @@ pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { } test { - if (native_os != .wasi) { - _ = &makeDirAbsolute; - _ = &makeDirAbsoluteZ; - _ = ©FileAbsolute; - _ = &updateFileAbsolute; - } - _ = &AtomicFile; - _ = &Dir; - _ = &File; - _ = &path; + _ = AtomicFile; + _ = Dir; + _ = File; + _ = path; _ = @import("fs/test.zig"); _ = @import("fs/get_app_data_dir.zig"); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 14b26c89bd..3e3577bbf0 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2630,74 +2630,6 @@ pub const CopyFileOptions = struct { override_mode: ?File.Mode = null, }; -pub const PrevStatus = enum { - stale, - fresh, -}; - -/// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. -/// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, -/// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. -/// Returns the previous status of the file before updating. -/// If any of the directories do not exist for dest_path, they are created. -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. -pub fn updateFile( - source_dir: Dir, - source_path: []const u8, - dest_dir: Dir, - dest_path: []const u8, - options: CopyFileOptions, -) !PrevStatus { - var src_file = try source_dir.openFile(source_path, .{}); - defer src_file.close(); - - const src_stat = try src_file.stat(); - const actual_mode = options.override_mode orelse src_stat.mode; - check_dest_stat: { - const dest_stat = blk: { - var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { - error.FileNotFound => break :check_dest_stat, - else => |e| return e, - }; - defer dest_file.close(); - - break :blk try dest_file.stat(); - }; - - if (src_stat.size == dest_stat.size and - src_stat.mtime == dest_stat.mtime and - actual_mode == dest_stat.mode) - { - return PrevStatus.fresh; - } - } - - if (fs.path.dirname(dest_path)) |dirname| { - try dest_dir.makePath(dirname); - } - - var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available. - var atomic_file = try dest_dir.atomicFile(dest_path, .{ - .mode = actual_mode, - .write_buffer = &buffer, - }); - defer atomic_file.deinit(); - - var src_reader: File.Reader = .initSize(src_file, &.{}, src_stat.size); - const dest_writer = &atomic_file.file_writer.interface; - - _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) { - error.ReadFailed => return src_reader.err.?, - error.WriteFailed => return atomic_file.file_writer.err.?, - }; - try atomic_file.flush(); - try atomic_file.file_writer.file.updateTimes(src_stat.atime, src_stat.mtime); - try atomic_file.renameIntoPlace(); - return .stale; -} - pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || AtomicFile.FinishError || File.ReadError || File.WriteError; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index e83e5a6251..b79ef44d3b 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -1144,13 +1144,7 @@ pub const Reader = struct { fn stream(io_reader: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); switch (r.mode) { - .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { - error.Unimplemented => { - r.mode = r.mode.toReading(); - return 0; - }, - else => |e| return e, - }, + .positional, .streaming => @panic("TODO"), .positional_reading => { const dest = limit.slice(try w.writableSliceGreedy(1)); var data: [1][]u8 = .{dest}; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 431aded9fe..f97b0574af 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -939,7 +939,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { } } -pub const PReadError = std.Io.ReadPositionalError; +pub const PReadError = std.Io.File.ReadPositionalError; /// Number of bytes read is returned. Upon reading end-of-file, zero is returned. /// From 5089352b864e9b32345619392a49f6a33db1025e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 21 Sep 2025 17:26:00 -0700 Subject: [PATCH 053/244] std.Io: rename ThreadPool to Threaded --- lib/std/Io.zig | 6 +++--- lib/std/Io/{ThreadPool.zig => Threaded.zig} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename lib/std/Io/{ThreadPool.zig => Threaded.zig} (100%) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 0d71727f3f..4d10afbafe 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -558,7 +558,7 @@ test { const Io = @This(); pub const EventLoop = @import("Io/EventLoop.zig"); -pub const ThreadPool = @import("Io/ThreadPool.zig"); +pub const Threaded = @import("Io/Threaded.zig"); pub const net = @import("Io/net.zig"); userdata: ?*anyopaque, @@ -668,8 +668,8 @@ pub const VTable = struct { netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, stream: net.Stream) void, - /// Equivalent to libc "if_nametoindex". - netInterfaceIndex: *const fn (?*anyopaque, name: []const u8) net.InterfaceIndexError!u32, + netInterfaceNameResolve: *const fn (?*anyopaque, net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, + netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, }; pub const Cancelable = error{ diff --git a/lib/std/Io/ThreadPool.zig b/lib/std/Io/Threaded.zig similarity index 100% rename from lib/std/Io/ThreadPool.zig rename to lib/std/Io/Threaded.zig From d776a6bbbe69618d5a8d2583eda47d98755163f9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 21 Sep 2025 21:13:26 -0700 Subject: [PATCH 054/244] Io.net: rework IPv6 parsing and printing extract pure functional logic into pure functions and then layer the scope crap on top properly the formatting code incorrectly didn't do the reverse operation (if_indextoname). fix that with some TODO panics --- lib/std/Io/Threaded.zig | 68 ++--- lib/std/Io/net.zig | 487 +++++++++++++++++++++--------------- lib/std/Io/net/HostName.zig | 6 +- 3 files changed, 326 insertions(+), 235 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 619525435f..1ac56469df 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -135,7 +135,8 @@ pub fn io(pool: *Pool) Io { else => netWritePosix, }, .netClose = netClose, - .netInterfaceIndex = netInterfaceIndex, + .netInterfaceNameResolve = netInterfaceNameResolve, + .netInterfaceName = netInterfaceName, }, }; } @@ -1123,16 +1124,11 @@ fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void { return net_stream.close(); } -fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIndexError!u32 { +fn netInterfaceNameResolve(userdata: ?*anyopaque, name: Io.net.Interface.Name) Io.net.Interface.Name.ResolveError!Io.net.Interface { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); if (native_os == .linux) { - if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound; - var ifr: posix.ifreq = undefined; - @memcpy(ifr.ifrn.name[0..name.len], name); - ifr.ifrn.name[name.len] = 0; - const rc = posix.system.socket(posix.AF.UNIX, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0); const sock_fd: posix.fd_t = switch (posix.errno(rc)) { .SUCCESS => @intCast(rc), @@ -1145,10 +1141,15 @@ fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIn }; defer posix.close(sock_fd); + var ifr: posix.ifreq = .{ + .ifrn = .{ .name = @bitCast(name.bytes) }, + .ifru = undefined, + }; + while (true) { try pool.checkCancel(); switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { - .SUCCESS => return @bitCast(ifr.ifru.ivalue), + .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) }, .INVAL => |err| return badErrno(err), // Bad parameters. .NOTTY => |err| return badErrno(err), .NXIO => |err| return badErrno(err), @@ -1162,28 +1163,39 @@ fn netInterfaceIndex(userdata: ?*anyopaque, name: []const u8) Io.net.InterfaceIn } } - if (native_os.isDarwin()) { - if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound; - var if_name: [posix.IFNAMESIZE:0]u8 = undefined; - @memcpy(if_name[0..name.len], name); - if_name[name.len] = 0; - const if_slice = if_name[0..name.len :0]; - const index = std.c.if_nametoindex(if_slice); + if (native_os == .windows) { + const index = std.os.windows.ws2_32.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; - return @bitCast(index); + return .{ .index = index }; + } + + if (builtin.link_libc) { + const index = std.c.if_nametoindex(&name.bytes); + if (index == 0) return error.InterfaceNotFound; + return .{ .index = @bitCast(index) }; + } + + @panic("unimplemented"); +} + +fn netInterfaceName(userdata: ?*anyopaque, interface: Io.net.Interface) Io.net.Interface.NameError!Io.net.Interface.Name { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + if (native_os == .linux) { + _ = interface; + @panic("TODO"); } if (native_os == .windows) { - if (name.len >= posix.IFNAMESIZE) return error.InterfaceNotFound; - var interface_name: [posix.IFNAMESIZE:0]u8 = undefined; - @memcpy(interface_name[0..name.len], name); - interface_name[name.len] = 0; - const index = std.os.windows.ws2_32.if_nametoindex(@as([*:0]const u8, &interface_name)); - if (index == 0) return error.InterfaceNotFound; - return index; + @panic("TODO"); } - @compileError("std.net.if_nametoindex unimplemented for this OS"); + if (builtin.link_libc) { + @panic("TODO"); + } + + @panic("unimplemented"); } const PosixAddress = extern union { @@ -1231,8 +1243,8 @@ fn address6FromPosix(in6: *posix.sockaddr.in6) Io.net.Ip6Address { return .{ .port = std.mem.bigToNative(u16, in6.port), .bytes = in6.addr, - .flowinfo = in6.flowinfo, - .scope_id = in6.scope_id, + .flow = in6.flowinfo, + .interface = .{ .index = in6.scope_id }, }; } @@ -1246,9 +1258,9 @@ fn address4ToPosix(a: Io.net.Ip4Address) posix.sockaddr.in { fn address6ToPosix(a: Io.net.Ip6Address) posix.sockaddr.in6 { return .{ .port = std.mem.nativeToBig(u16, a.port), - .flowinfo = a.flowinfo, + .flowinfo = a.flow, .addr = a.bytes, - .scope_id = a.scope_id, + .scope_id = a.interface.index, }; } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 0881fdc30c..c8555db48c 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -26,10 +26,12 @@ pub const IpAddress = union(enum) { pub const Family = @typeInfo(IpAddress).@"union".tag_type.?; /// Parse the given IP address string into an `IpAddress` value. + /// + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolve` must be + /// called instead. pub fn parse(name: []const u8, port: u16) !IpAddress { - if (Ip4Address.parse(name, port)) |ip4| { - return .{ .ip4 = ip4 }; - } else |err| switch (err) { + if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { error.Overflow, error.InvalidEnd, error.InvalidCharacter, @@ -38,26 +40,41 @@ pub const IpAddress = union(enum) { => {}, } - if (Ip6Address.parse(name, port)) |ip6| { - return .{ .ip6 = ip6 }; - } else |err| switch (err) { + return parseIp6(name, port); + } + + pub fn parseIp4(text: []const u8, port: u16) Ip4Address.ParseError!IpAddress { + return .{ .ip4 = try Ip4Address.parse(text, port) }; + } + + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolveIp6` must be + /// called instead. + pub fn parseIp6(text: []const u8, port: u16) Ip6Address.ParseError!IpAddress { + return .{ .ip6 = try Ip6Address.parse(text, port) }; + } + + /// This function requires an `Io` parameter because it must query the operating + /// system to convert interface name to index. For example, in + /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by + /// creating a socket and then using an `ioctl` syscall. + /// + /// For a pure function that cannot handle scopes, see `parse`. + pub fn resolve(io: Io, text: []const u8, port: u16) !IpAddress { + if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) { error.Overflow, error.InvalidEnd, error.InvalidCharacter, error.Incomplete, - error.InvalidIpv4Mapping, + error.NonCanonical, => {}, } - return error.InvalidIpAddressFormat; + return resolveIp6(io, text, port); } - pub fn parseIp6(buffer: []const u8, port: u16) Ip6Address.ParseError!IpAddress { - return .{ .ip6 = try Ip6Address.parse(buffer, port) }; - } - - pub fn parseIp4(buffer: []const u8, port: u16) Ip4Address.ParseError!IpAddress { - return .{ .ip4 = try Ip4Address.parse(buffer, port) }; + pub fn resolveIp6(io: Io, text: []const u8, port: u16) Ip6Address.ResolveError!IpAddress { + return .{ .ip6 = try Ip6Address.resolve(io, text, port) }; } /// Returns the port in native endian. @@ -74,6 +91,19 @@ pub const IpAddress = union(enum) { } } + /// Includes the optional scope ("%foo" at the end) in IPv6 addresses. + /// + /// See `format` for an alternative that omits scopes and does + /// not require an `Io` parameter. + pub fn formatResolved(a: IpAddress, io: Io, w: *Io.Writer) Ip6Address.FormatError!void { + switch (a) { + .ip4 => |x| return x.format(w), + .ip6 => |x| return x.formatResolved(io, w), + } + } + + /// See `formatResolved` for an alternative that additionally prints the optional + /// scope at the end of IPv6 addresses and requires an `Io` parameter. pub fn format(a: IpAddress, w: *Io.Writer) Io.Writer.Error!void { switch (a) { inline .ip4, .ip6 => |x| return x.format(w), @@ -99,11 +129,12 @@ pub const IpAddress = union(enum) { } }; +/// An IPv4 address in binary memory layout. pub const Ip4Address = struct { bytes: [4]u8, port: u16, - pub fn localhost(port: u16) Ip4Address { + pub fn loopback(port: u16) Ip4Address { return .{ .bytes = .{ 127, 0, 0, 1 }, .port = port, @@ -162,21 +193,14 @@ pub const Ip4Address = struct { } }; +/// An IPv6 address in binary memory layout. pub const Ip6Address = struct { /// Native endian port: u16, /// Big endian bytes: [16]u8, - flowinfo: u32 = 0, - scope_id: u32 = 0, - - pub const ParseError = error{ - Overflow, - InvalidCharacter, - InvalidEnd, - InvalidIpv4Mapping, - Incomplete, - }; + flow: u32 = 0, + interface: Interface = .none, pub const Policy = struct { addr: [16]u8, @@ -186,192 +210,205 @@ pub const Ip6Address = struct { label: u8, }; - pub fn localhost(port: u16) Ip6Address { + pub fn loopback(port: u16) Ip6Address { return .{ .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, .port = port, }; } - pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { - var result: Ip6Address = .{ - .port = port, - .bytes = undefined, + /// An IPv6 address but with `Interface` as a name rather than index. + pub const Unresolved = struct { + /// Big endian + bytes: [16]u8, + interface_name: ?Interface.Name, + + pub const Parsed = union(enum) { + success: Unresolved, + invalid_byte: usize, + unexpected_end, }; - var ip_slice: *[16]u8 = &result.bytes; - var tail: [16]u8 = undefined; - - var x: u16 = 0; - var saw_any_digits = false; - var index: u8 = 0; - var scope_id = false; - var abbrv = false; - for (buffer, 0..) |c, i| { - if (scope_id) { - if (c >= '0' and c <= '9') { - const digit = c - '0'; - { - const ov = @mulWithOverflow(result.scope_id, 10); - if (ov[1] != 0) return error.Overflow; - result.scope_id = ov[0]; - } - { - const ov = @addWithOverflow(result.scope_id, digit); - if (ov[1] != 0) return error.Overflow; - result.scope_id = ov[0]; - } - } else { - return error.InvalidCharacter; - } - } else if (c == ':') { - if (!saw_any_digits) { - if (abbrv) return error.InvalidCharacter; // ':::' - if (i != 0) abbrv = true; - @memset(ip_slice[index..], 0); - ip_slice = tail[0..]; - index = 0; - continue; - } - if (index == 14) { - return error.InvalidEnd; - } - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - - x = 0; - saw_any_digits = false; - } else if (c == '%') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - scope_id = true; - saw_any_digits = false; - } else if (c == '.') { - if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) { - // must start with '::ffff:' - return error.InvalidIpv4Mapping; - } - const start_index = std.mem.lastIndexOfScalar(u8, buffer[0..i], ':').? + 1; - const addr = (Ip4Address.parse(buffer[start_index..], 0) catch { - return error.InvalidIpv4Mapping; - }).bytes; - ip_slice = result.bytes[0..]; - ip_slice[10] = 0xff; - ip_slice[11] = 0xff; - - ip_slice[12] = addr[0]; - ip_slice[13] = addr[1]; - ip_slice[14] = addr[2]; - ip_slice[15] = addr[3]; - return result; - } else { - const digit = try std.fmt.charToDigit(c, 16); - { - const ov = @mulWithOverflow(x, 16); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - { - const ov = @addWithOverflow(x, digit); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - saw_any_digits = true; + pub fn parse(buffer: []const u8) Parsed { + if (buffer.len < 2) return .unexpected_end; + var parts: [8]u16 = @splat(0); + var parts_i: usize = 0; + var i: usize = 0; + var digit_i: usize = 0; + const State = union(enum) { digit, colon, end }; + state: switch (State.digit) { + .digit => c: switch (buffer[i]) { + 'a'...'f' => |c| { + const digit = c - 'a'; + parts[parts_i] = parts[parts_i] * 16 + digit; + if (digit_i == 3) { + digit_i = 0; + parts_i += 1; + i += 1; + if (parts.len - parts_i == 0) continue :state .end; + continue :state .colon; + } + digit_i += 1; + if (buffer.len - i == 0) return .unexpected_end; + i += 1; + continue :c buffer[i]; + }, + 'A'...'F' => |c| continue :c c + ('a' - 'A'), + '0'...'9' => |c| continue :c c + ('a' - '0'), + ':' => @panic("TODO"), + else => return .{ .invalid_byte = i }, + }, + .colon => @panic("TODO"), + .end => @panic("TODO"), } } - if (!saw_any_digits and !abbrv) { - return error.Incomplete; - } - if (!abbrv and index < 14) { - return error.Incomplete; + pub const FromAddressError = Interface.NameError; + + pub fn fromAddress(a: *const Ip6Address, io: Io) FromAddressError!Unresolved { + if (a.interface.isNone()) return .{ + .bytes = a.bytes, + .interface_name = null, + }; + return .{ + .bytes = a.bytes, + .interface_name = try a.interface.name(io), + }; } - if (index == 14) { - ip_slice[14] = @as(u8, @truncate(x >> 8)); - ip_slice[15] = @as(u8, @truncate(x)); - return result; - } else { - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - @memcpy(result.bytes[16 - index ..][0..index], ip_slice[0..index]); - return result; + pub fn format(u: *const Unresolved, w: *Io.Writer) Io.Writer.Error!void { + const bytes = &u.bytes; + if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { + try w.print("::ffff:{d}.{d}.{d}.{d}", .{ bytes[12], bytes[13], bytes[14], bytes[15] }); + } else { + const parts: [8]u16 = .{ + std.mem.readInt(u16, bytes[0..2], .big), + std.mem.readInt(u16, bytes[2..4], .big), + std.mem.readInt(u16, bytes[4..6], .big), + std.mem.readInt(u16, bytes[6..8], .big), + std.mem.readInt(u16, bytes[8..10], .big), + std.mem.readInt(u16, bytes[10..12], .big), + std.mem.readInt(u16, bytes[12..14], .big), + std.mem.readInt(u16, bytes[14..16], .big), + }; + + // Find the longest zero run + var longest_start: usize = 8; + var longest_len: usize = 0; + var current_start: usize = 0; + var current_len: usize = 0; + + for (parts, 0..) |part, i| { + if (part == 0) { + if (current_len == 0) { + current_start = i; + } + current_len += 1; + if (current_len > longest_len) { + longest_start = current_start; + longest_len = current_len; + } + } else { + current_len = 0; + } + } + + // Only compress if the longest zero run is 2 or more + if (longest_len < 2) { + longest_start = 8; + longest_len = 0; + } + + try w.writeAll("["); + var i: usize = 0; + var abbrv = false; + while (i < parts.len) : (i += 1) { + if (i == longest_start) { + // Emit "::" for the longest zero run + if (!abbrv) { + try w.writeAll(if (i == 0) "::" else ":"); + abbrv = true; + } + i += longest_len - 1; // Skip the compressed range + continue; + } + if (abbrv) { + abbrv = false; + } + try w.print("{x}", .{parts[i]}); + if (i != parts.len - 1) { + try w.writeAll(":"); + } + } + } + if (u.interface_name) |n| try w.print("%{s}", .{n.toSlice()}); } + }; + + pub const ParseError = error{ + /// If this is returned, more detailed diagnostics can be obtained by + /// calling `Ip6Address.Parsed.init`. + ParseFailed, + /// If this is returned, the IPv6 address had a scope id on it ("%foo" + /// at the end) which requires calling `resolve`. + UnresolvedScope, + }; + + /// This is a pure function but it cannot handle IPv6 addresses that have + /// scope ids ("%foo" at the end). To also handle those, `resolve` must be + /// called instead. + pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { + switch (Unresolved.parse(buffer)) { + .success => |p| return .{ + .bytes = p.bytes, + .port = port, + .interface = if (p.interface_name != null) return error.UnresolvedScope else .none, + }, + else => return error.ParseFailed, + } + return .{ .ip6 = try Ip6Address.parse(buffer, port) }; } - pub fn format(a: Ip6Address, w: *Io.Writer) Io.Writer.Error!void { - const bytes = &a.bytes; - if (std.mem.eql(u8, bytes[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { - try w.print("[::ffff:{d}.{d}.{d}.{d}]:{d}", .{ - bytes[12], bytes[13], bytes[14], bytes[15], a.port, - }); - return; - } - const parts: [8]u16 = .{ - std.mem.readInt(u16, bytes[0..2], .big), - std.mem.readInt(u16, bytes[2..4], .big), - std.mem.readInt(u16, bytes[4..6], .big), - std.mem.readInt(u16, bytes[6..8], .big), - std.mem.readInt(u16, bytes[8..10], .big), - std.mem.readInt(u16, bytes[10..12], .big), - std.mem.readInt(u16, bytes[12..14], .big), - std.mem.readInt(u16, bytes[14..16], .big), + pub const ResolveError = error{ + /// If this is returned, more detailed diagnostics can be obtained by + /// calling the `Parsed.init` function. + ParseFailed, + } || Interface.Name.ResolveError; + + /// This function requires an `Io` parameter because it must query the operating + /// system to convert interface name to index. For example, in + /// "fe80::e0e:76ff:fed4:cf22%eno1", "eno1" must be resolved to an index by + /// creating a socket and then using an `ioctl` syscall. + pub fn resolve(io: Io, buffer: []const u8, port: u16) ResolveError!Ip6Address { + return switch (Unresolved.parse(buffer)) { + .success => |p| return .{ + .bytes = p.bytes, + .port = port, + .interface = if (p.interface_name) |n| try n.resolve(io) else .none, + }, + else => return error.ParseFailed, }; + } - // Find the longest zero run - var longest_start: usize = 8; - var longest_len: usize = 0; - var current_start: usize = 0; - var current_len: usize = 0; + pub const FormatError = Io.Writer.Error || Unresolved.FromAddressError; - for (parts, 0..) |part, i| { - if (part == 0) { - if (current_len == 0) { - current_start = i; - } - current_len += 1; - if (current_len > longest_len) { - longest_start = current_start; - longest_len = current_len; - } - } else { - current_len = 0; - } - } + /// Includes the optional scope ("%foo" at the end). + /// + /// See `format` for an alternative that omits scopes and does + /// not require an `Io` parameter. + pub fn formatResolved(a: Ip6Address, io: Io, w: *Io.Writer) FormatError!void { + const u: Unresolved = try .fromAddress(io); + try w.print("[{f}]:{d}", .{ u, a.port }); + } - // Only compress if the longest zero run is 2 or more - if (longest_len < 2) { - longest_start = 8; - longest_len = 0; - } - - try w.writeAll("["); - var i: usize = 0; - var abbrv = false; - while (i < parts.len) : (i += 1) { - if (i == longest_start) { - // Emit "::" for the longest zero run - if (!abbrv) { - try w.writeAll(if (i == 0) "::" else ":"); - abbrv = true; - } - i += longest_len - 1; // Skip the compressed range - continue; - } - if (abbrv) { - abbrv = false; - } - try w.print("{x}", .{parts[i]}); - if (i != parts.len - 1) { - try w.writeAll(":"); - } - } - try w.print("]:{d}", .{a.port}); + /// See `formatResolved` for an alternative that additionally prints the optional + /// scope at the end of addresses and requires an `Io` parameter. + pub fn format(a: Ip6Address, w: *Io.Writer) Io.Writer.Error!void { + const u: Unresolved = .{ + .bytes = a.bytes, + .interface_name = null, + }; + try w.print("[{f}]:{d}", .{ u, a.port }); } pub fn eql(a: Ip6Address, b: Ip6Address) bool { @@ -471,11 +508,64 @@ pub const Ip6Address = struct { }; }; +pub const Interface = struct { + /// Value 0 indicates `none`. + index: u32, + + pub const none: Interface = .{ .index = 0 }; + + pub const Name = struct { + bytes: [max_len:0]u8, + + pub const max_len = std.posix.IFNAMESIZE - 1; + + pub fn toSlice(n: *const Name) []const u8 { + return std.mem.sliceTo(&n.bytes, 0); + } + + pub fn fromSlice(bytes: []const u8) error{NameTooLong}!Name { + if (bytes.len > max_len) return error.NameTooLong; + var result: Name = undefined; + @memcpy(result.bytes[0..bytes.len], bytes); + result.bytes[bytes.len] = 0; + return result; + } + + pub const ResolveError = error{ + InterfaceNotFound, + AccessDenied, + SystemResources, + } || Io.UnexpectedError || Io.Cancelable; + + /// Corresponds to "if_nametoindex" in libc. + pub fn resolve(n: []const u8, io: Io) ResolveError!Interface { + return io.vtable.netInterfaceNameResolve(io.userdata, n); + } + }; + + pub const NameError = Io.UnexpectedError || Io.Cancelable; + + /// Asserts not `none`. + /// + /// Corresponds to "if_indextoname" in libc. + pub fn name(i: Interface, io: Io) NameError!Name { + assert(i.index != 0); + return io.vtable.netInterfaceName(io.userdata, i); + } + + pub fn isNone(i: Interface) bool { + return i.index == 0; + } +}; + +/// An open socket connection with a network protocol that guarantees +/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain +/// socket. pub const Stream = struct { - /// Underlying platform-defined type which may or may not be - /// interchangeable with a file system file descriptor. handle: Handle, + /// Underlying platform-defined type which may or may not be + /// interchangeable with a file system file descriptor. pub const Handle = switch (native_os) { .windows => std.windows.ws2_32.SOCKET, else => std.posix.fd_t, @@ -583,17 +673,6 @@ pub const Server = struct { } }; -pub const InterfaceIndexError = error{ - InterfaceNotFound, - AccessDenied, - SystemResources, -} || Io.UnexpectedError || Io.Cancelable; - -/// Otherwise known as "if_nametoindex". -pub fn interfaceIndex(io: Io, name: []const u8) InterfaceIndexError!u32 { - return io.vtable.netInterfaceIndex(io.userdata, name); -} - test { _ = HostName; } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 06cf4219b3..710f0cb43c 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -105,11 +105,11 @@ pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!L { var i: usize = 0; if (options.family != .ip6) { - options.addresses_buffer[i] = .{ .ip4 = .localhost(options.port) }; + options.addresses_buffer[i] = .{ .ip4 = .loopback(options.port) }; i += 1; } if (options.family != .ip4) { - options.addresses_buffer[i] = .{ .ip6 = .localhost(options.port) }; + options.addresses_buffer[i] = .{ .ip6 = .loopback(options.port) }; i += 1; } const canon_name = "localhost"; @@ -166,7 +166,7 @@ fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult switch (a) { .ip6 => |ip6| { da6.bytes = ip6.bytes; - da6.scope_id = ip6.scope_id; + da6.interface = ip6.interface; }, .ip4 => |ip4| { da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; From e7c9df9fb0ca9a58b5c883c92d56e3c27dd627ef Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 21 Sep 2025 23:39:14 -0700 Subject: [PATCH 055/244] Io.net: use resolve for IPv6 /etc/resolv.conf might have IPv6 addresses with scope in it, so this is needed. --- lib/std/Io.zig | 2 +- lib/std/Io/Threaded.zig | 5 ++++- lib/std/Io/net.zig | 2 +- lib/std/Io/net/HostName.zig | 14 +++++++------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 4d10afbafe..2b2f0646f4 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -668,7 +668,7 @@ pub const VTable = struct { netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, stream: net.Stream) void, - netInterfaceNameResolve: *const fn (?*anyopaque, net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, + netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, }; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 1ac56469df..c9b0810dac 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1124,7 +1124,10 @@ fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void { return net_stream.close(); } -fn netInterfaceNameResolve(userdata: ?*anyopaque, name: Io.net.Interface.Name) Io.net.Interface.Name.ResolveError!Io.net.Interface { +fn netInterfaceNameResolve( + userdata: ?*anyopaque, + name: *const Io.net.Interface.Name, +) Io.net.Interface.Name.ResolveError!Io.net.Interface { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index c8555db48c..9ab0ba4328 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -538,7 +538,7 @@ pub const Interface = struct { } || Io.UnexpectedError || Io.Cancelable; /// Corresponds to "if_nametoindex" in libc. - pub fn resolve(n: []const u8, io: Io) ResolveError!Interface { + pub fn resolve(n: *const Name, io: Io) ResolveError!Interface { return io.vtable.netInterfaceNameResolve(io.userdata, n); } }; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 710f0cb43c..9bce195034 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -542,7 +542,7 @@ pub const ResolvConf = struct { error.NotDir, error.AccessDenied, => { - try addNumeric(&rc, "127.0.0.1", 53); + try addNumeric(&rc, io, "127.0.0.1", 53); return rc; }, @@ -552,7 +552,7 @@ pub const ResolvConf = struct { var line_buf: [512]u8 = undefined; var file_reader = file.reader(io, &line_buf); - parse(&rc, &file_reader.interface) catch |err| switch (err) { + parse(&rc, io, &file_reader.interface) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, else => |e| return e, }; @@ -562,7 +562,7 @@ pub const ResolvConf = struct { const Directive = enum { options, nameserver, domain, search }; const Option = enum { ndots, attempts, timeout }; - fn parse(rc: *ResolvConf, reader: *Io.Reader) !void { + fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void { while (reader.takeSentinel('\n')) |line_with_comment| { const line = line: { var split = std.mem.splitScalar(u8, line_with_comment, '#'); @@ -588,7 +588,7 @@ pub const ResolvConf = struct { }, .nameserver => { const ip_txt = line_it.next() orelse continue; - try addNumeric(rc, ip_txt, 53); + try addNumeric(rc, io, ip_txt, 53); }, .domain, .search => { const rest = line_it.rest(); @@ -602,13 +602,13 @@ pub const ResolvConf = struct { } if (rc.nameservers_len == 0) { - try addNumeric(rc, "127.0.0.1", 53); + try addNumeric(rc, io, "127.0.0.1", 53); } } - fn addNumeric(rc: *ResolvConf, name: []const u8, port: u16) !void { + fn addNumeric(rc: *ResolvConf, io: Io, name: []const u8, port: u16) !void { assert(rc.nameservers_len < rc.nameservers_buffer.len); - rc.nameservers_buffer[rc.nameservers_len] = try .parse(name, port); + rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port); rc.nameservers_len += 1; } From 885b3f8342e2ceb3ebb63bf46840b728407230d3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 22 Sep 2025 17:25:03 -0700 Subject: [PATCH 056/244] Io.net: finish implementing IPv6 parsing --- lib/std/Io/net.zig | 142 +++++++++++++++++++++++++++++------- lib/std/Io/net/HostName.zig | 6 +- lib/std/testing.zig | 2 +- 3 files changed, 118 insertions(+), 32 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 9ab0ba4328..3fb764504f 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -30,8 +30,8 @@ pub const IpAddress = union(enum) { /// This is a pure function but it cannot handle IPv6 addresses that have /// scope ids ("%foo" at the end). To also handle those, `resolve` must be /// called instead. - pub fn parse(name: []const u8, port: u16) !IpAddress { - if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { + pub fn parse(text: []const u8, port: u16) !IpAddress { + if (parseIp4(text, port)) |ip4| return ip4 else |err| switch (err) { error.Overflow, error.InvalidEnd, error.InvalidCharacter, @@ -40,7 +40,7 @@ pub const IpAddress = union(enum) { => {}, } - return parseIp6(name, port); + return parseIp6(text, port); } pub fn parseIp4(text: []const u8, port: u16) Ip4Address.ParseError!IpAddress { @@ -227,39 +227,102 @@ pub const Ip6Address = struct { success: Unresolved, invalid_byte: usize, unexpected_end, + junk_after_end: usize, + interface_name_oversized: usize, }; - pub fn parse(buffer: []const u8) Parsed { - if (buffer.len < 2) return .unexpected_end; + pub fn parse(text: []const u8) Parsed { + if (text.len < 2) return .unexpected_end; + // Has to be u16 elements to handle 3-digit hex numbers from compression. var parts: [8]u16 = @splat(0); - var parts_i: usize = 0; - var i: usize = 0; - var digit_i: usize = 0; - const State = union(enum) { digit, colon, end }; + var parts_i: u8 = 0; + var text_i: u8 = 0; + var digit_i: u8 = 0; + var compress_start: ?u8 = null; + var interface_name_text: ?[]const u8 = null; + const State = union(enum) { digit, end }; state: switch (State.digit) { - .digit => c: switch (buffer[i]) { + .digit => c: switch (text[text_i]) { 'a'...'f' => |c| { - const digit = c - 'a'; + const digit = c - 'a' + 10; parts[parts_i] = parts[parts_i] * 16 + digit; - if (digit_i == 3) { - digit_i = 0; - parts_i += 1; - i += 1; - if (parts.len - parts_i == 0) continue :state .end; - continue :state .colon; - } + if (digit_i == 4) return .{ .invalid_byte = text_i }; digit_i += 1; - if (buffer.len - i == 0) return .unexpected_end; - i += 1; - continue :c buffer[i]; + text_i += 1; + if (text.len - text_i == 0) { + parts_i += 1; + continue :state .end; + } + continue :c text[text_i]; }, - 'A'...'F' => |c| continue :c c + ('a' - 'A'), - '0'...'9' => |c| continue :c c + ('a' - '0'), - ':' => @panic("TODO"), - else => return .{ .invalid_byte = i }, + 'A'...'F' => |c| continue :c c - 'A' + 'a', + '0'...'9' => |c| { + const digit = c - '0'; + parts[parts_i] = parts[parts_i] * 16 + digit; + if (digit_i == 4) return .{ .invalid_byte = text_i }; + digit_i += 1; + text_i += 1; + if (text.len - text_i == 0) { + parts_i += 1; + continue :state .end; + } + continue :c text[text_i]; + }, + ':' => { + if (digit_i == 0) { + if (compress_start != null) return .{ .invalid_byte = text_i }; + if (text_i == 0) { + text_i += 1; + if (text[text_i] != ':') return .{ .invalid_byte = text_i }; + assert(parts_i == 0); + } + text_i += 1; + if (text.len - text_i == 0) return .unexpected_end; + compress_start = parts_i; + continue :c text[text_i]; + } else { + parts_i += 1; + if (parts.len - parts_i == 0) continue :state .end; + digit_i = 0; + text_i += 1; + if (text.len - text_i == 0) return .unexpected_end; + continue :c text[text_i]; + } + }, + '%' => { + if (digit_i == 0) return .{ .invalid_byte = text_i }; + parts_i += 1; + text_i += 1; + const name = text[text_i..]; + if (name.len > Interface.Name.max_len) return .{ .interface_name_oversized = text_i }; + interface_name_text = name; + text_i = @intCast(text.len); + continue :state .end; + }, + else => return .{ .invalid_byte = text_i }, + }, + .end => { + if (text.len - text_i != 0) return .{ .junk_after_end = text_i }; + const remaining = parts.len - parts_i; + if (compress_start) |s| { + const src = parts[s..parts_i]; + @memmove(parts[parts.len - src.len ..], src); + @memset(parts[s..][0..remaining], 0); + } else { + if (remaining != 0) return .unexpected_end; + } + + // Workaround that can be removed when this proposal is + // implemented https://github.com/ziglang/zig/issues/19755 + if ((comptime @import("builtin").cpu.arch.endian()) != .big) { + for (&parts) |*part| part.* = @byteSwap(part.*); + } + + return .{ .success = .{ + .bytes = @bitCast(parts), + .interface_name = if (interface_name_text) |t| .fromSliceUnchecked(t) else null, + } }; }, - .colon => @panic("TODO"), - .end => @panic("TODO"), } } @@ -319,7 +382,6 @@ pub const Ip6Address = struct { longest_len = 0; } - try w.writeAll("["); var i: usize = 0; var abbrv = false; while (i < parts.len) : (i += 1) { @@ -525,6 +587,12 @@ pub const Interface = struct { pub fn fromSlice(bytes: []const u8) error{NameTooLong}!Name { if (bytes.len > max_len) return error.NameTooLong; + return .fromSliceUnchecked(bytes); + } + + /// Asserts bytes.len fits in `max_len`. + pub fn fromSliceUnchecked(bytes: []const u8) Name { + assert(bytes.len <= max_len); var result: Name = undefined; @memcpy(result.bytes[0..bytes.len], bytes); result.bytes[bytes.len] = 0; @@ -673,6 +741,24 @@ pub const Server = struct { } }; +test "parsing IPv6 addresses" { + try testIp6Parse("fe80::e0e:76ff:fed4:cf22%eno1"); + try testIp6Parse("2001:db8::1"); +} + +fn testIp6Parse(text: []const u8) !void { + const ua = switch (Ip6Address.Unresolved.parse(text)) { + .success => |p| p, + else => |x| { + std.debug.print("failed to parse \"{s}\": {any}\n", .{ text, x }); + return error.TestFailed; + }, + }; + var buffer: [100]u8 = undefined; + const result = try std.fmt.bufPrint(&buffer, "{f}", .{ua}); + try std.testing.expectEqualStrings(text, result); +} + test { _ = HostName; } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 9bce195034..bb86eb7c5e 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -612,7 +612,7 @@ pub const ResolvConf = struct { rc.nameservers_len += 1; } - fn nameservers(rc: *const ResolvConf) []IpAddress { + fn nameservers(rc: *const ResolvConf) []const IpAddress { return rc.nameservers_buffer[0..rc.nameservers_len]; } @@ -651,6 +651,6 @@ test ResolvConf { .attempts = 2, }; - try rc.parse(&reader); - try std.testing.expect(false); + try rc.parse(std.testing.io, &reader); + try std.testing.expectEqual(3, rc.nameservers().len); } diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 5baaa0f77b..02ad99932e 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -28,7 +28,7 @@ pub var allocator_instance: std.heap.GeneralPurposeAllocator(.{ break :b .init; }; -pub var io_instance: std.Io.ThreadPool = undefined; +pub var io_instance: std.Io.Threaded = undefined; pub const io = io_instance.io(); /// TODO https://github.com/ziglang/zig/issues/5738 From 5782158628d861704354dda619152d1fde674b10 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 22 Sep 2025 17:32:38 -0700 Subject: [PATCH 057/244] std.net: fix parsing IPv6 addr "::" --- lib/std/Io/net.zig | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 3fb764504f..bdc9de0c56 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -276,9 +276,9 @@ pub const Ip6Address = struct { if (text[text_i] != ':') return .{ .invalid_byte = text_i }; assert(parts_i == 0); } - text_i += 1; - if (text.len - text_i == 0) return .unexpected_end; compress_start = parts_i; + text_i += 1; + if (text.len - text_i == 0) continue :state .end; continue :c text[text_i]; } else { parts_i += 1; @@ -744,19 +744,30 @@ pub const Server = struct { test "parsing IPv6 addresses" { try testIp6Parse("fe80::e0e:76ff:fed4:cf22%eno1"); try testIp6Parse("2001:db8::1"); + try testIp6ParseTransform("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0001"); + try testIp6Parse("::1"); + try testIp6Parse("::"); + try testIp6Parse("fe80::1"); + try testIp6Parse("fe80::abcd:ef12%3"); + try testIp6Parse("ff02::"); + try testIp6Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); } -fn testIp6Parse(text: []const u8) !void { - const ua = switch (Ip6Address.Unresolved.parse(text)) { +fn testIp6Parse(input: []const u8) !void { + return testIp6ParseTransform(input, input); +} + +fn testIp6ParseTransform(expected: []const u8, input: []const u8) !void { + const ua = switch (Ip6Address.Unresolved.parse(input)) { .success => |p| p, else => |x| { - std.debug.print("failed to parse \"{s}\": {any}\n", .{ text, x }); + std.debug.print("failed to parse \"{s}\": {any}\n", .{ input, x }); return error.TestFailed; }, }; var buffer: [100]u8 = undefined; const result = try std.fmt.bufPrint(&buffer, "{f}", .{ua}); - try std.testing.expectEqualStrings(text, result); + try std.testing.expectEqualStrings(expected, result); } test { From 8771a9f082c56a7203949b2abcb3a4bf5a8fd9dd Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 23 Sep 2025 00:12:16 -0700 Subject: [PATCH 058/244] std.Io.net: progress towards DNS resolution --- lib/std/Io.zig | 4 +- lib/std/Io/net.zig | 80 +++++++++++++++++++++++++++++---- lib/std/Io/net/HostName.zig | 88 ++++++++++++++++++++++++++++++------- lib/std/Random.zig | 6 +++ 4 files changed, 151 insertions(+), 27 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 2b2f0646f4..2c87c865b1 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -664,10 +664,12 @@ pub const VTable = struct { sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.ListenOptions) net.ListenError!net.Server, + bind: *const fn (?*anyopaque, address: net.IpAddress, options: net.BindOptions) net.BindError!net.Socket, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Server.Connection, + netSend: *const fn (?*anyopaque, address: net.IpAddress, data: []const []const u8) net.SendError!void, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, - netClose: *const fn (?*anyopaque, stream: net.Stream) void, + netClose: *const fn (?*anyopaque, socket: net.Socket) void, netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, }; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index bdc9de0c56..c9dc1d619a 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -8,6 +8,8 @@ pub const HostName = @import("net/HostName.zig"); pub const ListenError = std.net.Address.ListenError || Io.Cancelable; +pub const BindError = std.net.Address.BindError || Io.Cancelable; + pub const ListenOptions = struct { /// How many connections the kernel will accept on the application's behalf. /// If more than this many connections pool in the kernel, clients will start @@ -19,6 +21,13 @@ pub const ListenOptions = struct { force_nonblocking: bool = false, }; +pub const BindOptions = struct { + /// The socket is restricted to sending and receiving IPv6 packets only. + /// In this case, an IPv4 and an IPv6 application can bind to a single port + /// at the same time. + ip6_only: bool = false, +}; + pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, @@ -123,10 +132,21 @@ pub const IpAddress = union(enum) { }; } - /// The returned `Server` has an open `stream`. + /// Waits for a TCP connection. When using this API, `bind` does not need + /// to be called. The returned `Server` has an open `stream`. pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { return io.vtable.listen(io.userdata, address, options); } + + /// Associates an address with a `Socket` which can be used to receive UDP + /// packets and other kinds of non-streaming messages. See `listen` for a + /// streaming alternative. + /// + /// One bound `Socket` can be used to receive messages from multiple + /// different addresses. + pub fn bind(address: IpAddress, io: Io, options: BindOptions) BindError!Socket { + return io.vtable.bind(io.userdata, address, options); + } }; /// An IPv4 address in binary memory layout. @@ -141,6 +161,13 @@ pub const Ip4Address = struct { }; } + pub fn unspecified(port: u16) Ip4Address { + return .{ + .bytes = .{ 0, 0, 0, 0 }, + .port = port, + }; + } + pub const ParseError = error{ Overflow, InvalidEnd, @@ -217,6 +244,31 @@ pub const Ip6Address = struct { }; } + pub fn unspecified(port: u16) Ip6Address { + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + .port = port, + }; + } + + /// Constructs an IPv4-mapped IPv6 address. + pub fn fromIp4(ip4: Ip4Address) Ip6Address { + const b = &ip4.bytes; + return .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, + .port = ip4.port, + }; + } + + /// Given an `IpAddress`, converts it to an `Ip6Address` directly, or via + /// constructing an IPv4-mapped IPv6 address. + pub fn fromAny(addr: IpAddress) Ip6Address { + return switch (addr) { + .ip4 => |ip4| fromIp4(ip4), + .ip6 => |ip6| ip6, + }; + } + /// An IPv6 address but with `Interface` as a name rather than index. pub const Unresolved = struct { /// Big endian @@ -626,11 +678,11 @@ pub const Interface = struct { } }; -/// An open socket connection with a network protocol that guarantees -/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain -/// socket. -pub const Stream = struct { +/// An open port with unspecified protocol. +pub const Socket = struct { handle: Handle, + /// Contains the resolved ephemeral port number if requested. + bind_address: IpAddress, /// Underlying platform-defined type which may or may not be /// interchangeable with a file system file descriptor. @@ -639,8 +691,19 @@ pub const Stream = struct { else => std.posix.fd_t, }; + pub fn close(s: Socket, io: Io) void { + return io.vtable.netClose(io.userdata, s); + } +}; + +/// An open socket connection with a network protocol that guarantees +/// sequencing, delivery, and prevents repetition. Typically TCP or UNIX domain +/// socket. +pub const Stream = struct { + socket: Socket, + pub fn close(s: Stream, io: Io) void { - return io.vtable.close(io.userdata, s); + return io.vtable.netClose(io.userdata, s.socket); } pub const Reader = struct { @@ -719,8 +782,7 @@ pub const Stream = struct { }; pub const Server = struct { - listen_address: IpAddress, - stream: Stream, + socket: Socket, pub const Connection = struct { stream: Stream, @@ -728,7 +790,7 @@ pub const Server = struct { }; pub fn deinit(s: *Server, io: Io) void { - s.stream.close(io); + s.socket.close(io); s.* = undefined; } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index bb86eb7c5e..5e41e12b7d 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -218,6 +218,11 @@ fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) !LookupR return lookupDns(io, lookup_canon_name, &rc, options); } +const DnsReply = struct { + buf: [512]u8, + len: usize, +}; + fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) !LookupResult { const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ .{ .af = .ip6, .rr = std.posix.RR.A }, @@ -225,21 +230,21 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio }; var query_buffers: [2][280]u8 = undefined; var queries_buffer: [2][]const u8 = undefined; - var answer_buffers: [2][512]u8 = undefined; - var answers_buffer: [2][]u8 = .{ &answer_buffers[0], &answer_buffers[1] }; var nq: usize = 0; for (family_records) |fr| { if (options.family != fr.af) { - const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr); + const entropy = std.crypto.random.array(u8, 2); + const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy); queries_buffer[nq] = query_buffers[nq][0..len]; nq += 1; } } const queries = queries_buffer[0..nq]; - const replies = answers_buffer[0..nq]; - try rc.sendMessage(io, queries, replies); + var replies_buffer: [2]DnsReply = undefined; + var replies: Io.Queue(DnsReply) = .init(&replies_buffer); + try rc.sendMessage(io, queries, &replies); for (replies) |reply| { if (reply.len < 4 or (reply[3] & 15) == 2) return error.TemporaryNameServerFailure; @@ -391,7 +396,7 @@ fn copyCanon(canonical_name_buffer: *[max_len]u8, name: []const u8) HostName { } /// Writes DNS resolution query packet data to `w`; at most 280 bytes. -fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8) usize { +fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize { // This implementation is ported from musl libc. // A more idiomatic "ziggy" implementation would be welcome. var name = dname; @@ -400,7 +405,8 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u const n = 17 + name.len + @intFromBool(name.len != 0); // Construct query template - ID will be filled later - @memset(q[0..n], 0); + q[0..2].* = entropy; + @memset(q[2..n], 0); q[2] = @as(u8, op) * 8 + 1; q[5] = 1; @memcpy(q[13..][0..name.len], name); @@ -416,8 +422,6 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u } q[i + 1] = ty; q[i + 3] = class; - - std.crypto.random.bytes(q[0..2]); return n; } @@ -519,12 +523,14 @@ pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream pub const ResolvConf = struct { attempts: u32, ndots: u32, - timeout: u32, - nameservers_buffer: [3]IpAddress, + timeout: Io.Duration, + nameservers_buffer: [max_nameservers]IpAddress, nameservers_len: usize, search_buffer: [max_len]u8, search_len: usize, + pub const max_nameservers = 3; + /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. fn init(io: Io) !ResolvConf { var rc: ResolvConf = .{ @@ -620,13 +626,61 @@ pub const ResolvConf = struct { rc: *const ResolvConf, io: Io, queries: []const []const u8, - answers: [][]u8, + replies: *Io.Queue(DnsReply), ) !void { - _ = rc; - _ = io; - _ = queries; - _ = answers; - @panic("TODO"); + var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined; + var any_ip6 = false; + for (rc.nameservers(), &ip4_mapped) |*ns, *m| { + m.* = .{ .ip6 = .fromAny(ns.*) }; + any_ip6 = any_ip6 or ns.* == .ip6; + } + + const socket = s: { + if (any_ip6) ip6: { + const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; + const socket = ip6_addr.bind(io, .{ .ip6_only = true }) catch |err| switch (err) { + error.AddressFamilyNotSupported => break :ip6, + }; + break :s socket; + } + any_ip6 = false; + const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; + const socket = try ip4_addr.bind(io, .{}); + break :s socket; + }; + defer socket.close(); + + const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); + + var group: Io.Group = .{}; + defer group.cancel(); + + for (queries) |query| { + for (mapped_nameservers) |*ns| { + group.async(sendOneMessage, .{ io, query, ns }); + } + } + + const deadline: Io.Deadline = .fromDuration(rc.timeout); + + for (0..queries.len) |_| { + const msg = socket.receiveDeadline(deadline) catch |err| switch (err) { + error.Timeout => return error.Timeout, + error.Canceled => return error.Canceled, + else => continue, + }; + _ = msg; + _ = replies; + @panic("TODO check msg for dns reply and put into replies queue"); + } + } + + fn sendOneMessage( + io: Io, + query: []const u8, + ns: *const IpAddress, + ) void { + io.vtable.netSend(io.userdata, ns.*, &.{query}) catch |err| switch (err) {}; } }; diff --git a/lib/std/Random.zig b/lib/std/Random.zig index ae88d8b4fe..b90c8e3958 100644 --- a/lib/std/Random.zig +++ b/lib/std/Random.zig @@ -58,6 +58,12 @@ pub fn bytes(r: Random, buf: []u8) void { r.fillFn(r.ptr, buf); } +pub fn array(r: Random, comptime E: type, comptime N: usize) [N]E { + var result: [N]E = undefined; + bytes(r, &result); + return result; +} + pub fn boolean(r: Random) bool { return r.int(u1) != 0; } From 60c4bdb14c2989992019b81d344b8b777871283c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 26 Sep 2025 18:16:22 -0700 Subject: [PATCH 059/244] Io.net: implement more networking the next task is now implementing Io.Group --- lib/std/Io.zig | 15 +- lib/std/Io/Threaded.zig | 335 ++++++++++++++++++++++++++++++------ lib/std/Io/net.zig | 193 +++++++++++++++++---- lib/std/Io/net/HostName.zig | 11 +- lib/std/net.zig | 1 + 5 files changed, 459 insertions(+), 96 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 2c87c865b1..359d1a496f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -663,13 +663,14 @@ pub const VTable = struct { now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp, sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, - listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.ListenOptions) net.ListenError!net.Server, - bind: *const fn (?*anyopaque, address: net.IpAddress, options: net.BindOptions) net.BindError!net.Socket, - accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Server.Connection, - netSend: *const fn (?*anyopaque, address: net.IpAddress, data: []const []const u8) net.SendError!void, + listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, + accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, + ipBind: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, + netSend: *const fn (?*anyopaque, handle: net.Socket.Handle, address: net.IpAddress, data: []const u8) net.Socket.SendError!void, + netReceive: *const fn (?*anyopaque, handle: net.Socket.Handle, address: net.IpAddress, buffer: []u8) net.Socket.ReceiveError!void, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, - netClose: *const fn (?*anyopaque, socket: net.Socket) void, + netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, }; @@ -711,6 +712,10 @@ pub const Duration = struct { pub fn ms(x: u64) Duration { return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_ms }; } + + pub fn seconds(x: u64) Duration { + return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_s }; + } }; pub const Deadline = union(enum) { duration: Duration, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c9b0810dac..07f87fdf0c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -124,8 +124,19 @@ pub fn io(pool: *Pool) Io { .now = now, .sleep = sleep, - .listen = listen, - .accept = accept, + .listen = switch (builtin.os.tag) { + .windows => @panic("TODO"), + else => listenPosix, + }, + .accept = switch (builtin.os.tag) { + .windows => @panic("TODO"), + else => acceptPosix, + }, + .ipBind = switch (builtin.os.tag) { + .windows => @panic("TODO"), + else => ipBindPosix, + }, + .netClose = netClose, .netRead = switch (builtin.os.tag) { .windows => @panic("TODO"), else => netReadPosix, @@ -134,7 +145,8 @@ pub fn io(pool: *Pool) Io { .windows => @panic("TODO"), else => netWritePosix, }, - .netClose = netClose, + .netSend = netSend, + .netReceive = netReceive, .netInterfaceNameResolve = netInterfaceNameResolve, .netInterfaceName = netInterfaceName, }, @@ -460,7 +472,7 @@ fn asyncDetached( fn await( userdata: ?*anyopaque, - any_future: *std.Io.AnyFuture, + any_future: *Io.AnyFuture, result: []u8, result_alignment: std.mem.Alignment, ) void { @@ -984,59 +996,228 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { return result.?; } -fn listen(userdata: ?*anyopaque, address: Io.net.IpAddress, options: Io.net.ListenOptions) Io.net.ListenError!Io.net.Server { +fn listenPosix( + userdata: ?*anyopaque, + address: Io.net.IpAddress, + options: Io.net.IpAddress.ListenOptions, +) Io.net.IpAddress.ListenError!Io.net.Server { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - - const nonblock: u32 = if (options.force_nonblocking) posix.SOCK.NONBLOCK else 0; - const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | nonblock; - const proto: u32 = posix.IPPROTO.TCP; - const family = posixAddressFamily(address); - const sockfd = try posix.socket(family, sock_flags, proto); - const stream: std.net.Stream = .{ .handle = sockfd }; - errdefer stream.close(); + const family = posixAddressFamily(&address); + const protocol: u32 = posix.IPPROTO.TCP; + const socket_fd = while (true) { + try pool.checkCancel(); + const flags: u32 = posix.SOCK.STREAM | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; + const socket_rc = posix.system.socket(family, flags, protocol); + switch (posix.errno(socket_rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(socket_rc); + errdefer posix.close(fd); + if (socket_flags_unsupported) while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + break fd; + }, + .INTR => continue, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(socket_fd); if (options.reuse_address) { - try posix.setsockopt( - sockfd, - posix.SOL.SOCKET, - posix.SO.REUSEADDR, - &std.mem.toBytes(@as(c_int, 1)), - ); - if (@hasDecl(posix.SO, "REUSEPORT") and family != posix.AF.UNIX) { - try posix.setsockopt( - sockfd, - posix.SOL.SOCKET, - posix.SO.REUSEPORT, - &std.mem.toBytes(@as(c_int, 1)), - ); - } + try setSocketOption(pool, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1); + if (@hasDecl(posix.SO, "REUSEPORT")) + try setSocketOption(pool, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1); } var storage: PosixAddress = undefined; var socklen = addressToPosix(address, &storage); - try posix.bind(sockfd, &storage.any, socklen); - try posix.listen(sockfd, options.kernel_backlog); - try posix.getsockname(sockfd, &storage.any, &socklen); + try posixBind(pool, socket_fd, &storage.any, socklen); + + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { + .SUCCESS => break, + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } + + try posixGetSockName(pool, socket_fd, &storage.any, &socklen); return .{ - .listen_address = addressFromPosix(&storage), - .stream = .{ .handle = stream.handle }, + .socket = .{ + .handle = socket_fd, + .address = addressFromPosix(&storage), + }, }; } -fn accept(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.AcceptError!Io.net.Server.Connection { +fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), // always a race condition if this error is returned + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixGetSockName(pool: *Pool, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .BADF => |err| return errnoBug(err), // always a race condition + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // always a race condition + .NOBUFS => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { + const o: []const u8 = @ptrCast(&option); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) { + .SUCCESS => return, + .INTR => continue, + .BADF => |err| return errnoBug(err), // always a race condition + .NOTSOCK => |err| return errnoBug(err), // always a race condition + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn ipBindPosix( + userdata: ?*anyopaque, + address: Io.net.IpAddress, + options: Io.net.IpAddress.BindOptions, +) Io.net.IpAddress.BindError!Io.net.Socket { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const mode = posixSocketMode(options.mode); + const family = posixAddressFamily(&address); + const protocol = posixProtocol(options.protocol); + const socket_fd = while (true) { + try pool.checkCancel(); + const flags: u32 = mode | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; + const socket_rc = posix.system.socket(family, flags, protocol); + switch (posix.errno(socket_rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(socket_rc); + errdefer posix.close(fd); + if (socket_flags_unsupported) while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + break fd; + }, + .INTR => continue, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .INVAL => return error.ProtocolUnsupportedBySystem, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .PROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + .PROTOTYPE => return error.SocketModeUnsupported, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (options.ip6_only) { + try setSocketOption(pool, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); + } var storage: PosixAddress = undefined; - var addr_len: posix.socklen_t = @sizeOf(PosixAddress); - const fd = try posix.accept(server.stream.handle, &storage.any, &addr_len, posix.SOCK.CLOEXEC); + var socklen = addressToPosix(address, &storage); + try posixBind(pool, socket_fd, &storage.any, socklen); + try posixGetSockName(pool, socket_fd, &storage.any, &socklen); return .{ - .stream = .{ .handle = fd }, + .handle = socket_fd, .address = addressFromPosix(&storage), }; } +const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 +const have_accept4 = !socket_flags_unsupported; + +fn acceptPosix(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.AcceptError!Io.net.Stream { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const listen_fd = server.socket.handle; + var storage: PosixAddress = undefined; + var addr_len: posix.socklen_t = @sizeOf(PosixAddress); + const fd = while (true) { + try pool.checkCancel(); + const rc = if (have_accept4) + posix.system.accept4(listen_fd, &storage.any, &addr_len, posix.SOCK.CLOEXEC) + else + posix.system.accept(listen_fd, &storage.any, &addr_len); + switch (posix.errno(rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(rc); + errdefer posix.close(fd); + if (!have_accept4) while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + break fd; + }, + .INTR => continue, + .AGAIN => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // always a race condition + .CONNABORTED => return error.ConnectionAborted, + .FAULT => |err| return errnoBug(err), + .INVAL => return error.SocketNotListening, + .NOTSOCK => |err| return errnoBug(err), + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .OPNOTSUPP => |err| return errnoBug(err), + .PROTO => return error.ProtocolFailure, + .PERM => return error.BlockedByFirewall, + else => |err| return posix.unexpectedErrno(err), + } + }; + return .{ .socket = .{ + .handle = fd, + .address = addressFromPosix(&storage), + } }; +} + fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.net.Stream.Reader.Error!usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); @@ -1052,11 +1233,41 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n } const dest = iovecs_buffer[0..i]; assert(dest[0].len > 0); - const n = try posix.readv(stream.handle, dest); + const n = try posix.readv(stream.socket.handle, dest); if (n == 0) return error.EndOfStream; return n; } +fn netSend( + userdata: ?*anyopaque, + handle: Io.net.Socket.Handle, + address: Io.net.IpAddress, + data: []const u8, +) Io.net.Socket.SendError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = handle; + _ = address; + _ = data; + @panic("TODO"); +} + +fn netReceive( + userdata: ?*anyopaque, + handle: Io.net.Socket.Handle, + address: Io.net.IpAddress, + buffer: []u8, +) Io.net.Socket.ReceiveError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = handle; + _ = address; + _ = buffer; + @panic("TODO"); +} + fn netWritePosix( userdata: ?*anyopaque, stream: Io.net.Stream, @@ -1106,7 +1317,7 @@ fn netWritePosix( }, }; const flags = posix.MSG.NOSIGNAL; - return posix.sendmsg(stream.handle, &msg, flags); + return posix.sendmsg(stream.socket.handle, &msg, flags); } fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { @@ -1117,11 +1328,13 @@ fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), i.* += 1; } -fn netClose(userdata: ?*anyopaque, stream: Io.net.Stream) void { +fn netClose(userdata: ?*anyopaque, handle: Io.net.Socket.Handle) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; - const net_stream: std.net.Stream = .{ .handle = stream.handle }; - return net_stream.close(); + switch (native_os) { + .windows => windows.closesocket(handle) catch recoverableOsBugDetected(), + else => posix.close(handle), + } } fn netInterfaceNameResolve( @@ -1153,13 +1366,13 @@ fn netInterfaceNameResolve( try pool.checkCancel(); switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) }, - .INVAL => |err| return badErrno(err), // Bad parameters. - .NOTTY => |err| return badErrno(err), - .NXIO => |err| return badErrno(err), - .BADF => |err| return badErrno(err), // Always a race condition. - .FAULT => |err| return badErrno(err), // Bad pointer parameter. + .INVAL => |err| return errnoBug(err), // Bad parameters. + .NOTTY => |err| return errnoBug(err), + .NXIO => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. + .FAULT => |err| return errnoBug(err), // Bad pointer parameter. .INTR => continue, - .IO => |err| return badErrno(err), // sock_fd is not a file descriptor + .IO => |err| return errnoBug(err), // sock_fd is not a file descriptor .NODEV => return error.InterfaceNotFound, else => |err| return posix.unexpectedErrno(err), } @@ -1207,8 +1420,8 @@ const PosixAddress = extern union { in6: posix.sockaddr.in6, }; -fn posixAddressFamily(a: Io.net.IpAddress) posix.sa_family_t { - return switch (a) { +fn posixAddressFamily(a: *const Io.net.IpAddress) posix.sa_family_t { + return switch (a.*) { .ip4 => posix.AF.INET, .ip6 => posix.AF.INET6, }; @@ -1267,9 +1480,27 @@ fn address6ToPosix(a: Io.net.Ip6Address) posix.sockaddr.in6 { }; } -fn badErrno(err: posix.E) Io.UnexpectedError { +fn errnoBug(err: posix.E) Io.UnexpectedError { switch (builtin.mode) { .Debug => std.debug.panic("programmer bug caused syscall error: {t}", .{err}), else => return error.Unexpected, } } + +fn posixSocketMode(mode: Io.net.Socket.Mode) u32 { + return switch (mode) { + .stream => posix.SOCK.STREAM, + .dgram => posix.SOCK.DGRAM, + .seqpacket => posix.SOCK.SEQPACKET, + .raw => posix.SOCK.RAW, + .rdm => posix.SOCK.RDM, + }; +} + +fn posixProtocol(protocol: ?Io.net.Protocol) u32 { + return @intFromEnum(protocol orelse return 0); +} + +fn recoverableOsBugDetected() void { + if (builtin.mode == .Debug) unreachable; +} diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index c9dc1d619a..eca6cdd1f7 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -6,26 +6,41 @@ const assert = std.debug.assert; pub const HostName = @import("net/HostName.zig"); -pub const ListenError = std.net.Address.ListenError || Io.Cancelable; - -pub const BindError = std.net.Address.BindError || Io.Cancelable; - -pub const ListenOptions = struct { - /// How many connections the kernel will accept on the application's behalf. - /// If more than this many connections pool in the kernel, clients will start - /// seeing "Connection refused". - kernel_backlog: u31 = 128, - /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. - /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. - reuse_address: bool = false, - force_nonblocking: bool = false, -}; - -pub const BindOptions = struct { - /// The socket is restricted to sending and receiving IPv6 packets only. - /// In this case, an IPv4 and an IPv6 application can bind to a single port - /// at the same time. - ip6_only: bool = false, +/// Source of truth: Internet Assigned Numbers Authority (IANA) +pub const Protocol = enum(u32) { + hopopts = 0, + icmp = 1, + igmp = 2, + ipip = 4, + tcp = 6, + egp = 8, + pup = 12, + udp = 17, + idp = 22, + tp = 29, + dccp = 33, + ipv6 = 41, + routing = 43, + fragment = 44, + rsvp = 46, + gre = 47, + esp = 50, + ah = 51, + icmpv6 = 58, + none = 59, + dstopts = 60, + mtp = 92, + beetph = 94, + encap = 98, + pim = 103, + comp = 108, + sctp = 132, + mh = 135, + udplite = 136, + mpls = 137, + ethernet = 143, + raw = 255, + mptcp = 262, }; pub const IpAddress = union(enum) { @@ -132,12 +147,70 @@ pub const IpAddress = union(enum) { }; } + pub const ListenError = error{ + /// The address is already taken. Can occur when bound port is 0 but + /// all ephemeral ports are already in use. + AddressInUse, + /// A nonexistent interface was requested or the requested address was not local. + AddressUnavailable, + /// The local network interface used to reach the destination is offline. + NetworkSubsystemDown, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// The requested address family (IPv4 or IPv6) is not supported by the operating system. + AddressFamilyUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const ListenOptions = struct { + /// How many connections the kernel will accept on the application's behalf. + /// If more than this many connections pool in the kernel, clients will start + /// seeing "Connection refused". + kernel_backlog: u31 = 128, + /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. + /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. + reuse_address: bool = false, + }; + /// Waits for a TCP connection. When using this API, `bind` does not need /// to be called. The returned `Server` has an open `stream`. pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { - return io.vtable.listen(io.userdata, address, options); + return io.vtable.tcpListen(io.userdata, address, options); } + pub const BindError = error{ + /// The address is already taken. Can occur when bound port is 0 but + /// all ephemeral ports are already in use. + AddressInUse, + /// A nonexistent interface was requested or the requested address was not local. + AddressUnavailable, + /// The address is not valid for the address family of socket. + AddressFamilyUnsupported, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// The local network interface used to reach the destination is offline. + NetworkSubsystemDown, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + SocketModeUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const BindOptions = struct { + /// The socket is restricted to sending and receiving IPv6 packets only. + /// In this case, an IPv4 and an IPv6 application can bind to a single port + /// at the same time. + ip6_only: bool = false, + mode: Socket.Mode, + protocol: ?Protocol = null, + }; + /// Associates an address with a `Socket` which can be used to receive UDP /// packets and other kinds of non-streaming messages. See `listen` for a /// streaming alternative. @@ -145,7 +218,7 @@ pub const IpAddress = union(enum) { /// One bound `Socket` can be used to receive messages from multiple /// different addresses. pub fn bind(address: IpAddress, io: Io, options: BindOptions) BindError!Socket { - return io.vtable.bind(io.userdata, address, options); + return io.vtable.ipBind(io.userdata, address, options); } }; @@ -255,7 +328,7 @@ pub const Ip6Address = struct { pub fn fromIp4(ip4: Ip4Address) Ip6Address { const b = &ip4.bytes; return .{ - .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, .port = ip4.port, }; } @@ -682,7 +755,25 @@ pub const Interface = struct { pub const Socket = struct { handle: Handle, /// Contains the resolved ephemeral port number if requested. - bind_address: IpAddress, + address: IpAddress, + + pub const Mode = enum { + /// Provides sequenced, reliable, two-way, connection-based byte + /// streams. An out-of-band data transmission mechanism may be + /// supported. + stream, + /// Supports datagrams (connectionless, unreliable messages of a fixed + /// maximum length). + dgram, + /// Provides a sequenced, reliable, two-way connection-based data + /// transmission path for datagrams of fixed maximum length; a consumer + /// is required to read an entire packet with each input system call. + seqpacket, + /// Provides raw network protocol access. + raw, + /// Provides a reliable datagram layer that does not guarantee ordering. + rdm, + }; /// Underlying platform-defined type which may or may not be /// interchangeable with a file system file descriptor. @@ -691,8 +782,48 @@ pub const Socket = struct { else => std.posix.fd_t, }; - pub fn close(s: Socket, io: Io) void { - return io.vtable.netClose(io.userdata, s); + pub fn close(s: *Socket, io: Io) void { + io.vtable.netClose(io.userdata, s.handle); + s.handle = undefined; + } + + pub const SendError = error{ + /// The socket type requires that message be sent atomically, and the size of the message + /// to be sent made this impossible. The message is not transmitted. + MessageTooBig, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. + SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is offline. + NetworkSubsystemDown, + /// The destination address is not listening. Can still occur for + /// connectionless messages. + ConnectionRefused, + /// Operating system or protocol does not support the address family. + AddressFamilyUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + /// Transfers `data` to `dest`, connectionless. + pub fn send(s: *const Socket, io: Io, dest: *const IpAddress, data: []const u8) SendError!void { + return io.vtable.netSend(io.userdata, s.handle, dest, data); + } + + pub const ReceiveError = error{} || Io.Cancelable; + + /// Transfers `data` from `source`, connectionless. + /// + /// Returned slice has same pointer as `buffer` with possibly shorter length. + pub fn receive(s: *const Socket, io: Io, source: *const IpAddress, buffer: []u8) ReceiveError![]u8 { + const n = try io.vtable.netReceive(io.userdata, s.handle, source, buffer); + return buffer[0..n]; } }; @@ -784,11 +915,6 @@ pub const Stream = struct { pub const Server = struct { socket: Socket, - pub const Connection = struct { - stream: Stream, - address: IpAddress, - }; - pub fn deinit(s: *Server, io: Io) void { s.socket.close(io); s.* = undefined; @@ -796,9 +922,8 @@ pub const Server = struct { pub const AcceptError = std.posix.AcceptError || Io.Cancelable; - /// Blocks until a client connects to the server. The returned `Connection` has - /// an open stream. - pub fn accept(s: *Server, io: Io) AcceptError!Connection { + /// Blocks until a client connects to the server. + pub fn accept(s: *Server, io: Io) AcceptError!Stream { return io.vtable.accept(io, s); } }; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 5e41e12b7d..091c6e2b96 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -539,7 +539,7 @@ pub const ResolvConf = struct { .search_buffer = undefined, .search_len = 0, .ndots = 1, - .timeout = 5, + .timeout = .seconds(5), .attempts = 2, }; @@ -589,7 +589,7 @@ pub const ResolvConf = struct { switch (std.meta.stringToEnum(Option, name) orelse continue) { .ndots => rc.ndots = @min(value, 15), .attempts => rc.attempts = @min(value, 10), - .timeout => rc.timeout = @min(value, 60), + .timeout => rc.timeout = .seconds(@min(value, 60)), } }, .nameserver => { @@ -638,14 +638,15 @@ pub const ResolvConf = struct { const socket = s: { if (any_ip6) ip6: { const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; - const socket = ip6_addr.bind(io, .{ .ip6_only = true }) catch |err| switch (err) { - error.AddressFamilyNotSupported => break :ip6, + const socket = ip6_addr.bind(io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { + error.AddressFamilyUnsupported => break :ip6, + else => |e| return e, }; break :s socket; } any_ip6 = false; const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; - const socket = try ip4_addr.bind(io, .{}); + const socket = try ip4_addr.bind(io, .{ .mode = .dgram }); break :s socket; }; defer socket.close(); diff --git a/lib/std/net.zig b/lib/std/net.zig index 5814334dd6..083ee1da93 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -2384,6 +2384,7 @@ pub const Stream = struct { } }; +/// A bound, listening TCP socket, ready to accept new connections. pub const Server = struct { listen_address: Address, stream: Stream, From f9d976a4e1616ab49664e5293508a151f11c1e08 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 26 Sep 2025 18:22:37 -0700 Subject: [PATCH 060/244] std.Io: rename asyncConcurrent to concurrent --- lib/std/Io.zig | 10 +++++----- lib/std/Io/EventLoop.zig | 6 +++--- lib/std/Io/Threaded.zig | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 359d1a496f..7db4943a84 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -583,7 +583,7 @@ pub const VTable = struct { start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*AnyFuture, /// Thread-safe. - asyncConcurrent: *const fn ( + concurrent: *const fn ( /// Corresponds to `Io.userdata`. userdata: ?*anyopaque, result_len: usize, @@ -1095,7 +1095,7 @@ pub fn Queue(Elem: type) type { /// not guaranteed to be available until `await` is called. /// /// `function` *may* be called immediately, before `async` returns. This has -/// weaker guarantees than `asyncConcurrent`, making more portable and +/// weaker guarantees than `concurrent`, making more portable and /// reusable. /// /// See also: @@ -1133,7 +1133,7 @@ pub fn async( /// This has stronger guarantee than `async`, placing restrictions on what kind /// of `Io` implementations are supported. By calling `async` instead, one /// allows, for example, stackful single-threaded blocking I/O. -pub fn asyncConcurrent( +pub fn concurrent( io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function)), @@ -1148,7 +1148,7 @@ pub fn asyncConcurrent( } }; var future: Future(Result) = undefined; - future.any_future = try io.vtable.asyncConcurrent( + future.any_future = try io.vtable.concurrent( io.userdata, @sizeOf(Result), .of(Result), @@ -1166,7 +1166,7 @@ pub fn asyncConcurrent( /// /// See also: /// * `async` -/// * `asyncConcurrent` +/// * `concurrent` pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); const TypeErased = struct { diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index e24b6b445e..90f5dbdb22 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -139,7 +139,7 @@ pub fn io(el: *EventLoop) Io { .userdata = el, .vtable = &.{ .async = async, - .asyncConcurrent = asyncConcurrent, + .concurrent = concurrent, .await = await, .asyncDetached = asyncDetached, .select = select, @@ -878,13 +878,13 @@ fn async( context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*std.Io.AnyFuture { - return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { + return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; }; } -fn asyncConcurrent( +fn concurrent( userdata: ?*anyopaque, result_len: usize, result_alignment: Alignment, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 07f87fdf0c..762eb81060 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -99,7 +99,7 @@ pub fn io(pool: *Pool) Io { .userdata = pool, .vtable = &.{ .async = async, - .asyncConcurrent = asyncConcurrent, + .concurrent = concurrent, .await = await, .asyncDetached = asyncDetached, .cancel = cancel, @@ -261,7 +261,7 @@ fn async( } const pool: *Pool = @ptrCast(@alignCast(userdata)); const cpu_count = pool.cpu_count catch { - return asyncConcurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { + return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; }; @@ -325,7 +325,7 @@ fn async( return @ptrCast(closure); } -fn asyncConcurrent( +fn concurrent( userdata: ?*anyopaque, result_len: usize, result_alignment: std.mem.Alignment, From 5469db66e4b3f1ffe2ed9b58d5e12dc5a9c815f0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 26 Sep 2025 21:58:06 -0700 Subject: [PATCH 061/244] std.Thread.ResetEvent: make it more reusable --- CMakeLists.txt | 1 - lib/std/Progress.zig | 2 +- lib/std/Thread.zig | 244 ++++++++++++++++++++++++++++- lib/std/Thread/ResetEvent.zig | 278 ---------------------------------- lib/std/Thread/WaitGroup.zig | 29 ++-- 5 files changed, 264 insertions(+), 290 deletions(-) delete mode 100644 lib/std/Thread/ResetEvent.zig diff --git a/CMakeLists.txt b/CMakeLists.txt index 6efe4e7490..d3988946b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -413,7 +413,6 @@ set(ZIG_STAGE2_SOURCES lib/std/Thread/Futex.zig lib/std/Thread/Mutex.zig lib/std/Thread/Pool.zig - lib/std/Thread/ResetEvent.zig lib/std/Thread/WaitGroup.zig lib/std/array_hash_map.zig lib/std/array_list.zig diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index fb06fca2e9..102b4d8404 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -392,7 +392,7 @@ var global_progress: Progress = .{ .terminal = undefined, .terminal_mode = .off, .update_thread = null, - .redraw_event = .{}, + .redraw_event = .unset, .refresh_rate_ns = undefined, .initial_delay_ns = undefined, .rows = 0, diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 3d78596f84..46698cbe99 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -10,9 +10,9 @@ const target = builtin.target; const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; +const testing = std.testing; pub const Futex = @import("Thread/Futex.zig"); -pub const ResetEvent = @import("Thread/ResetEvent.zig"); pub const Mutex = @import("Thread/Mutex.zig"); pub const Semaphore = @import("Thread/Semaphore.zig"); pub const Condition = @import("Thread/Condition.zig"); @@ -22,6 +22,126 @@ pub const WaitGroup = @import("Thread/WaitGroup.zig"); pub const use_pthreads = native_os != .windows and native_os != .wasi and builtin.link_libc; +/// A thread-safe logical boolean value which can be `set` and `unset`. +/// +/// It can also block threads until the value is set with cancelation via timed +/// waits. Statically initializable; four bytes on all targets. +pub const ResetEvent = enum(u32) { + unset = 0, + waiting = 1, + is_set = 2, + + /// Returns whether the logical boolean is `set`. + /// + /// Once `reset` is called, this returns false until the next `set`. + /// + /// The memory accesses before the `set` can be said to happen before + /// `isSet` returns true. + pub fn isSet(re: *const ResetEvent) bool { + if (builtin.single_threaded) return switch (re.*) { + .unset => false, + .waiting => unreachable, + .is_set => true, + }; + // Acquire barrier ensures memory accesses before `set` happen before + // returning true. + return @atomicLoad(ResetEvent, re, .acquire) == .is_set; + } + + /// Blocks the calling thread until `set` is called. + /// + /// This is effectively a more efficient version of `while (!isSet()) {}`. + /// + /// The memory accesses before the `set` can be said to happen before `wait` returns. + pub fn wait(re: *ResetEvent) void { + if (builtin.single_threaded) switch (re.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + if (!re.isSet()) return timedWaitInner(re, null) catch |err| switch (err) { + error.Timeout => unreachable, // No timeout specified. + }; + } + + /// Blocks the calling thread until `set` is called, or until the + /// corresponding timeout expires, returning `error.Timeout`. + /// + /// This is effectively a more efficient version of `while (!isSet()) {}`. + /// + /// The memory accesses before the set() can be said to happen before + /// timedWait() returns without error. + pub fn timedWait(re: *ResetEvent, timeout_ns: u64) void { + if (builtin.single_threaded) switch (re.*) { + .unset => { + sleep(timeout_ns); + return error.Timeout; + }, + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + if (!re.isSet()) return timedWaitInner(re, timeout_ns); + } + + fn timedWaitInner(re: *ResetEvent, timeout: ?u64) error{Timeout}!void { + @branchHint(.cold); + + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEvent. Avoid using + // any strict barriers until we know the ResetEvent is set. + var state = @atomicLoad(ResetEvent, re, .acquire); + if (state == .unset) { + state = @cmpxchgStrong(ResetEvent, re, state, .waiting, .acquire, .acquire) orelse .waiting; + } + + // Wait until the ResetEvent is set since the state is waiting. + if (state == .waiting) { + var futex_deadline = Futex.Deadline.init(timeout); + while (true) { + const wait_result = futex_deadline.wait(@ptrCast(re), @intFromEnum(ResetEvent.waiting)); + + // Check if the ResetEvent was set before possibly reporting error.Timeout below. + state = @atomicLoad(ResetEvent, re, .acquire); + if (state != .waiting) break; + + try wait_result; + } + } + + assert(state == .is_set); + } + + /// Marks the logical boolean as `set` and unblocks any threads in `wait` + /// or `timedWait` to observe the new state. + /// + /// The logical boolean stays `set` until `reset` is called, making future + /// `set` calls do nothing semantically. + /// + /// The memory accesses before `set` can be said to happen before `isSet` + /// returns true or `wait`/`timedWait` return successfully. + pub fn set(re: *ResetEvent) void { + if (builtin.single_threaded) { + re.* = .is_set; + return; + } + if (@atomicRmw(ResetEvent, re, .Xchg, .is_set, .release) == .waiting) { + Futex.wake(@ptrCast(re), std.math.maxInt(u32)); + } + } + + /// Unmarks the ResetEvent as if `set` was never called. + /// + /// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent + /// calls to `set`, `isSet` and `reset` are allowed. + pub fn reset(re: *ResetEvent) void { + if (builtin.single_threaded) { + re.* = .unset; + return; + } + @atomicStore(ResetEvent, re, .unset, .monotonic); + } +}; + /// Spurious wakeups are possible and no precision of timing is guaranteed. pub fn sleep(nanoseconds: u64) void { if (builtin.os.tag == .windows) { @@ -1780,3 +1900,125 @@ fn testTls() !void { x += 1; if (x != 1235) return error.TlsBadEndValue; } + +test "ResetEvent smoke test" { + // make sure the event is unset + var event = ResetEvent{}; + try testing.expectEqual(false, event.isSet()); + + // make sure the event gets set + event.set(); + try testing.expectEqual(true, event.isSet()); + + // make sure the event gets unset again + event.reset(); + try testing.expectEqual(false, event.isSet()); + + // waits should timeout as there's no other thread to set the event + try testing.expectError(error.Timeout, event.timedWait(0)); + try testing.expectError(error.Timeout, event.timedWait(std.time.ns_per_ms)); + + // set the event again and make sure waits complete + event.set(); + event.wait(); + try event.timedWait(std.time.ns_per_ms); + try testing.expectEqual(true, event.isSet()); +} + +test "ResetEvent signaling" { + // This test requires spawning threads + if (builtin.single_threaded) { + return error.SkipZigTest; + } + + const Context = struct { + in: ResetEvent = .{}, + out: ResetEvent = .{}, + value: usize = 0, + + fn input(self: *@This()) !void { + // wait for the value to become 1 + self.in.wait(); + self.in.reset(); + try testing.expectEqual(self.value, 1); + + // bump the value and wake up output() + self.value = 2; + self.out.set(); + + // wait for output to receive 2, bump the value and wake us up with 3 + self.in.wait(); + self.in.reset(); + try testing.expectEqual(self.value, 3); + + // bump the value and wake up output() for it to see 4 + self.value = 4; + self.out.set(); + } + + fn output(self: *@This()) !void { + // start with 0 and bump the value for input to see 1 + try testing.expectEqual(self.value, 0); + self.value = 1; + self.in.set(); + + // wait for input to receive 1, bump the value to 2 and wake us up + self.out.wait(); + self.out.reset(); + try testing.expectEqual(self.value, 2); + + // bump the value to 3 for input to see (rhymes) + self.value = 3; + self.in.set(); + + // wait for input to bump the value to 4 and receive no more (rhymes) + self.out.wait(); + self.out.reset(); + try testing.expectEqual(self.value, 4); + } + }; + + var ctx = Context{}; + + const thread = try std.Thread.spawn(.{}, Context.output, .{&ctx}); + defer thread.join(); + + try ctx.input(); +} + +test "ResetEvent broadcast" { + // This test requires spawning threads + if (builtin.single_threaded) { + return error.SkipZigTest; + } + + const num_threads = 10; + const Barrier = struct { + event: ResetEvent = .{}, + counter: std.atomic.Value(usize) = std.atomic.Value(usize).init(num_threads), + + fn wait(self: *@This()) void { + if (self.counter.fetchSub(1, .acq_rel) == 1) { + self.event.set(); + } + } + }; + + const Context = struct { + start_barrier: Barrier = .{}, + finish_barrier: Barrier = .{}, + + fn run(self: *@This()) void { + self.start_barrier.wait(); + self.finish_barrier.wait(); + } + }; + + var ctx = Context{}; + var threads: [num_threads - 1]std.Thread = undefined; + + for (&threads) |*t| t.* = try std.Thread.spawn(.{}, Context.run, .{&ctx}); + defer for (threads) |t| t.join(); + + ctx.run(); +} diff --git a/lib/std/Thread/ResetEvent.zig b/lib/std/Thread/ResetEvent.zig deleted file mode 100644 index 2f22d8456a..0000000000 --- a/lib/std/Thread/ResetEvent.zig +++ /dev/null @@ -1,278 +0,0 @@ -//! ResetEvent is a thread-safe bool which can be set to true/false ("set"/"unset"). -//! It can also block threads until the "bool" is set with cancellation via timed waits. -//! ResetEvent can be statically initialized and is at most `@sizeOf(u64)` large. - -const std = @import("../std.zig"); -const builtin = @import("builtin"); -const ResetEvent = @This(); - -const os = std.os; -const assert = std.debug.assert; -const testing = std.testing; -const Futex = std.Thread.Futex; - -impl: Impl = .{}, - -/// Returns if the ResetEvent was set(). -/// Once reset() is called, this returns false until the next set(). -/// The memory accesses before the set() can be said to happen before isSet() returns true. -pub fn isSet(self: *const ResetEvent) bool { - return self.impl.isSet(); -} - -/// Block's the callers thread until the ResetEvent is set(). -/// This is effectively a more efficient version of `while (!isSet()) {}`. -/// The memory accesses before the set() can be said to happen before wait() returns. -pub fn wait(self: *ResetEvent) void { - self.impl.wait(null) catch |err| switch (err) { - error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out - }; -} - -/// Block's the callers thread until the ResetEvent is set(), or until the corresponding timeout expires. -/// If the timeout expires before the ResetEvent is set, `error.Timeout` is returned. -/// This is effectively a more efficient version of `while (!isSet()) {}`. -/// The memory accesses before the set() can be said to happen before timedWait() returns without error. -pub fn timedWait(self: *ResetEvent, timeout_ns: u64) error{Timeout}!void { - return self.impl.wait(timeout_ns); -} - -/// Marks the ResetEvent as "set" and unblocks any threads in `wait()` or `timedWait()` to observe the new state. -/// The ResetEvent says "set" until reset() is called, making future set() calls do nothing semantically. -/// The memory accesses before set() can be said to happen before isSet() returns true or wait()/timedWait() return successfully. -pub fn set(self: *ResetEvent) void { - self.impl.set(); -} - -/// Unmarks the ResetEvent from its "set" state if set() was called previously. -/// It is undefined behavior is reset() is called while threads are blocked in wait() or timedWait(). -/// Concurrent calls to set(), isSet() and reset() are allowed. -pub fn reset(self: *ResetEvent) void { - self.impl.reset(); -} - -const Impl = if (builtin.single_threaded) - SingleThreadedImpl -else - FutexImpl; - -const SingleThreadedImpl = struct { - is_set: bool = false, - - fn isSet(self: *const Impl) bool { - return self.is_set; - } - - fn wait(self: *Impl, timeout: ?u64) error{Timeout}!void { - if (self.isSet()) { - return; - } - - // There are no other threads to wake us up. - // So if we wait without a timeout we would never wake up. - const timeout_ns = timeout orelse { - unreachable; // deadlock detected - }; - - std.Thread.sleep(timeout_ns); - return error.Timeout; - } - - fn set(self: *Impl) void { - self.is_set = true; - } - - fn reset(self: *Impl) void { - self.is_set = false; - } -}; - -const FutexImpl = struct { - state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unset), - - const unset = 0; - const waiting = 1; - const is_set = 2; - - fn isSet(self: *const Impl) bool { - // Acquire barrier ensures memory accesses before set() happen before we return true. - return self.state.load(.acquire) == is_set; - } - - fn wait(self: *Impl, timeout: ?u64) error{Timeout}!void { - // Outline the slow path to allow isSet() to be inlined - if (!self.isSet()) { - return self.waitUntilSet(timeout); - } - } - - fn waitUntilSet(self: *Impl, timeout: ?u64) error{Timeout}!void { - @branchHint(.cold); - - // Try to set the state from `unset` to `waiting` to indicate - // to the set() thread that others are blocked on the ResetEvent. - // We avoid using any strict barriers until the end when we know the ResetEvent is set. - var state = self.state.load(.acquire); - if (state == unset) { - state = self.state.cmpxchgStrong(state, waiting, .acquire, .acquire) orelse waiting; - } - - // Wait until the ResetEvent is set since the state is waiting. - if (state == waiting) { - var futex_deadline = Futex.Deadline.init(timeout); - while (true) { - const wait_result = futex_deadline.wait(&self.state, waiting); - - // Check if the ResetEvent was set before possibly reporting error.Timeout below. - state = self.state.load(.acquire); - if (state != waiting) { - break; - } - - try wait_result; - } - } - - assert(state == is_set); - } - - fn set(self: *Impl) void { - // Quick check if the ResetEvent is already set before doing the atomic swap below. - // set() could be getting called quite often and multiple threads calling swap() increases contention unnecessarily. - if (self.state.load(.monotonic) == is_set) { - return; - } - - // Mark the ResetEvent as set and unblock all waiters waiting on it if any. - // Release barrier ensures memory accesses before set() happen before the ResetEvent is observed to be "set". - if (self.state.swap(is_set, .release) == waiting) { - Futex.wake(&self.state, std.math.maxInt(u32)); - } - } - - fn reset(self: *Impl) void { - self.state.store(unset, .monotonic); - } -}; - -test "smoke test" { - // make sure the event is unset - var event = ResetEvent{}; - try testing.expectEqual(false, event.isSet()); - - // make sure the event gets set - event.set(); - try testing.expectEqual(true, event.isSet()); - - // make sure the event gets unset again - event.reset(); - try testing.expectEqual(false, event.isSet()); - - // waits should timeout as there's no other thread to set the event - try testing.expectError(error.Timeout, event.timedWait(0)); - try testing.expectError(error.Timeout, event.timedWait(std.time.ns_per_ms)); - - // set the event again and make sure waits complete - event.set(); - event.wait(); - try event.timedWait(std.time.ns_per_ms); - try testing.expectEqual(true, event.isSet()); -} - -test "signaling" { - // This test requires spawning threads - if (builtin.single_threaded) { - return error.SkipZigTest; - } - - const Context = struct { - in: ResetEvent = .{}, - out: ResetEvent = .{}, - value: usize = 0, - - fn input(self: *@This()) !void { - // wait for the value to become 1 - self.in.wait(); - self.in.reset(); - try testing.expectEqual(self.value, 1); - - // bump the value and wake up output() - self.value = 2; - self.out.set(); - - // wait for output to receive 2, bump the value and wake us up with 3 - self.in.wait(); - self.in.reset(); - try testing.expectEqual(self.value, 3); - - // bump the value and wake up output() for it to see 4 - self.value = 4; - self.out.set(); - } - - fn output(self: *@This()) !void { - // start with 0 and bump the value for input to see 1 - try testing.expectEqual(self.value, 0); - self.value = 1; - self.in.set(); - - // wait for input to receive 1, bump the value to 2 and wake us up - self.out.wait(); - self.out.reset(); - try testing.expectEqual(self.value, 2); - - // bump the value to 3 for input to see (rhymes) - self.value = 3; - self.in.set(); - - // wait for input to bump the value to 4 and receive no more (rhymes) - self.out.wait(); - self.out.reset(); - try testing.expectEqual(self.value, 4); - } - }; - - var ctx = Context{}; - - const thread = try std.Thread.spawn(.{}, Context.output, .{&ctx}); - defer thread.join(); - - try ctx.input(); -} - -test "broadcast" { - // This test requires spawning threads - if (builtin.single_threaded) { - return error.SkipZigTest; - } - - const num_threads = 10; - const Barrier = struct { - event: ResetEvent = .{}, - counter: std.atomic.Value(usize) = std.atomic.Value(usize).init(num_threads), - - fn wait(self: *@This()) void { - if (self.counter.fetchSub(1, .acq_rel) == 1) { - self.event.set(); - } - } - }; - - const Context = struct { - start_barrier: Barrier = .{}, - finish_barrier: Barrier = .{}, - - fn run(self: *@This()) void { - self.start_barrier.wait(); - self.finish_barrier.wait(); - } - }; - - var ctx = Context{}; - var threads: [num_threads - 1]std.Thread = undefined; - - for (&threads) |*t| t.* = try std.Thread.spawn(.{}, Context.run, .{&ctx}); - defer for (threads) |t| t.join(); - - ctx.run(); -} diff --git a/lib/std/Thread/WaitGroup.zig b/lib/std/Thread/WaitGroup.zig index 52e9c379c2..a5970b7d69 100644 --- a/lib/std/Thread/WaitGroup.zig +++ b/lib/std/Thread/WaitGroup.zig @@ -7,11 +7,15 @@ const is_waiting: usize = 1 << 0; const one_pending: usize = 1 << 1; state: std.atomic.Value(usize) = std.atomic.Value(usize).init(0), -event: std.Thread.ResetEvent = .{}, +event: std.Thread.ResetEvent = .unset, pub fn start(self: *WaitGroup) void { - const state = self.state.fetchAdd(one_pending, .monotonic); - assert((state / one_pending) < (std.math.maxInt(usize) / one_pending)); + return startStateless(&self.state); +} + +pub fn startStateless(state: *std.atomic.Value(usize)) void { + const prev_state = state.fetchAdd(one_pending, .monotonic); + assert((prev_state / one_pending) < (std.math.maxInt(usize) / one_pending)); } pub fn startMany(self: *WaitGroup, n: usize) void { @@ -28,13 +32,20 @@ pub fn finish(self: *WaitGroup) void { } } -pub fn wait(self: *WaitGroup) void { - const state = self.state.fetchAdd(is_waiting, .acquire); - assert(state & is_waiting == 0); +pub fn finishStateless(state: *std.atomic.Value(usize), event: *std.Thread.ResetEvent) void { + const prev_state = state.fetchSub(one_pending, .acq_rel); + assert((prev_state / one_pending) > 0); + if (prev_state == (one_pending | is_waiting)) event.set(); +} - if ((state / one_pending) > 0) { - self.event.wait(); - } +pub fn wait(wg: *WaitGroup) void { + return waitStateless(&wg.state, &wg.event); +} + +pub fn waitStateless(state: *std.atomic.Value(usize), event: *std.Thread.ResetEvent) void { + const prev_state = state.fetchAdd(is_waiting, .acquire); + assert(prev_state & is_waiting == 0); + if ((prev_state / one_pending) > 0) event.wait(); } pub fn reset(self: *WaitGroup) void { From 8e1da66ba11a4fca559ce29ff962051fdc25f13a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 26 Sep 2025 21:58:51 -0700 Subject: [PATCH 062/244] std.Io: implement Group API --- lib/std/Io.zig | 89 ++++++++++++++++++++++++------------- lib/std/Io/Threaded.zig | 58 +++++++++++++++++------- lib/std/Io/net/HostName.zig | 4 +- 3 files changed, 102 insertions(+), 49 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 7db4943a84..363aaeb787 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -593,18 +593,6 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) error{OutOfMemory}!*AnyFuture, - /// Executes `start` asynchronously in a manner such that it cleans itself - /// up. This mode does not support results, await, or cancel. - /// - /// Thread-safe. - asyncDetached: *const fn ( - /// Corresponds to `Io.userdata`. - userdata: ?*anyopaque, - /// Copied and then passed to `start`. - context: []const u8, - context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) void, - ) void, /// This function is only called when `async` returns a non-null value. /// /// Thread-safe. @@ -639,6 +627,23 @@ pub const VTable = struct { /// Thread-safe. cancelRequested: *const fn (?*anyopaque) bool, + /// Executes `start` asynchronously in a manner such that it cleans itself + /// up. This mode does not support results, await, or cancel. + /// + /// Thread-safe. + groupAsync: *const fn ( + /// Corresponds to `Io.userdata`. + userdata: ?*anyopaque, + /// Owner of the spawned async task. + group: *Group, + /// Copied and then passed to `start`. + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque) void, + ) void, + groupWait: *const fn (?*anyopaque, *Group) void, + groupCancel: *const fn (?*anyopaque, *Group) void, + /// Blocks until one of the futures from the list has a result ready, such /// that awaiting it will not block. Returns that index. select: *const fn (?*anyopaque, futures: []const *AnyFuture) usize, @@ -751,6 +756,45 @@ pub fn Future(Result: type) type { }; } +pub const Group = struct { + state: usize, + context: ?*anyopaque, + + pub const init: Group = .{ .state = 0, .context = null }; + + /// Calls `function` with `args` asynchronously. The resource spawned is + /// owned by the group. + /// + /// `function` *may* be called immediately, before `async` returns. + /// + /// After this is called, `wait` must be called before the group is + /// deinitialized. + /// + /// See also: + /// * `async` + /// * `concurrent` + pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(context: *const anyopaque) void { + const args_casted: *const Args = @ptrCast(@alignCast(context)); + @call(.auto, function, args_casted.*); + } + }; + io.vtable.groupAsync(io.userdata, g, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + } + + /// Idempotent. + pub fn wait(g: *Group, io: Io) void { + io.vtable.groupWait(io.userdata, g); + } + + /// Idempotent. + pub fn cancel(g: *Group, io: Io) void { + io.vtable.groupCancel(io.userdata, g); + } +}; + pub const Mutex = if (true) struct { state: State, @@ -1099,7 +1143,7 @@ pub fn Queue(Elem: type) type { /// reusable. /// /// See also: -/// * `asyncDetached` +/// * `Group` pub fn async( io: Io, function: anytype, @@ -1159,25 +1203,6 @@ pub fn concurrent( return future; } -/// Calls `function` with `args` asynchronously. The resource cleans itself up -/// when the function returns. Does not support await, cancel, or a return value. -/// -/// `function` *may* be called immediately, before `async` returns. -/// -/// See also: -/// * `async` -/// * `concurrent` -pub fn asyncDetached(io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { - const Args = @TypeOf(args); - const TypeErased = struct { - fn start(context: *const anyopaque) void { - const args_casted: *const Args = @ptrCast(@alignCast(context)); - @call(.auto, function, args_casted.*); - } - }; - io.vtable.asyncDetached(io.userdata, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); -} - pub fn cancelRequested(io: Io) bool { return io.vtable.cancelRequested(io.userdata); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 762eb81060..1459f2efba 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -8,7 +8,6 @@ const windows = std.os.windows; const std = @import("../std.zig"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const WaitGroup = std.Thread.WaitGroup; const posix = std.posix; const Io = std.Io; @@ -101,10 +100,12 @@ pub fn io(pool: *Pool) Io { .async = async, .concurrent = concurrent, .await = await, - .asyncDetached = asyncDetached, .cancel = cancel, .cancelRequested = cancelRequested, .select = select, + .groupAsync = groupAsync, + .groupWait = groupWait, + .groupCancel = groupCancel, .mutexLock = mutexLock, .mutexUnlock = mutexUnlock, @@ -279,7 +280,7 @@ fn async( .func = start, .context_offset = context_offset, .result_offset = result_offset, - .reset_event = .{}, + .reset_event = .unset, .cancel_tid = 0, .select_condition = null, .runnable = .{ @@ -347,7 +348,7 @@ fn concurrent( .func = start, .context_offset = context_offset, .result_offset = result_offset, - .reset_event = .{}, + .reset_event = .unset, .cancel_tid = 0, .select_condition = null, .runnable = .{ @@ -385,41 +386,47 @@ fn concurrent( return @ptrCast(closure); } -const DetachedClosure = struct { +const GroupClosure = struct { pool: *Pool, + group: *Io.Group, func: *const fn (context: *anyopaque) void, runnable: Runnable, context_alignment: std.mem.Alignment, context_len: usize, fn start(runnable: *Runnable) void { - const closure: *DetachedClosure = @alignCast(@fieldParentPtr("runnable", runnable)); + const closure: *GroupClosure = @alignCast(@fieldParentPtr("runnable", runnable)); closure.func(closure.contextPointer()); + const group = closure.group; const gpa = closure.pool.allocator; free(closure, gpa); + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *std.Thread.ResetEvent = @ptrCast(&group.context); + std.Thread.WaitGroup.finishStateless(group_state, reset_event); } - fn free(closure: *DetachedClosure, gpa: Allocator) void { - const base: [*]align(@alignOf(DetachedClosure)) u8 = @ptrCast(closure); + fn free(closure: *GroupClosure, gpa: Allocator) void { + const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(closure); gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]); } fn contextOffset(context_alignment: std.mem.Alignment) usize { - return context_alignment.forward(@sizeOf(DetachedClosure)); + return context_alignment.forward(@sizeOf(GroupClosure)); } fn contextEnd(context_alignment: std.mem.Alignment, context_len: usize) usize { return contextOffset(context_alignment) + context_len; } - fn contextPointer(closure: *DetachedClosure) [*]u8 { + fn contextPointer(closure: *GroupClosure) [*]u8 { const base: [*]u8 = @ptrCast(closure); return base + contextOffset(closure.context_alignment); } }; -fn asyncDetached( +fn groupAsync( userdata: ?*anyopaque, + group: *Io.Group, context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque) void, @@ -428,17 +435,18 @@ fn asyncDetached( const pool: *Pool = @ptrCast(@alignCast(userdata)); const cpu_count = pool.cpu_count catch 1; const gpa = pool.allocator; - const n = DetachedClosure.contextEnd(context_alignment, context.len); - const closure: *DetachedClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(DetachedClosure), n) catch { + const n = GroupClosure.contextEnd(context_alignment, context.len); + const closure: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { return start(context.ptr); })); closure.* = .{ .pool = pool, + .group = group, .func = start, .context_alignment = context_alignment, .context_len = context.len, .runnable = .{ - .start = DetachedClosure.start, + .start = GroupClosure.start, .is_parallel = false, }, }; @@ -466,10 +474,30 @@ fn asyncDetached( pool.threads.appendAssumeCapacity(thread); } + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + std.Thread.WaitGroup.startStateless(group_state); + pool.mutex.unlock(); pool.cond.signal(); } +fn groupWait(userdata: ?*anyopaque, group: *Io.Group) void { + if (builtin.single_threaded) return; + const pool: *Pool = @ptrCast(@alignCast(userdata)); + _ = pool; + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *std.Thread.ResetEvent = @ptrCast(&group.context); + std.Thread.WaitGroup.waitStateless(group_state, reset_event); +} + +fn groupCancel(userdata: ?*anyopaque, group: *Io.Group) void { + if (builtin.single_threaded) return; + const pool: *Pool = @ptrCast(@alignCast(userdata)); + _ = pool; + _ = group; + @panic("TODO threaded group cancel"); +} + fn await( userdata: ?*anyopaque, any_future: *Io.AnyFuture, @@ -968,7 +996,7 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; - var reset_event: std.Thread.ResetEvent = .{}; + var reset_event: std.Thread.ResetEvent = .unset; for (futures, 0..) |future, i| { const closure: *AsyncClosure = @ptrCast(@alignCast(future)); diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 091c6e2b96..907f5bca1b 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -653,7 +653,7 @@ pub const ResolvConf = struct { const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); - var group: Io.Group = .{}; + var group: Io.Group = .init; defer group.cancel(); for (queries) |query| { @@ -702,7 +702,7 @@ test ResolvConf { .search_buffer = undefined, .search_len = 0, .ndots = 1, - .timeout = 5, + .timeout = .seconds(5), .attempts = 2, }; From 2e1ab5d3f791c596a663a2c83cd751e3729e7a44 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 29 Sep 2025 14:04:07 -0700 Subject: [PATCH 063/244] std.Io.Threaded: implement Group.cancel --- lib/std/Io.zig | 22 ++- lib/std/Io/Threaded.zig | 349 +++++++++++++++++++++------------------- 2 files changed, 194 insertions(+), 177 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 363aaeb787..5514cff6f9 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -736,10 +736,9 @@ pub fn Future(Result: type) type { any_future: ?*AnyFuture, result: Result, - /// Equivalent to `await` but sets a flag observable to application - /// code that cancellation has been requested. + /// Equivalent to `await` but places a cancellation request. /// - /// Idempotent. + /// Idempotent. Not threadsafe. pub fn cancel(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; io.vtable.cancel(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); @@ -747,6 +746,7 @@ pub fn Future(Result: type) type { return f.result; } + /// Idempotent. Not threadsafe. pub fn await(f: *@This(), io: Io) Result { const any_future = f.any_future orelse return f.result; io.vtable.await(io.userdata, any_future, @ptrCast((&f.result)[0..1]), .of(Result)); @@ -759,8 +759,9 @@ pub fn Future(Result: type) type { pub const Group = struct { state: usize, context: ?*anyopaque, + token: ?*anyopaque, - pub const init: Group = .{ .state = 0, .context = null }; + pub const init: Group = .{ .state = 0, .context = null, .token = null }; /// Calls `function` with `args` asynchronously. The resource spawned is /// owned by the group. @@ -771,7 +772,7 @@ pub const Group = struct { /// deinitialized. /// /// See also: - /// * `async` + /// * `Io.async` /// * `concurrent` pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); @@ -784,14 +785,21 @@ pub const Group = struct { io.vtable.groupAsync(io.userdata, g, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); } - /// Idempotent. + /// Blocks until all tasks of the group finish. + /// + /// Idempotent. Not threadsafe. pub fn wait(g: *Group, io: Io) void { io.vtable.groupWait(io.userdata, g); } - /// Idempotent. + /// Equivalent to `wait` but requests cancellation on all tasks owned by + /// the group. + /// + /// Idempotent. Not threadsafe. pub fn cancel(g: *Group, io: Io) void { + if (g.token == null) return; io.vtable.groupCancel(io.userdata, g); + assert(g.token == null); } }; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 1459f2efba..db0dad5669 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -10,6 +10,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const posix = std.posix; const Io = std.Io; +const ResetEvent = std.Thread.ResetEvent; /// Thread-safe. allocator: Allocator, @@ -20,9 +21,9 @@ join_requested: bool = false, threads: std.ArrayListUnmanaged(std.Thread), stack_size: usize, cpu_count: std.Thread.CpuCountError!usize, -parallel_count: usize, +concurrent_count: usize, -threadlocal var current_closure: ?*AsyncClosure = null; +threadlocal var current_closure: ?*Closure = null; const max_iovecs_len = 8; const splat_buffer_size = 64; @@ -31,12 +32,33 @@ comptime { assert(max_iovecs_len <= posix.IOV_MAX); } -pub const Runnable = struct { +const Closure = struct { start: Start, node: std.SinglyLinkedList.Node = .{}, - is_parallel: bool, + cancel_tid: std.Thread.Id, + /// Whether this task bumps minimum number of threads in the pool. + is_concurrent: bool, - pub const Start = *const fn (*Runnable) void; + const Start = *const fn (*Closure) void; + + const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { + .int => |int_info| switch (int_info.signedness) { + .signed => -1, + .unsigned => std.math.maxInt(std.Thread.Id), + }, + .pointer => @ptrFromInt(std.math.maxInt(usize)), + else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), + }; + + fn requestCancel(closure: *Closure) void { + switch (@atomicRmw(std.Thread.Id, &closure.cancel_tid, .Xchg, canceling_tid, .acq_rel)) { + 0, canceling_tid => {}, + else => |tid| switch (builtin.os.tag) { + .linux => _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid), posix.SIG.IO), + else => {}, + }, + } + } }; pub const InitError = std.Thread.CpuCountError || Allocator.Error; @@ -47,7 +69,7 @@ pub fn init(gpa: Allocator) Pool { .threads = .empty, .stack_size = std.Thread.SpawnConfig.default_stack_size, .cpu_count = std.Thread.getCpuCount(), - .parallel_count = 0, + .concurrent_count = 0, }; if (pool.cpu_count) |n| { pool.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {}; @@ -78,14 +100,15 @@ fn worker(pool: *Pool) void { defer pool.mutex.unlock(); while (true) { - while (pool.run_queue.popFirst()) |run_node| { + while (pool.run_queue.popFirst()) |closure_node| { pool.mutex.unlock(); - const runnable: *Runnable = @fieldParentPtr("node", run_node); - runnable.start(runnable); + const closure: *Closure = @fieldParentPtr("node", closure_node); + const is_concurrent = closure.is_concurrent; + closure.start(closure); pool.mutex.lock(); - if (runnable.is_parallel) { + if (is_concurrent) { // TODO also pop thread and join sometimes - pool.parallel_count -= 1; + pool.concurrent_count -= 1; } } if (pool.join_requested) break; @@ -154,97 +177,71 @@ pub fn io(pool: *Pool) Io { }; } +/// Trailing data: +/// 1. context +/// 2. result const AsyncClosure = struct { + closure: Closure, func: *const fn (context: *anyopaque, result: *anyopaque) void, - runnable: Runnable, - reset_event: std.Thread.ResetEvent, - select_condition: ?*std.Thread.ResetEvent, - cancel_tid: std.Thread.Id, - context_offset: usize, + reset_event: ResetEvent, + select_condition: ?*ResetEvent, + context_alignment: std.mem.Alignment, result_offset: usize, + /// Whether the task has a return type with nonzero bits. + has_result: bool, - const done_reset_event: *std.Thread.ResetEvent = @ptrFromInt(@alignOf(std.Thread.ResetEvent)); + const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent)); - const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { - .int => |int_info| switch (int_info.signedness) { - .signed => -1, - .unsigned => std.math.maxInt(std.Thread.Id), - }, - .pointer => @ptrFromInt(std.math.maxInt(usize)), - else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), - }; - - fn start(runnable: *Runnable) void { - const closure: *AsyncClosure = @alignCast(@fieldParentPtr("runnable", runnable)); + fn start(closure: *Closure) void { + const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure)); const tid = std.Thread.getCurrentId(); - if (@cmpxchgStrong( - std.Thread.Id, - &closure.cancel_tid, - 0, - tid, - .acq_rel, - .acquire, - )) |cancel_tid| { - assert(cancel_tid == canceling_tid); - closure.reset_event.set(); - return; + if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, 0, tid, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == Closure.canceling_tid); + // Even though we already know the task is canceled, we must still + // run the closure in order to make the return value valid - that + // is, unless the result is zero bytes! + if (!ac.has_result) { + ac.reset_event.set(); + return; + } } current_closure = closure; - closure.func(closure.contextPointer(), closure.resultPointer()); + ac.func(ac.contextPointer(), ac.resultPointer()); current_closure = null; - if (@cmpxchgStrong( - std.Thread.Id, - &closure.cancel_tid, - tid, - 0, - .acq_rel, - .acquire, - )) |cancel_tid| assert(cancel_tid == canceling_tid); - if (@atomicRmw( - ?*std.Thread.ResetEvent, - &closure.select_condition, - .Xchg, - done_reset_event, - .release, - )) |select_reset| { + // In case a cancel happens after successful task completion, prevents + // signal from being delivered to the thread in `requestCancel`. + if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, tid, 0, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == Closure.canceling_tid); + } + + if (@atomicRmw(?*ResetEvent, &ac.select_condition, .Xchg, done_reset_event, .release)) |select_reset| { assert(select_reset != done_reset_event); select_reset.set(); } - closure.reset_event.set(); + ac.reset_event.set(); } - fn contextOffset(context_alignment: std.mem.Alignment) usize { - return context_alignment.forward(@sizeOf(AsyncClosure)); + fn resultPointer(ac: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(ac); + return base + ac.result_offset; } - fn resultOffset( - context_alignment: std.mem.Alignment, - context_len: usize, - result_alignment: std.mem.Alignment, - ) usize { - return result_alignment.forward(contextOffset(context_alignment) + context_len); + fn contextPointer(ac: *AsyncClosure) [*]u8 { + const base: [*]u8 = @ptrCast(ac); + return base + ac.context_alignment.forward(@sizeOf(AsyncClosure)); } - fn resultPointer(closure: *AsyncClosure) [*]u8 { - const base: [*]u8 = @ptrCast(closure); - return base + closure.result_offset; + fn waitAndFree(ac: *AsyncClosure, gpa: Allocator, result: []u8) void { + ac.reset_event.wait(); + @memcpy(result, ac.resultPointer()[0..result.len]); + free(ac, gpa, result.len); } - fn contextPointer(closure: *AsyncClosure) [*]u8 { - const base: [*]u8 = @ptrCast(closure); - return base + closure.context_offset; - } - - fn waitAndFree(closure: *AsyncClosure, gpa: Allocator, result: []u8) void { - closure.reset_event.wait(); - @memcpy(result, closure.resultPointer()[0..result.len]); - free(closure, gpa, result.len); - } - - fn free(closure: *AsyncClosure, gpa: Allocator, result_len: usize) void { - const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(closure); - gpa.free(base[0 .. closure.result_offset + result_len]); + fn free(ac: *AsyncClosure, gpa: Allocator, result_len: usize) void { + if (!ac.has_result) assert(result_len == 0); + const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac); + gpa.free(base[0 .. ac.result_offset + result_len]); } }; @@ -271,59 +268,60 @@ fn async( const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result.len; - const closure: *AsyncClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { + const ac: *AsyncClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { start(context.ptr, result.ptr); return null; })); - closure.* = .{ - .func = start, - .context_offset = context_offset, - .result_offset = result_offset, - .reset_event = .unset, - .cancel_tid = 0, - .select_condition = null, - .runnable = .{ + ac.* = .{ + .closure = .{ + .cancel_tid = 0, .start = AsyncClosure.start, - .is_parallel = false, + .is_concurrent = false, }, + .func = start, + .context_alignment = context_alignment, + .result_offset = result_offset, + .has_result = result.len != 0, + .reset_event = .unset, + .select_condition = null, }; - @memcpy(closure.contextPointer()[0..context.len], context); + @memcpy(ac.contextPointer()[0..context.len], context); pool.mutex.lock(); - const thread_capacity = cpu_count - 1 + pool.parallel_count; + const thread_capacity = cpu_count - 1 + pool.concurrent_count; pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { pool.mutex.unlock(); - closure.free(gpa, result.len); + ac.free(gpa, result.len); start(context.ptr, result.ptr); return null; }; - pool.run_queue.prepend(&closure.runnable.node); + pool.run_queue.prepend(&ac.closure.node); if (pool.threads.items.len < thread_capacity) { const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { if (pool.threads.items.len == 0) { - assert(pool.run_queue.popFirst() == &closure.runnable.node); + assert(pool.run_queue.popFirst() == &ac.closure.node); pool.mutex.unlock(); - closure.free(gpa, result.len); + ac.free(gpa, result.len); start(context.ptr, result.ptr); return null; } // Rely on other workers to do it. pool.mutex.unlock(); pool.cond.signal(); - return @ptrCast(closure); + return @ptrCast(ac); }; pool.threads.appendAssumeCapacity(thread); } pool.mutex.unlock(); pool.cond.signal(); - return @ptrCast(closure); + return @ptrCast(ac); } fn concurrent( @@ -342,40 +340,41 @@ fn concurrent( const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result_len; - const closure: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), n))); + const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), n))); - closure.* = .{ - .func = start, - .context_offset = context_offset, - .result_offset = result_offset, - .reset_event = .unset, - .cancel_tid = 0, - .select_condition = null, - .runnable = .{ + ac.* = .{ + .closure = .{ + .cancel_tid = 0, .start = AsyncClosure.start, - .is_parallel = true, + .is_concurrent = true, }, + .func = start, + .context_alignment = context_alignment, + .result_offset = result_offset, + .has_result = result_len != 0, + .reset_event = .unset, + .select_condition = null, }; - @memcpy(closure.contextPointer()[0..context.len], context); + @memcpy(ac.contextPointer()[0..context.len], context); pool.mutex.lock(); - pool.parallel_count += 1; - const thread_capacity = cpu_count - 1 + pool.parallel_count; + pool.concurrent_count += 1; + const thread_capacity = cpu_count - 1 + pool.concurrent_count; pool.threads.ensureTotalCapacity(gpa, thread_capacity) catch { pool.mutex.unlock(); - closure.free(gpa, result_len); + ac.free(gpa, result_len); return error.OutOfMemory; }; - pool.run_queue.prepend(&closure.runnable.node); + pool.run_queue.prepend(&ac.closure.node); if (pool.threads.items.len < thread_capacity) { const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { - assert(pool.run_queue.popFirst() == &closure.runnable.node); + assert(pool.run_queue.popFirst() == &ac.closure.node); pool.mutex.unlock(); - closure.free(gpa, result_len); + ac.free(gpa, result_len); return error.OutOfMemory; }; pool.threads.appendAssumeCapacity(thread); @@ -383,31 +382,48 @@ fn concurrent( pool.mutex.unlock(); pool.cond.signal(); - return @ptrCast(closure); + return @ptrCast(ac); } const GroupClosure = struct { + closure: Closure, pool: *Pool, group: *Io.Group, + /// Points to sibling `GroupClosure`. Used for walking the group to cancel all. + node: std.SinglyLinkedList.Node, func: *const fn (context: *anyopaque) void, - runnable: Runnable, context_alignment: std.mem.Alignment, context_len: usize, - fn start(runnable: *Runnable) void { - const closure: *GroupClosure = @alignCast(@fieldParentPtr("runnable", runnable)); - closure.func(closure.contextPointer()); - const group = closure.group; - const gpa = closure.pool.allocator; - free(closure, gpa); + fn start(closure: *Closure) void { + const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure)); + const tid = std.Thread.getCurrentId(); + const group = gc.group; const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); - const reset_event: *std.Thread.ResetEvent = @ptrCast(&group.context); + const reset_event: *ResetEvent = @ptrCast(&group.context); + if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, 0, tid, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == Closure.canceling_tid); + // We already know the task is canceled before running the callback. Since all closures + // in a Group have void return type, we can return early. + std.Thread.WaitGroup.finishStateless(group_state, reset_event); + return; + } + current_closure = closure; + gc.func(gc.contextPointer()); + current_closure = null; + + // In case a cancel happens after successful task completion, prevents + // signal from being delivered to the thread in `requestCancel`. + if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, tid, 0, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == Closure.canceling_tid); + } + std.Thread.WaitGroup.finishStateless(group_state, reset_event); } - fn free(closure: *GroupClosure, gpa: Allocator) void { - const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(closure); - gpa.free(base[0..contextEnd(closure.context_alignment, closure.context_len)]); + fn free(gc: *GroupClosure, gpa: Allocator) void { + const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc); + gpa.free(base[0..contextEnd(gc.context_alignment, gc.context_len)]); } fn contextOffset(context_alignment: std.mem.Alignment) usize { @@ -418,9 +434,9 @@ const GroupClosure = struct { return contextOffset(context_alignment) + context_len; } - fn contextPointer(closure: *GroupClosure) [*]u8 { - const base: [*]u8 = @ptrCast(closure); - return base + contextOffset(closure.context_alignment); + fn contextPointer(gc: *GroupClosure) [*]u8 { + const base: [*]u8 = @ptrCast(gc); + return base + contextOffset(gc.context_alignment); } }; @@ -436,39 +452,42 @@ fn groupAsync( const cpu_count = pool.cpu_count catch 1; const gpa = pool.allocator; const n = GroupClosure.contextEnd(context_alignment, context.len); - const closure: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { + const gc: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { return start(context.ptr); })); - closure.* = .{ + gc.* = .{ + .closure = .{ + .cancel_tid = 0, + .start = GroupClosure.start, + .is_concurrent = false, + }, .pool = pool, .group = group, + .node = .{ .next = @ptrCast(@alignCast(group.token)) }, .func = start, .context_alignment = context_alignment, .context_len = context.len, - .runnable = .{ - .start = GroupClosure.start, - .is_parallel = false, - }, }; - @memcpy(closure.contextPointer()[0..context.len], context); + group.token = &gc.node; + @memcpy(gc.contextPointer()[0..context.len], context); pool.mutex.lock(); - const thread_capacity = cpu_count - 1 + pool.parallel_count; + const thread_capacity = cpu_count - 1 + pool.concurrent_count; pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { pool.mutex.unlock(); - closure.free(gpa); + gc.free(gpa); return start(context.ptr); }; - pool.run_queue.prepend(&closure.runnable.node); + pool.run_queue.prepend(&gc.closure.node); if (pool.threads.items.len < thread_capacity) { const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { - assert(pool.run_queue.popFirst() == &closure.runnable.node); + assert(pool.run_queue.popFirst() == &gc.closure.node); pool.mutex.unlock(); - closure.free(gpa); + gc.free(gpa); return start(context.ptr); }; pool.threads.appendAssumeCapacity(thread); @@ -486,7 +505,7 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); - const reset_event: *std.Thread.ResetEvent = @ptrCast(&group.context); + const reset_event: *ResetEvent = @ptrCast(&group.context); std.Thread.WaitGroup.waitStateless(group_state, reset_event); } @@ -494,8 +513,14 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group) void { if (builtin.single_threaded) return; const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; - _ = group; - @panic("TODO threaded group cancel"); + const token = group.token.?; + group.token = null; + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + gc.closure.requestCancel(); + node = node.next orelse break; + } } fn await( @@ -518,32 +543,16 @@ fn cancel( ) void { _ = result_alignment; const pool: *Pool = @ptrCast(@alignCast(userdata)); - const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - switch (@atomicRmw( - std.Thread.Id, - &closure.cancel_tid, - .Xchg, - AsyncClosure.canceling_tid, - .acq_rel, - )) { - 0, AsyncClosure.canceling_tid => {}, - else => |cancel_tid| switch (builtin.os.tag) { - .linux => _ = std.os.linux.tgkill( - std.os.linux.getpid(), - @bitCast(cancel_tid), - posix.SIG.IO, - ), - else => {}, - }, - } - closure.waitAndFree(pool.allocator, result); + const ac: *AsyncClosure = @ptrCast(@alignCast(any_future)); + ac.closure.requestCancel(); + ac.waitAndFree(pool.allocator, result); } fn cancelRequested(userdata: ?*anyopaque) bool { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const closure = current_closure orelse return false; - return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; + return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == Closure.canceling_tid; } fn checkCancel(pool: *Pool) error{Canceled}!void { @@ -996,14 +1005,14 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; - var reset_event: std.Thread.ResetEvent = .unset; + var reset_event: ResetEvent = .unset; for (futures, 0..) |future, i| { const closure: *AsyncClosure = @ptrCast(@alignCast(future)); - if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) { + if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, &reset_event, .seq_cst) == AsyncClosure.done_reset_event) { for (futures[0..i]) |cleanup_future| { const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future)); - if (@atomicRmw(?*std.Thread.ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + if (@atomicRmw(?*ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { cleanup_closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. } } @@ -1016,7 +1025,7 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { var result: ?usize = null; for (futures, 0..) |future, i| { const closure: *AsyncClosure = @ptrCast(@alignCast(future)); - if (@atomicRmw(?*std.Thread.ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { + if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. if (result == null) result = i; // In case multiple are ready, return first. } From b22400271fc004128be1eadaf9206909bc1b0937 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 29 Sep 2025 23:27:00 -0700 Subject: [PATCH 064/244] std.Io.net.HostName: finish implementing DNS lookup --- lib/std/Io.zig | 52 +++++---- lib/std/Io/Threaded.zig | 85 ++++++++++----- lib/std/Io/net.zig | 42 ++++++-- lib/std/Io/net/HostName.zig | 206 ++++++++++++++++++++---------------- 4 files changed, 237 insertions(+), 148 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 5514cff6f9..6f2d05aacd 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -641,8 +641,8 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque) void, ) void, - groupWait: *const fn (?*anyopaque, *Group) void, - groupCancel: *const fn (?*anyopaque, *Group) void, + groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, + groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, /// Blocks until one of the futures from the list has a result ready, such /// that awaiting it will not block. Returns that index. @@ -665,14 +665,14 @@ pub const VTable = struct { fileSeekBy: *const fn (?*anyopaque, file: File, offset: i64) File.SeekError!void, fileSeekTo: *const fn (?*anyopaque, file: File, offset: u64) File.SeekError!void, - now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp, - sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void, + now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) NowError!Timestamp, + sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, timeout: Timeout) SleepError!void, listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, ipBind: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, - netSend: *const fn (?*anyopaque, handle: net.Socket.Handle, address: net.IpAddress, data: []const u8) net.Socket.SendError!void, - netReceive: *const fn (?*anyopaque, handle: net.Socket.Handle, address: net.IpAddress, buffer: []u8) net.Socket.ReceiveError!void, + netSend: *const fn (?*anyopaque, handle: net.Socket.Handle, address: *const net.IpAddress, data: []const u8) net.Socket.SendError!void, + netReceive: *const fn (?*anyopaque, handle: net.Socket.Handle, buffer: []u8, timeout: Timeout) net.Socket.ReceiveTimeoutError!net.ReceivedMessage, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, @@ -710,6 +710,15 @@ pub const Timestamp = enum(i96) { pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { return @enumFromInt(@intFromEnum(from) + duration.nanoseconds); } + + pub fn fromNow(io: Io, clockid: std.posix.clockid_t, duration: Duration) NowError!Timestamp { + const now_ts = try now(io, clockid); + return addDuration(now_ts, duration); + } + + pub fn compare(lhs: Timestamp, op: std.math.CompareOperator, rhs: Timestamp) bool { + return std.math.compare(@intFromEnum(lhs), op, @intFromEnum(rhs)); + } }; pub const Duration = struct { nanoseconds: i96, @@ -722,11 +731,14 @@ pub const Duration = struct { return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_s }; } }; -pub const Deadline = union(enum) { +pub const Timeout = union(enum) { + none, duration: Duration, - timestamp: Timestamp, + deadline: Timestamp, + + pub const Error = error{Timeout}; }; -pub const ClockGetTimeError = std.posix.ClockGetTimeError || Cancelable; +pub const NowError = std.posix.ClockGetTimeError || Cancelable; pub const SleepError = error{ UnsupportedClock, Unexpected, Canceled }; pub const AnyFuture = opaque {}; @@ -768,8 +780,10 @@ pub const Group = struct { /// /// `function` *may* be called immediately, before `async` returns. /// - /// After this is called, `wait` must be called before the group is - /// deinitialized. + /// After this is called, `wait` or `cancel` must be called before the + /// group is deinitialized. + /// + /// Threadsafe. /// /// See also: /// * `Io.async` @@ -789,7 +803,9 @@ pub const Group = struct { /// /// Idempotent. Not threadsafe. pub fn wait(g: *Group, io: Io) void { - io.vtable.groupWait(io.userdata, g); + const token = g.token orelse return; + g.token = null; + io.vtable.groupWait(io.userdata, g, token); } /// Equivalent to `wait` but requests cancellation on all tasks owned by @@ -797,9 +813,9 @@ pub const Group = struct { /// /// Idempotent. Not threadsafe. pub fn cancel(g: *Group, io: Io) void { - if (g.token == null) return; - io.vtable.groupCancel(io.userdata, g); - assert(g.token == null); + const token = g.token orelse return; + g.token = null; + io.vtable.groupCancel(io.userdata, g, token); } }; @@ -1215,12 +1231,12 @@ pub fn cancelRequested(io: Io) bool { return io.vtable.cancelRequested(io.userdata); } -pub fn now(io: Io, clockid: std.posix.clockid_t) ClockGetTimeError!Timestamp { +pub fn now(io: Io, clockid: std.posix.clockid_t) NowError!Timestamp { return io.vtable.now(io.userdata, clockid); } -pub fn sleep(io: Io, clockid: std.posix.clockid_t, deadline: Deadline) SleepError!void { - return io.vtable.sleep(io.userdata, clockid, deadline); +pub fn sleep(io: Io, clockid: std.posix.clockid_t, timeout: Timeout) SleepError!void { + return io.vtable.sleep(io.userdata, clockid, timeout); } pub fn sleepDuration(io: Io, duration: Duration) SleepError!void { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index db0dad5669..76291527f1 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -463,16 +463,19 @@ fn groupAsync( }, .pool = pool, .group = group, - .node = .{ .next = @ptrCast(@alignCast(group.token)) }, + .node = undefined, .func = start, .context_alignment = context_alignment, .context_len = context.len, }; - group.token = &gc.node; @memcpy(gc.contextPointer()[0..context.len], context); pool.mutex.lock(); + // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe. + gc.node = .{ .next = @ptrCast(@alignCast(group.token)) }; + group.token = &gc.node; + const thread_capacity = cpu_count - 1 + pool.concurrent_count; pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { @@ -493,6 +496,8 @@ fn groupAsync( pool.threads.appendAssumeCapacity(thread); } + // This needs to be done before unlocking the mutex to avoid a race with + // the associated task finishing. const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); std.Thread.WaitGroup.startStateless(group_state); @@ -500,21 +505,16 @@ fn groupAsync( pool.cond.signal(); } -fn groupWait(userdata: ?*anyopaque, group: *Io.Group) void { - if (builtin.single_threaded) return; +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; + + if (builtin.single_threaded) return; + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); std.Thread.WaitGroup.waitStateless(group_state, reset_event); -} -fn groupCancel(userdata: ?*anyopaque, group: *Io.Group) void { - if (builtin.single_threaded) return; - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; - const token = group.token.?; - group.token = null; var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); while (true) { const gc: *GroupClosure = @fieldParentPtr("node", node); @@ -523,6 +523,36 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group) void { } } +fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const gpa = pool.allocator; + + if (builtin.single_threaded) return; + + { + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + gc.closure.requestCancel(); + node = node.next orelse break; + } + } + + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *ResetEvent = @ptrCast(&group.context); + std.Thread.WaitGroup.waitStateless(group_state, reset_event); + + { + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + const node_next = node.next; + gc.free(gpa); + node = node_next orelse break; + } + } +} + fn await( userdata: ?*anyopaque, any_future: *Io.AnyFuture, @@ -774,7 +804,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .SUCCESS => return nread, .INTR => unreachable, .INVAL => unreachable, - .FAULT => unreachable, + .FAULT => |err| return errnoBug(err), .AGAIN => unreachable, // currently not support in WASI .BADF => return error.NotOpenForReading, // can be a race condition .IO => return error.InputOutput, @@ -796,7 +826,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .SUCCESS => return @intCast(rc), .INTR => continue, .INVAL => unreachable, - .FAULT => unreachable, + .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, .BADF => return error.NotOpenForReading, // can be a race condition @@ -896,7 +926,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .SUCCESS => return nread, .INTR => unreachable, .INVAL => unreachable, - .FAULT => unreachable, + .FAULT => |err| return errnoBug(err), .AGAIN => unreachable, .BADF => return error.NotOpenForReading, // can be a race condition .IO => return error.InputOutput, @@ -922,7 +952,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .SUCCESS => return @bitCast(rc), .INTR => continue, .INVAL => unreachable, - .FAULT => unreachable, + .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, .BADF => return error.NotOpenForReading, // can be a race condition @@ -969,18 +999,19 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posi }; } -fn now(userdata: ?*anyopaque, clockid: posix.clockid_t) Io.ClockGetTimeError!Io.Timestamp { +fn now(userdata: ?*anyopaque, clockid: posix.clockid_t) Io.NowError!Io.Timestamp { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); const timespec = try posix.clock_gettime(clockid); return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); } -fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, deadline: Io.Deadline) Io.SleepError!void { +fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, timeout: Io.Timeout) Io.SleepError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); - const deadline_nanoseconds: i96 = switch (deadline) { + const deadline_nanoseconds: i96 = switch (timeout) { + .none => std.math.maxInt(i96), .duration => |duration| duration.nanoseconds, - .timestamp => |timestamp| @intFromEnum(timestamp), + .deadline => |deadline| @intFromEnum(deadline), }; var timespec: posix.timespec = .{ .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), @@ -988,12 +1019,12 @@ fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, deadline: Io.Deadline) }; while (true) { try pool.checkCancel(); - switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (deadline) { - .duration => false, - .timestamp => true, + switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (timeout) { + .none, .duration => false, + .deadline => true, } }, ×pec, ×pec))) { .SUCCESS => return, - .FAULT => unreachable, + .FAULT => |err| return errnoBug(err), .INTR => {}, .INVAL => return error.UnsupportedClock, else => |err| return posix.unexpectedErrno(err), @@ -1278,7 +1309,7 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n fn netSend( userdata: ?*anyopaque, handle: Io.net.Socket.Handle, - address: Io.net.IpAddress, + address: *const Io.net.IpAddress, data: []const u8, ) Io.net.Socket.SendError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); @@ -1293,15 +1324,15 @@ fn netSend( fn netReceive( userdata: ?*anyopaque, handle: Io.net.Socket.Handle, - address: Io.net.IpAddress, buffer: []u8, -) Io.net.Socket.ReceiveError!void { + timeout: Io.Timeout, +) Io.net.Socket.ReceiveTimeoutError!Io.net.ReceivedMessage { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); _ = handle; - _ = address; _ = buffer; + _ = timeout; @panic("TODO"); } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index eca6cdd1f7..d06f7bbce0 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -134,13 +134,13 @@ pub const IpAddress = union(enum) { } } - pub fn eql(a: IpAddress, b: IpAddress) bool { - return switch (a) { - .ip4 => |a_ip4| switch (b) { + pub fn eql(a: *const IpAddress, b: *const IpAddress) bool { + return switch (a.*) { + .ip4 => |a_ip4| switch (b.*) { .ip4 => |b_ip4| a_ip4.eql(b_ip4), else => false, }, - .ip6 => |a_ip6| switch (b) { + .ip6 => |a_ip6| switch (b.*) { .ip6 => |b_ip6| a_ip6.eql(b_ip6), else => false, }, @@ -695,6 +695,11 @@ pub const Ip6Address = struct { }; }; +pub const ReceivedMessage = struct { + from: IpAddress, + len: usize, +}; + pub const Interface = struct { /// Value 0 indicates `none`. index: u32, @@ -816,14 +821,31 @@ pub const Socket = struct { return io.vtable.netSend(io.userdata, s.handle, dest, data); } - pub const ReceiveError = error{} || Io.Cancelable; + pub const ReceiveError = error{} || Io.UnexpectedError || Io.Cancelable; - /// Transfers `data` from `source`, connectionless. + /// Waits for data. Connectionless. /// - /// Returned slice has same pointer as `buffer` with possibly shorter length. - pub fn receive(s: *const Socket, io: Io, source: *const IpAddress, buffer: []u8) ReceiveError![]u8 { - const n = try io.vtable.netReceive(io.userdata, s.handle, source, buffer); - return buffer[0..n]; + /// See also: + /// * `receiveTimeout` + pub fn receive(s: *const Socket, io: Io, source: *const IpAddress, buffer: []u8) ReceiveError!ReceivedMessage { + return io.vtable.netReceive(io.userdata, s.handle, source, buffer, .none); + } + + pub const ReceiveTimeoutError = ReceiveError || Io.Timeout.Error; + + /// Waits for data. Connectionless. + /// + /// Returns `error.Timeout` if no message arrives early enough. + /// + /// See also: + /// * `receive` + pub fn receiveTimeout( + s: *const Socket, + io: Io, + buffer: []u8, + timeout: Io.Timeout, + ) ReceiveTimeoutError!ReceivedMessage { + return io.vtable.netReceive(io.userdata, s.handle, buffer, timeout); } }; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 907f5bca1b..6f3f421a9c 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -46,15 +46,13 @@ pub const LookupOptions = struct { family: ?IpAddress.Family = null, }; -pub const LookupError = Io.Cancelable || Io.File.OpenError || Io.File.Reader.Error || error{ +pub const LookupError = error{ UnknownHostName, ResolvConfParseFailed, - // TODO remove from error set; retry a few times then report a different error - TemporaryNameServerFailure, InvalidDnsARecord, InvalidDnsAAAARecord, NameServerFailure, -}; +} || Io.NowError || IpAddress.BindError || Io.File.OpenError || Io.File.Reader.Error || Io.Cancelable; pub const LookupResult = struct { /// How many `LookupOptions.addresses_buffer` elements are populated. @@ -185,7 +183,7 @@ fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult return result; } -fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { +fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { const rc = ResolvConf.init(io) catch return error.ResolvConfParseFailed; // Count dots, suppress search when >=ndots or name ends in @@ -218,19 +216,17 @@ fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) !LookupR return lookupDns(io, lookup_canon_name, &rc, options); } -const DnsReply = struct { - buf: [512]u8, - len: usize, -}; - -fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) !LookupResult { +fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) LookupError!LookupResult { const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ .{ .af = .ip6, .rr = std.posix.RR.A }, .{ .af = .ip4, .rr = std.posix.RR.AAAA }, }; var query_buffers: [2][280]u8 = undefined; + var answer_buffers: [2][512]u8 = undefined; var queries_buffer: [2][]const u8 = undefined; + var answers_buffer: [2][]const u8 = undefined; var nq: usize = 0; + var next_answer_buffer: usize = 0; for (family_records) |fr| { if (options.family != fr.af) { @@ -241,41 +237,123 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio } } - const queries = queries_buffer[0..nq]; - var replies_buffer: [2]DnsReply = undefined; - var replies: Io.Queue(DnsReply) = .init(&replies_buffer); - try rc.sendMessage(io, queries, &replies); + var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined; + var any_ip6 = false; + for (rc.nameservers(), &ip4_mapped) |*ns, *m| { + m.* = .{ .ip6 = .fromAny(ns.*) }; + any_ip6 = any_ip6 or ns.* == .ip6; + } + var socket = s: { + if (any_ip6) ip6: { + const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; + const socket = ip6_addr.bind(io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { + error.AddressFamilyUnsupported => break :ip6, + else => |e| return e, + }; + break :s socket; + } + any_ip6 = false; + const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; + const socket = try ip4_addr.bind(io, .{ .mode = .dgram }); + break :s socket; + }; + defer socket.close(io); - for (replies) |reply| { - if (reply.len < 4 or (reply[3] & 15) == 2) return error.TemporaryNameServerFailure; - if ((reply[3] & 15) == 3) return .empty; - if ((reply[3] & 15) != 0) return error.UnknownHostName; + const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); + const queries = queries_buffer[0..nq]; + const answers = answers_buffer[0..queries.len]; + for (answers) |*answer| answer.len = 0; + + var now_ts = try io.now(.MONOTONIC); + const final_ts = now_ts.addDuration(.seconds(rc.timeout_seconds)); + const attempt_duration: Io.Duration = .{ + .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, + }; + + send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try io.now(.MONOTONIC)) { + var group: Io.Group = .init; + defer group.cancel(io); + + for (queries, answers) |query, *answer| { + if (answer.len != 0) continue; + for (mapped_nameservers) |*ns| { + group.async(io, sendIgnoringResult, .{ io, socket.handle, ns, query }); + } + } + + const timeout: Io.Timeout = .{ .deadline = now_ts.addDuration(attempt_duration) }; + + while (true) { + const buf = &answer_buffers[next_answer_buffer]; + const reply = socket.receiveTimeout(io, buf, timeout) catch |err| switch (err) { + error.Canceled => return error.Canceled, + error.Timeout => continue :send, + else => continue, + }; + + // Ignore non-identifiable packets. + if (reply.len < 4) continue; + + // Ignore replies from addresses we didn't send to. + const ns = for (mapped_nameservers) |*ns| { + if (reply.from.eql(ns)) break ns; + } else { + continue; + }; + + const reply_msg = buf[0..reply.len]; + + // Find which query this answer goes with, if any. + const query, const answer = for (queries, answers) |query, *answer| { + if (reply_msg[0] == query[0] and reply_msg[1] == query[1]) break .{ query, answer }; + } else { + continue; + }; + if (answer.len != 0) continue; + + // Only accept positive or negative responses; retry immediately on + // server failure, and ignore all other codes such as refusal. + switch (reply_msg[3] & 15) { + 0, 3 => { + answer.* = reply_msg; + next_answer_buffer += 1; + if (next_answer_buffer == answers.len) break :send; + }, + 2 => { + group.async(io, sendIgnoringResult, .{ io, socket.handle, ns, query }); + continue; + }, + else => continue, + } + } + } else { + return error.NameServerFailure; } var addresses_len: usize = 0; var canonical_name: ?HostName = null; - for (replies) |reply| { - var it = DnsResponse.init(reply) catch { + for (answers) |answer| { + var it = DnsResponse.init(answer) catch { // TODO accept a diagnostics struct and append warnings continue; }; while (it.next() catch { // TODO accept a diagnostics struct and append warnings continue; - }) |answer| switch (answer.rr) { + }) |record| switch (record.rr) { std.posix.RR.A => { - if (answer.data.len != 4) return error.InvalidDnsARecord; + if (record.data.len != 4) return error.InvalidDnsARecord; options.addresses_buffer[addresses_len] = .{ .ip4 = .{ - .bytes = answer.data[0..4].*, + .bytes = record.data[0..4].*, .port = options.port, } }; addresses_len += 1; }, std.posix.RR.AAAA => { - if (answer.data.len != 16) return error.InvalidDnsAAAARecord; + if (record.data.len != 16) return error.InvalidDnsAAAARecord; options.addresses_buffer[addresses_len] = .{ .ip6 = .{ - .bytes = answer.data[0..16].*, + .bytes = record.data[0..16].*, .port = options.port, } }; addresses_len += 1; @@ -285,7 +363,7 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio @panic("TODO"); //var tmp: [256]u8 = undefined; //// Returns len of compressed name. strlen to get canon name. - //_ = try posix.dn_expand(packet, answer.data, &tmp); + //_ = try posix.dn_expand(packet, record.data, &tmp); //const canon_name = mem.sliceTo(&tmp, 0); //if (isValidHostName(canon_name)) { // ctx.canon.items.len = 0; @@ -304,6 +382,10 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio return error.NameServerFailure; } +fn sendIgnoringResult(io: Io, socket_handle: Io.net.Socket.Handle, dest: *const IpAddress, msg: []const u8) void { + _ = io.vtable.netSend(io.userdata, socket_handle, dest, msg) catch {}; +} + fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) { error.FileNotFound, @@ -523,7 +605,7 @@ pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream pub const ResolvConf = struct { attempts: u32, ndots: u32, - timeout: Io.Duration, + timeout_seconds: u32, nameservers_buffer: [max_nameservers]IpAddress, nameservers_len: usize, search_buffer: [max_len]u8, @@ -539,7 +621,7 @@ pub const ResolvConf = struct { .search_buffer = undefined, .search_len = 0, .ndots = 1, - .timeout = .seconds(5), + .timeout_seconds = 5, .attempts = 2, }; @@ -589,7 +671,7 @@ pub const ResolvConf = struct { switch (std.meta.stringToEnum(Option, name) orelse continue) { .ndots => rc.ndots = @min(value, 15), .attempts => rc.attempts = @min(value, 10), - .timeout => rc.timeout = .seconds(@min(value, 60)), + .timeout => rc.timeout_seconds = @min(value, 60), } }, .nameserver => { @@ -621,68 +703,6 @@ pub const ResolvConf = struct { fn nameservers(rc: *const ResolvConf) []const IpAddress { return rc.nameservers_buffer[0..rc.nameservers_len]; } - - fn sendMessage( - rc: *const ResolvConf, - io: Io, - queries: []const []const u8, - replies: *Io.Queue(DnsReply), - ) !void { - var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined; - var any_ip6 = false; - for (rc.nameservers(), &ip4_mapped) |*ns, *m| { - m.* = .{ .ip6 = .fromAny(ns.*) }; - any_ip6 = any_ip6 or ns.* == .ip6; - } - - const socket = s: { - if (any_ip6) ip6: { - const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; - const socket = ip6_addr.bind(io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { - error.AddressFamilyUnsupported => break :ip6, - else => |e| return e, - }; - break :s socket; - } - any_ip6 = false; - const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; - const socket = try ip4_addr.bind(io, .{ .mode = .dgram }); - break :s socket; - }; - defer socket.close(); - - const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); - - var group: Io.Group = .init; - defer group.cancel(); - - for (queries) |query| { - for (mapped_nameservers) |*ns| { - group.async(sendOneMessage, .{ io, query, ns }); - } - } - - const deadline: Io.Deadline = .fromDuration(rc.timeout); - - for (0..queries.len) |_| { - const msg = socket.receiveDeadline(deadline) catch |err| switch (err) { - error.Timeout => return error.Timeout, - error.Canceled => return error.Canceled, - else => continue, - }; - _ = msg; - _ = replies; - @panic("TODO check msg for dns reply and put into replies queue"); - } - } - - fn sendOneMessage( - io: Io, - query: []const u8, - ns: *const IpAddress, - ) void { - io.vtable.netSend(io.userdata, ns.*, &.{query}) catch |err| switch (err) {}; - } }; test ResolvConf { @@ -702,7 +722,7 @@ test ResolvConf { .search_buffer = undefined, .search_len = 0, .ndots = 1, - .timeout = .seconds(5), + .timeout_seconds = 5, .attempts = 2, }; From cde5a51d0ca26b8274f0208cfae88e548385fe3b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 30 Sep 2025 23:56:52 -0700 Subject: [PATCH 065/244] std.Io.net: make netSend support multiple messages this lowers to sendmmsg on linux, and means Io.Group is no longer needed, resulting in a more efficient implementation. --- lib/std/Io.zig | 2 +- lib/std/Io/Threaded.zig | 8 ++++---- lib/std/Io/net.zig | 22 +++++++++++++++++++++- lib/std/Io/net/HostName.zig | 22 +++++++++++++--------- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 6f2d05aacd..d0a5c5df8b 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -671,7 +671,7 @@ pub const VTable = struct { listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, ipBind: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, - netSend: *const fn (?*anyopaque, handle: net.Socket.Handle, address: *const net.IpAddress, data: []const u8) net.Socket.SendError!void, + netSend: *const fn (?*anyopaque, net.Socket.Handle, []const net.OutgoingMessage, net.SendFlags) net.Socket.SendError!void, netReceive: *const fn (?*anyopaque, handle: net.Socket.Handle, buffer: []u8, timeout: Timeout) net.Socket.ReceiveTimeoutError!net.ReceivedMessage, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 76291527f1..3e0c1380df 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1309,15 +1309,15 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n fn netSend( userdata: ?*anyopaque, handle: Io.net.Socket.Handle, - address: *const Io.net.IpAddress, - data: []const u8, + messages: []const Io.net.OutgoingMessage, + flags: Io.net.SendFlags, ) Io.net.Socket.SendError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); _ = handle; - _ = address; - _ = data; + _ = messages; + _ = flags; @panic("TODO"); } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index d06f7bbce0..02cec48b0c 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -700,6 +700,21 @@ pub const ReceivedMessage = struct { len: usize, }; +pub const OutgoingMessage = struct { + address: *const IpAddress, + data: []const u8, + control: []const u8 = &.{}, +}; + +pub const SendFlags = packed struct(u8) { + confirm: bool = false, + dont_route: bool = false, + eor: bool = false, + oob: bool = false, + fastopen: bool = false, + _: u3 = 0, +}; + pub const Interface = struct { /// Value 0 indicates `none`. index: u32, @@ -818,7 +833,12 @@ pub const Socket = struct { /// Transfers `data` to `dest`, connectionless. pub fn send(s: *const Socket, io: Io, dest: *const IpAddress, data: []const u8) SendError!void { - return io.vtable.netSend(io.userdata, s.handle, dest, data); + const message: OutgoingMessage = .{ .address = dest, .data = data }; + return io.vtable.netSend(io.userdata, s.handle, &.{message}, .{}); + } + + pub fn sendMany(s: *const Socket, io: Io, messages: []const OutgoingMessage, flags: SendFlags) SendError!void { + return io.vtable.netSend(io.userdata, s.handle, messages, flags); } pub const ReceiveError = error{} || Io.UnexpectedError || Io.Cancelable; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 6f3f421a9c..e77987aa1a 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -271,15 +271,19 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio }; send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try io.now(.MONOTONIC)) { - var group: Io.Group = .init; - defer group.cancel(io); - + var message_buffer: [queries_buffer.len * ResolvConf.max_nameservers]Io.net.OutgoingMessage = undefined; + var message_i: usize = 0; for (queries, answers) |query, *answer| { if (answer.len != 0) continue; for (mapped_nameservers) |*ns| { - group.async(io, sendIgnoringResult, .{ io, socket.handle, ns, query }); + message_buffer[message_i] = .{ + .address = ns, + .data = query, + }; + message_i += 1; } } + io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{}) catch {}; const timeout: Io.Timeout = .{ .deadline = now_ts.addDuration(attempt_duration) }; @@ -320,7 +324,11 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio if (next_answer_buffer == answers.len) break :send; }, 2 => { - group.async(io, sendIgnoringResult, .{ io, socket.handle, ns, query }); + const message: Io.net.OutgoingMessage = .{ + .address = ns, + .data = query, + }; + io.vtable.netSend(io.userdata, socket.handle, &.{message}, .{}) catch {}; continue; }, else => continue, @@ -382,10 +390,6 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio return error.NameServerFailure; } -fn sendIgnoringResult(io: Io, socket_handle: Io.net.Socket.Handle, dest: *const IpAddress, msg: []const u8) void { - _ = io.vtable.netSend(io.userdata, socket_handle, dest, msg) catch {}; -} - fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) { error.FileNotFound, From 3b80fde6f42f104278d4102562dd2b714aafe877 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 1 Oct 2025 14:30:00 -0700 Subject: [PATCH 066/244] std.os.linux: remove sendmmsg workaround This "fix" is too opinionated to belong here. Better instead to document the pitfalls. --- lib/std/os/linux.zig | 51 ++++++++------------------------------------ 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 11add50a05..df46a18cf7 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -1,10 +1,8 @@ //! This file provides the system interface functions for Linux matching those //! that are provided by libc, whether or not libc is linked. The following //! abstractions are made: -//! * Work around kernel bugs and limitations. For example, see sendmmsg. //! * Implement all the syscalls in the same way that libc functions will //! provide `rename` when only the `renameat` syscall exists. -//! * Does not support POSIX thread cancellation. const std = @import("../std.zig"); const builtin = @import("builtin"); const assert = std.debug.assert; @@ -1836,7 +1834,7 @@ pub fn seteuid(euid: uid_t) usize { // id will not be changed. Since uid_t is unsigned, this wraps around to the // max value in C. comptime assert(@typeInfo(uid_t) == .int and @typeInfo(uid_t).int.signedness == .unsigned); - return setresuid(std.math.maxInt(uid_t), euid, std.math.maxInt(uid_t)); + return setresuid(maxInt(uid_t), euid, maxInt(uid_t)); } pub fn setegid(egid: gid_t) usize { @@ -1847,7 +1845,7 @@ pub fn setegid(egid: gid_t) usize { // id will not be changed. Since gid_t is unsigned, this wraps around to the // max value in C. comptime assert(@typeInfo(uid_t) == .int and @typeInfo(uid_t).int.signedness == .unsigned); - return setresgid(std.math.maxInt(gid_t), egid, std.math.maxInt(gid_t)); + return setresgid(maxInt(gid_t), egid, maxInt(gid_t)); } pub fn getresuid(ruid: *uid_t, euid: *uid_t, suid: *uid_t) usize { @@ -2081,44 +2079,13 @@ pub fn sendmsg(fd: i32, msg: *const msghdr_const, flags: u32) usize { } } +/// Warning: libc is defined to have incompatible integer types with the +/// corresponding kernel data structures for this syscall. +/// +/// Warning: on 64-bit systems, if any message length would exceed `maxInt(i32)`, +/// number of bytes sent cannot be determined, because the kernel uses `ssize_t` +/// for `sendmsg` return value but `int` for the corresponding values here. pub fn sendmmsg(fd: i32, msgvec: [*]mmsghdr_const, vlen: u32, flags: u32) usize { - if (@typeInfo(usize).int.bits > @typeInfo(@typeInfo(mmsghdr).@"struct".fields[1].type).int.bits) { - // workaround kernel brokenness: - // if adding up all iov_len overflows a i32 then split into multiple calls - // see https://www.openwall.com/lists/musl/2014/06/07/5 - const kvlen = if (vlen > IOV_MAX) IOV_MAX else vlen; // matches kernel - var next_unsent: usize = 0; - for (msgvec[0..kvlen], 0..) |*msg, i| { - var size: i32 = 0; - const msg_iovlen = @as(usize, @intCast(msg.hdr.iovlen)); // kernel side this is treated as unsigned - for (msg.hdr.iov[0..msg_iovlen]) |iov| { - if (iov.len > std.math.maxInt(i32) or @addWithOverflow(size, @as(i32, @intCast(iov.len)))[1] != 0) { - // batch-send all messages up to the current message - if (next_unsent < i) { - const batch_size = i - next_unsent; - const r = syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(&msgvec[next_unsent]), batch_size, flags); - if (E.init(r) != .SUCCESS) return next_unsent; - if (r < batch_size) return next_unsent + r; - } - // send current message as own packet - const r = sendmsg(fd, &msg.hdr, flags); - if (E.init(r) != .SUCCESS) return r; - // Linux limits the total bytes sent by sendmsg to INT_MAX, so this cast is safe. - msg.len = @as(u32, @intCast(r)); - next_unsent = i + 1; - break; - } - size += @intCast(iov.len); - } - } - if (next_unsent < kvlen or next_unsent == 0) { // want to make sure at least one syscall occurs (e.g. to trigger MSG.EOR) - const batch_size = kvlen - next_unsent; - const r = syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(&msgvec[next_unsent]), batch_size, flags); - if (E.init(r) != .SUCCESS) return r; - return next_unsent + r; - } - return kvlen; - } return syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(msgvec), vlen, flags); } @@ -8700,7 +8667,7 @@ pub const PR = enum(i32) { pub const SET_MM_MAP = 14; pub const SET_MM_MAP_SIZE = 15; - pub const SET_PTRACER_ANY = std.math.maxInt(c_ulong); + pub const SET_PTRACER_ANY = maxInt(c_ulong); pub const FP_MODE_FR = 1 << 0; pub const FP_MODE_FRE = 1 << 1; From bcb6760fa5a2dce29f917912a86ba3ba7414fccc Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 1 Oct 2025 14:30:49 -0700 Subject: [PATCH 067/244] std.os.linux: remove unnecessary warnings from sendmmsg The one about INT_MAX is self-evident from the type system. The one about kernel having bad types doesn't seem accurate as I checked the source code and it uses size_t for all the appropriate types, matching the libc struct definition for msghdr and msghdr_const. --- lib/std/os/linux.zig | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index df46a18cf7..8bbececdf0 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -2079,12 +2079,6 @@ pub fn sendmsg(fd: i32, msg: *const msghdr_const, flags: u32) usize { } } -/// Warning: libc is defined to have incompatible integer types with the -/// corresponding kernel data structures for this syscall. -/// -/// Warning: on 64-bit systems, if any message length would exceed `maxInt(i32)`, -/// number of bytes sent cannot be determined, because the kernel uses `ssize_t` -/// for `sendmsg` return value but `int` for the corresponding values here. pub fn sendmmsg(fd: i32, msgvec: [*]mmsghdr_const, vlen: u32, flags: u32) usize { return syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(msgvec), vlen, flags); } From 95dee2af9c6ed17286a1b3be81b11093c2ecb5f2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 1 Oct 2025 16:07:50 -0700 Subject: [PATCH 068/244] std.Io: implement netSend --- lib/std/Io.zig | 2 +- lib/std/Io/Threaded.zig | 111 +++++++++++++++++++++++++++++++----- lib/std/Io/net.zig | 34 +++++++---- lib/std/Io/net/HostName.zig | 10 ++-- lib/std/os/linux.zig | 7 +-- 5 files changed, 127 insertions(+), 37 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index d0a5c5df8b..ddfb8c2e01 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -671,7 +671,7 @@ pub const VTable = struct { listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, ipBind: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, - netSend: *const fn (?*anyopaque, net.Socket.Handle, []const net.OutgoingMessage, net.SendFlags) net.Socket.SendError!void, + netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) net.Socket.SendError!void, netReceive: *const fn (?*anyopaque, handle: net.Socket.Handle, buffer: []u8, timeout: Timeout) net.Socket.ReceiveTimeoutError!net.ReceivedMessage, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3e0c1380df..3b72f1ede1 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1108,8 +1108,8 @@ fn listenPosix( } var storage: PosixAddress = undefined; - var socklen = addressToPosix(address, &storage); - try posixBind(pool, socket_fd, &storage.any, socklen); + var addr_len = addressToPosix(&address, &storage); + try posixBind(pool, socket_fd, &storage.any, addr_len); while (true) { try pool.checkCancel(); @@ -1121,7 +1121,7 @@ fn listenPosix( } } - try posixGetSockName(pool, socket_fd, &storage.any, &socklen); + try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); return .{ .socket = .{ .handle = socket_fd, @@ -1226,9 +1226,9 @@ fn ipBindPosix( } var storage: PosixAddress = undefined; - var socklen = addressToPosix(address, &storage); - try posixBind(pool, socket_fd, &storage.any, socklen); - try posixGetSockName(pool, socket_fd, &storage.any, &socklen); + var addr_len = addressToPosix(&address, &storage); + try posixBind(pool, socket_fd, &storage.any, addr_len); + try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); return .{ .handle = socket_fd, .address = addressFromPosix(&storage), @@ -1306,21 +1306,102 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n return n; } +const have_sendmmsg = builtin.os.tag == .linux; + fn netSend( userdata: ?*anyopaque, handle: Io.net.Socket.Handle, - messages: []const Io.net.OutgoingMessage, + messages: []Io.net.OutgoingMessage, flags: Io.net.SendFlags, ) Io.net.Socket.SendError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - _ = handle; - _ = messages; - _ = flags; + if (have_sendmmsg) { + var i: usize = 0; + while (messages.len - i != 0) { + i += try netSendMany(pool, handle, messages[i..], flags); + } + return; + } + + try pool.checkCancel(); @panic("TODO"); } +fn netSendMany( + pool: *Pool, + handle: Io.net.Socket.Handle, + messages: []Io.net.OutgoingMessage, + flags: Io.net.SendFlags, +) Io.net.Socket.SendError!usize { + var msg_buffer: [64]std.os.linux.mmsghdr = undefined; + var addr_buffer: [msg_buffer.len]PosixAddress = undefined; + var iovecs_buffer: [msg_buffer.len]posix.iovec = undefined; + const min_len: usize = @min(messages.len, msg_buffer.len); + const clamped_messages = messages[0..min_len]; + const clamped_msgs = (&msg_buffer)[0..min_len]; + const clamped_addrs = (&addr_buffer)[0..min_len]; + const clamped_iovecs = (&iovecs_buffer)[0..min_len]; + + for (clamped_messages, clamped_msgs, clamped_addrs, clamped_iovecs) |*message, *msg, *addr, *iovec| { + iovec.* = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + msg.* = .{ + .hdr = .{ + .name = &addr.any, + .namelen = addressToPosix(message.address, addr), + .iov = iovec[0..1], + .iovlen = 1, + .control = @constCast(message.control.ptr), + .controllen = message.control.len, + .flags = 0, + }, + .len = undefined, // Populated by calling sendmmsg below. + }; + } + + const posix_flags: u32 = + @as(u32, if (flags.confirm) posix.MSG.CONFIRM else 0) | + @as(u32, if (flags.dont_route) posix.MSG.DONTROUTE else 0) | + @as(u32, if (flags.eor) posix.MSG.EOR else 0) | + @as(u32, if (flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (flags.fastopen) posix.MSG.FASTOPEN else 0) | + posix.MSG.NOSIGNAL; + + while (true) { + try pool.checkCancel(); + const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), posix_flags); + switch (posix.errno(rc)) { + .SUCCESS => { + for (clamped_messages[0..rc], clamped_msgs[0..rc]) |*message, *msg| { + message.data_len = msg.len; + } + return rc; + }, + .AGAIN => |err| return errnoBug(err), + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // Always a race condition. + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. + .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. + .INTR => continue, + .INVAL => |err| return errnoBug(err), // Invalid argument passed. + .ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified + .MSGSIZE => return error.MessageOversize, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket. + .OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type. + .PIPE => return error.SocketNotConnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.NetworkUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketNotConnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn netReceive( userdata: ?*anyopaque, handle: Io.net.Socket.Handle, @@ -1503,13 +1584,13 @@ fn addressFromPosix(posix_address: *PosixAddress) Io.net.IpAddress { }; } -fn addressToPosix(a: Io.net.IpAddress, storage: *PosixAddress) posix.socklen_t { - return switch (a) { +fn addressToPosix(a: *const Io.net.IpAddress, storage: *PosixAddress) posix.socklen_t { + return switch (a.*) { .ip4 => |ip4| { storage.in = address4ToPosix(ip4); return @sizeOf(posix.sockaddr.in); }, - .ip6 => |ip6| { + .ip6 => |*ip6| { storage.in6 = address6ToPosix(ip6); return @sizeOf(posix.sockaddr.in6); }, @@ -1539,7 +1620,7 @@ fn address4ToPosix(a: Io.net.Ip4Address) posix.sockaddr.in { }; } -fn address6ToPosix(a: Io.net.Ip6Address) posix.sockaddr.in6 { +fn address6ToPosix(a: *const Io.net.Ip6Address) posix.sockaddr.in6 { return .{ .port = std.mem.nativeToBig(u16, a.port), .flowinfo = a.flow, diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 02cec48b0c..f83dc7e97c 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -154,7 +154,7 @@ pub const IpAddress = union(enum) { /// A nonexistent interface was requested or the requested address was not local. AddressUnavailable, /// The local network interface used to reach the destination is offline. - NetworkSubsystemDown, + NetworkDown, /// Insufficient memory or other resource internal to the operating system. SystemResources, /// Per-process limit on the number of open file descriptors has been reached. @@ -192,7 +192,7 @@ pub const IpAddress = union(enum) { /// Insufficient memory or other resource internal to the operating system. SystemResources, /// The local network interface used to reach the destination is offline. - NetworkSubsystemDown, + NetworkDown, ProtocolUnsupportedBySystem, ProtocolUnsupportedByAddressFamily, /// Per-process limit on the number of open file descriptors has been reached. @@ -702,7 +702,10 @@ pub const ReceivedMessage = struct { pub const OutgoingMessage = struct { address: *const IpAddress, - data: []const u8, + data_ptr: [*]const u8, + /// Initialized with how many bytes of `data_ptr` to send. After sending + /// succeeds, replaced with how many bytes were actually sent. + data_len: usize, control: []const u8 = &.{}, }; @@ -808,9 +811,10 @@ pub const Socket = struct { } pub const SendError = error{ - /// The socket type requires that message be sent atomically, and the size of the message - /// to be sent made this impossible. The message is not transmitted. - MessageTooBig, + /// The socket type requires that message be sent atomically, and the + /// size of the message to be sent made this impossible. The message + /// was not transmitted, or was partially transmitted. + MessageOversize, /// The output queue for a network interface was full. This generally indicates that the /// interface has stopped sending, but may be caused by transient congestion. (Normally, /// this does not occur in Linux. Packets are just silently dropped when a device queue @@ -823,21 +827,29 @@ pub const Socket = struct { /// Network reached but no route to host. HostUnreachable, /// The local network interface used to reach the destination is offline. - NetworkSubsystemDown, + NetworkDown, /// The destination address is not listening. Can still occur for /// connectionless messages. ConnectionRefused, /// Operating system or protocol does not support the address family. AddressFamilyUnsupported, + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network connection was unexpectedly closed by recipient. + ConnectionResetByPeer, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketNotConnected, } || Io.UnexpectedError || Io.Cancelable; - /// Transfers `data` to `dest`, connectionless. + /// Transfers `data` to `dest`, connectionless, in one packet. pub fn send(s: *const Socket, io: Io, dest: *const IpAddress, data: []const u8) SendError!void { - const message: OutgoingMessage = .{ .address = dest, .data = data }; - return io.vtable.netSend(io.userdata, s.handle, &.{message}, .{}); + var message: OutgoingMessage = .{ .address = dest, .data_ptr = data.ptr, .data_len = data.len }; + try io.vtable.netSend(io.userdata, s.handle, &message, .{}); + if (message.data_len != data.len) return error.MessageOversize; } - pub fn sendMany(s: *const Socket, io: Io, messages: []const OutgoingMessage, flags: SendFlags) SendError!void { + pub fn sendMany(s: *const Socket, io: Io, messages: []OutgoingMessage, flags: SendFlags) SendError!void { return io.vtable.netSend(io.userdata, s.handle, messages, flags); } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index e77987aa1a..1a595a0b67 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -278,7 +278,8 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio for (mapped_nameservers) |*ns| { message_buffer[message_i] = .{ .address = ns, - .data = query, + .data_ptr = query.ptr, + .data_len = query.len, }; message_i += 1; } @@ -324,11 +325,12 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio if (next_answer_buffer == answers.len) break :send; }, 2 => { - const message: Io.net.OutgoingMessage = .{ + var message: Io.net.OutgoingMessage = .{ .address = ns, - .data = query, + .data_ptr = query.ptr, + .data_len = query.len, }; - io.vtable.netSend(io.userdata, socket.handle, &.{message}, .{}) catch {}; + io.vtable.netSend(io.userdata, socket.handle, (&message)[0..1], .{}) catch {}; continue; }, else => continue, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 8bbececdf0..1f921dddb7 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -2079,7 +2079,7 @@ pub fn sendmsg(fd: i32, msg: *const msghdr_const, flags: u32) usize { } } -pub fn sendmmsg(fd: i32, msgvec: [*]mmsghdr_const, vlen: u32, flags: u32) usize { +pub fn sendmmsg(fd: i32, msgvec: [*]mmsghdr, vlen: u32, flags: u32) usize { return syscall4(.sendmmsg, @as(usize, @bitCast(@as(isize, fd))), @intFromPtr(msgvec), vlen, flags); } @@ -5955,11 +5955,6 @@ pub const mmsghdr = extern struct { len: u32, }; -pub const mmsghdr_const = extern struct { - hdr: msghdr_const, - len: u32, -}; - pub const epoll_data = extern union { ptr: usize, fd: i32, From 961961cf85618083702799ef60f9f77dec806774 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 1 Oct 2025 17:18:30 -0700 Subject: [PATCH 069/244] std: fix msghdr and cmsghdr when using musl libc glibc and linux kernel use size_t for some field lengths while POSIX and musl use int. This bug would have caused breakage the first time someone tried to call sendmsg on a 64-bit big endian system when linking musl libc. my opinion: * msghdr.iovlen: kernel and glibc have it right. This field should definitely be size_t. With int, the padding bytes are wasted for no reason. * msghdr.controllen: POSIX and musl have it right. 4 bytes is plenty for the length, and it saves 4 bytes next to flags. * cmsghdr.len: POSIX and musl have it right. 4 bytes is plenty for the length, and it saves 4 bytes since the other fields are also 32-bits each. --- lib/std/c.zig | 100 ++++++++++++++++++------------------------- lib/std/os/linux.zig | 3 ++ 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/lib/std/c.zig b/lib/std/c.zig index 30e584c1a1..d4fd066233 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -4087,8 +4087,9 @@ pub const linger = switch (native_os) { }, else => void, }; + pub const msghdr = switch (native_os) { - .linux => linux.msghdr, + .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_msghdr else linux.msghdr, .openbsd, .emscripten, .dragonfly, @@ -4102,36 +4103,24 @@ pub const msghdr = switch (native_os) { .tvos, .visionos, .watchos, - => extern struct { - /// optional address - name: ?*sockaddr, - /// size of address - namelen: socklen_t, - /// scatter/gather array - iov: [*]iovec, - /// # elements in iov - iovlen: i32, - /// ancillary data - control: ?*anyopaque, - /// ancillary data buffer len - controllen: socklen_t, - /// flags on received message - flags: i32, - }, - // https://github.com/SerenityOS/serenity/blob/ac44ec5ebc707f9dd0c3d4759a1e17e91db5d74f/Kernel/API/POSIX/sys/socket.h#L74-L82 - .serenity => extern struct { - name: ?*anyopaque, - namelen: socklen_t, - iov: [*]iovec, - iovlen: c_int, - control: ?*anyopaque, - controllen: socklen_t, - flags: c_int, - }, + .serenity, // https://github.com/SerenityOS/serenity/blob/ac44ec5ebc707f9dd0c3d4759a1e17e91db5d74f/Kernel/API/POSIX/sys/socket.h#L74-L82 + => private.posix_msghdr, else => void, }; + +/// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/sys_socket.h.html +const posix_msghdr = extern struct { + name: ?*sockaddr, + namelen: socklen_t, + iov: [*]iovec, + iovlen: u32, + control: ?*anyopaque, + controllen: socklen_t, + flags: u32, +}; + pub const msghdr_const = switch (native_os) { - .linux => linux.msghdr_const, + .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_msghdr_const else linux.msghdr_const, .openbsd, .emscripten, .dragonfly, @@ -4145,36 +4134,25 @@ pub const msghdr_const = switch (native_os) { .tvos, .visionos, .watchos, - => extern struct { - /// optional address - name: ?*const sockaddr, - /// size of address - namelen: socklen_t, - /// scatter/gather array - iov: [*]const iovec_const, - /// # elements in iov - iovlen: u32, - /// ancillary data - control: ?*const anyopaque, - /// ancillary data buffer len - controllen: socklen_t, - /// flags on received message - flags: i32, - }, - .serenity => extern struct { - name: ?*const anyopaque, - namelen: socklen_t, - iov: [*]const iovec_const, - iovlen: c_uint, - control: ?*const anyopaque, - controllen: socklen_t, - flags: c_int, - }, + .serenity, + => posix_msghdr_const, else => void, }; + +const posix_msghdr_const = extern struct { + name: ?*const sockaddr, + namelen: socklen_t, + iov: [*]const iovec_const, + iovlen: u32, + control: ?*const anyopaque, + controllen: socklen_t, + flags: u32, +}; + pub const cmsghdr = switch (native_os) { + .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_cmsghdr else linux.cmsghdr, // https://github.com/emscripten-core/emscripten/blob/96371ed7888fc78c040179f4d4faa82a6a07a116/system/lib/libc/musl/include/sys/socket.h#L44 - .linux, .emscripten => linux.cmsghdr, + .emscripten => linux.cmsghdr, // https://github.com/freebsd/freebsd-src/blob/b197d2abcb6895d78bc9df8404e374397aa44748/sys/sys/socket.h#L492 .freebsd, // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/107c0518337ba90e7fa49e74845d8d44320c9a6d/sys/sys/socket.h#L452 @@ -4196,13 +4174,17 @@ pub const cmsghdr = switch (native_os) { .tvos, .visionos, .watchos, - => extern struct { - len: socklen_t, - level: c_int, - type: c_int, - }, + => posix_cmsghdr, + else => void, }; + +const posix_cmsghdr = extern struct { + len: socklen_t, + level: c_int, + type: c_int, +}; + pub const nfds_t = switch (native_os) { .linux => linux.nfds_t, .emscripten => emscripten.nfds_t, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 1f921dddb7..5ae50e0270 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -9840,8 +9840,10 @@ pub const msghdr = extern struct { name: ?*sockaddr, namelen: socklen_t, iov: [*]iovec, + /// The kernel and glibc use `usize` for this field; POSIX and musl use `c_int`. iovlen: usize, control: ?*anyopaque, + /// The kernel and glibc use `usize` for this field; POSIX and musl use `socklen_t`. controllen: usize, flags: u32, }; @@ -9858,6 +9860,7 @@ pub const msghdr_const = extern struct { // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/socket.h?id=b320789d6883cc00ac78ce83bccbfe7ed58afcf0#n105 pub const cmsghdr = extern struct { + /// The kernel and glibc use `usize` for this field; musl uses `socklen_t`. len: usize, level: i32, type: i32, From a6347a68a94b80c5b3e79a9bea3d7711a8f013a6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 1 Oct 2025 23:52:07 -0700 Subject: [PATCH 070/244] std.Io.net: implement receiving connectionless messages --- lib/std/Io.zig | 131 ++++++++++++++++++++----- lib/std/Io/EventLoop.zig | 2 +- lib/std/Io/File.zig | 2 +- lib/std/Io/Threaded.zig | 184 +++++++++++++++++++++++++++++++----- lib/std/Io/net.zig | 99 +++++++++++++++++-- lib/std/Io/net/HostName.zig | 140 ++++++++++++++------------- lib/std/net.zig | 8 +- lib/std/posix.zig | 42 ++++---- lib/std/posix/test.zig | 2 +- lib/std/zig/system.zig | 2 +- 10 files changed, 462 insertions(+), 150 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index ddfb8c2e01..4ae1e25c04 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -665,14 +665,14 @@ pub const VTable = struct { fileSeekBy: *const fn (?*anyopaque, file: File, offset: i64) File.SeekError!void, fileSeekTo: *const fn (?*anyopaque, file: File, offset: u64) File.SeekError!void, - now: *const fn (?*anyopaque, clockid: std.posix.clockid_t) NowError!Timestamp, - sleep: *const fn (?*anyopaque, clockid: std.posix.clockid_t, timeout: Timeout) SleepError!void, + now: *const fn (?*anyopaque, Timestamp.Clock) Timestamp.Error!i96, + sleep: *const fn (?*anyopaque, Timeout) SleepError!void, listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, ipBind: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, - netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) net.Socket.SendError!void, - netReceive: *const fn (?*anyopaque, handle: net.Socket.Handle, buffer: []u8, timeout: Timeout) net.Socket.ReceiveTimeoutError!net.ReceivedMessage, + netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) net.SendResult, + netReceive: *const fn (?*anyopaque, net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, net.ReceiveFlags, Timeout) struct { ?net.Socket.ReceiveTimeoutError, usize }, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, @@ -700,46 +700,135 @@ pub const UnexpectedError = error{ pub const Dir = @import("Io/Dir.zig"); pub const File = @import("Io/File.zig"); -pub const Timestamp = enum(i96) { - _, +pub const Timestamp = struct { + nanoseconds: i96, + clock: Clock, + + pub const Clock = enum { + /// A settable system-wide clock that measures real (i.e. wall-clock) + /// time. This clock is affected by discontinuous jumps in the system + /// time (e.g., if the system administrator manually changes the + /// clock), and by frequency adjust‐ ments performed by NTP and similar + /// applications. + /// This clock normally counts the number of seconds since + /// 1970-01-01 00:00:00 Coordinated Universal Time (UTC) except that it + /// ignores leap seconds; near a leap second it is typically + /// adjusted by NTP to stay roughly in sync with UTC. + realtime, + /// A nonsettable system-wide clock that represents time since some + /// unspecified point in the past. + /// + /// On Linux, corresponds to how long the system has been running since + /// it booted. + /// + /// Not affected by discontinuous jumps in the system time (e.g., if + /// the system administrator manually changes the clock), but is + /// affected by frequency adjustments. **This clock does not count time + /// that the system is suspended.** + /// + /// Guarantees that the time returned by consecutive calls will not go + /// backwards, but successive calls may return identical + /// (not-increased) time values. + monotonic, + /// Identical to `monotonic` except it also includes any time that the + /// system is suspended. + boottime, + }; pub fn durationTo(from: Timestamp, to: Timestamp) Duration { - return .{ .nanoseconds = @intFromEnum(to) - @intFromEnum(from) }; + assert(from.clock == to.clock); + return .{ .nanoseconds = to.nanoseconds - from.nanoseconds }; } pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { - return @enumFromInt(@intFromEnum(from) + duration.nanoseconds); + return .{ + .nanoseconds = from.nanoseconds + duration.nanoseconds, + .clock = from.clock, + }; } - pub fn fromNow(io: Io, clockid: std.posix.clockid_t, duration: Duration) NowError!Timestamp { - const now_ts = try now(io, clockid); + pub const Error = error{UnsupportedClock} || UnexpectedError; + + /// This function is not cancelable because first of all it does not block, + /// but more importantly, the cancelation logic itself may want to check + /// the time. + pub fn now(io: Io, clock: Clock) Error!Timestamp { + return .{ + .nanoseconds = try io.vtable.now(io.userdata, clock), + .clock = clock, + }; + } + + pub fn fromNow(io: Io, clock: Clock, duration: Duration) Error!Timestamp { + const now_ts = try now(io, clock); return addDuration(now_ts, duration); } + pub fn untilNow(timestamp: Timestamp, io: Io) Error!Duration { + const now_ts = try Timestamp.now(io, timestamp.clock); + return timestamp.durationTo(now_ts); + } + + pub fn durationFromNow(timestamp: Timestamp, io: Io) Error!Duration { + const now_ts = try now(io, timestamp.clock); + return now_ts.durationTo(timestamp); + } + + pub fn toClock(t: Timestamp, io: Io, clock: Clock) Error!Timestamp { + if (t.clock == clock) return t; + const now_old = try now(io, t.clock); + const now_new = try now(io, clock); + const duration = now_old.durationTo(t); + return now_new.addDuration(duration); + } + pub fn compare(lhs: Timestamp, op: std.math.CompareOperator, rhs: Timestamp) bool { - return std.math.compare(@intFromEnum(lhs), op, @intFromEnum(rhs)); + assert(lhs.clock == rhs.clock); + return std.math.compare(lhs.nanoseconds, op, rhs.nanoseconds); } }; + pub const Duration = struct { nanoseconds: i96, - pub fn ms(x: u64) Duration { + pub fn fromMilliseconds(x: i64) Duration { return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_ms }; } - pub fn seconds(x: u64) Duration { + pub fn fromSeconds(x: i64) Duration { return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_s }; } + + pub fn toMilliseconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_ms)); + } + + pub fn toSeconds(d: Duration) i64 { + return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_s)); + } }; + +/// Declares under what conditions an operation should return `error.Timeout`. pub const Timeout = union(enum) { none, - duration: Duration, + duration: ClockAndDuration, deadline: Timestamp, - pub const Error = error{Timeout}; + pub const Error = error{ Timeout, UnsupportedClock }; + + pub const ClockAndDuration = struct { + clock: Timestamp.Clock, + duration: Duration, + }; + + pub fn toDeadline(t: Timeout, io: Io) Timestamp.Error!?Timestamp { + return switch (t) { + .none => null, + .duration => |d| try .fromNow(io, d.clock, d.duration), + .deadline => |d| d, + }; + } }; -pub const NowError = std.posix.ClockGetTimeError || Cancelable; -pub const SleepError = error{ UnsupportedClock, Unexpected, Canceled }; pub const AnyFuture = opaque {}; @@ -1231,12 +1320,10 @@ pub fn cancelRequested(io: Io) bool { return io.vtable.cancelRequested(io.userdata); } -pub fn now(io: Io, clockid: std.posix.clockid_t) NowError!Timestamp { - return io.vtable.now(io.userdata, clockid); -} +pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable; -pub fn sleep(io: Io, clockid: std.posix.clockid_t, timeout: Timeout) SleepError!void { - return io.vtable.sleep(io.userdata, clockid, timeout); +pub fn sleep(io: Io, timeout: Timeout) SleepError!void { + return io.vtable.sleep(io.userdata, timeout); } pub fn sleepDuration(io: Io, duration: Duration) SleepError!void { diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 90f5dbdb22..d1d7799907 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -1406,7 +1406,7 @@ fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.o .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 63381fe99c..abd74bdd98 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -157,7 +157,7 @@ pub const ReadStreamingError = error{ ConnectionResetByPeer, ConnectionTimedOut, NotOpenForReading, - SocketNotConnected, + SocketUnconnected, /// This error occurs when no global event loop is configured, /// and reading from the file descriptor would block. WouldBlock, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3b72f1ede1..f61a75f9ba 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -811,7 +811,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NOTCAPABLE => return error.AccessDenied, @@ -834,7 +834,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, else => |err| return posix.unexpectedErrno(err), @@ -933,7 +933,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, @@ -960,7 +960,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, @@ -999,19 +999,29 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posi }; } -fn now(userdata: ?*anyopaque, clockid: posix.clockid_t) Io.NowError!Io.Timestamp { +fn now(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - const timespec = try posix.clock_gettime(clockid); - return @enumFromInt(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec); + _ = pool; + const clock_id: posix.clockid_t = clockToPosix(clock); + var tp: posix.timespec = undefined; + switch (posix.errno(posix.system.clock_gettime(clock_id, &tp))) { + .SUCCESS => return @intCast(@as(i128, tp.sec) * std.time.ns_per_s + tp.nsec), + .INVAL => return error.UnsupportedClock, + else => |err| return posix.unexpectedErrno(err), + } } -fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, timeout: Io.Timeout) Io.SleepError!void { +fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); + const clock_id: posix.clockid_t = clockToPosix(switch (timeout) { + .none => .monotonic, + .duration => |d| d.clock, + .deadline => |d| d.clock, + }); const deadline_nanoseconds: i96 = switch (timeout) { .none => std.math.maxInt(i96), - .duration => |duration| duration.nanoseconds, - .deadline => |deadline| @intFromEnum(deadline), + .duration => |d| d.duration.nanoseconds, + .deadline => |deadline| deadline.nanoseconds, }; var timespec: posix.timespec = .{ .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), @@ -1019,13 +1029,12 @@ fn sleep(userdata: ?*anyopaque, clockid: posix.clockid_t, timeout: Io.Timeout) I }; while (true) { try pool.checkCancel(); - switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clockid, .{ .ABSTIME = switch (timeout) { + switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clock_id, .{ .ABSTIME = switch (timeout) { .none, .duration => false, .deadline => true, } }, ×pec, ×pec))) { .SUCCESS => return, - .FAULT => |err| return errnoBug(err), - .INTR => {}, + .INTR => continue, .INVAL => return error.UnsupportedClock, else => |err| return posix.unexpectedErrno(err), } @@ -1313,15 +1322,18 @@ fn netSend( handle: Io.net.Socket.Handle, messages: []Io.net.OutgoingMessage, flags: Io.net.SendFlags, -) Io.net.Socket.SendError!void { +) Io.net.SendResult { const pool: *Pool = @ptrCast(@alignCast(userdata)); if (have_sendmmsg) { var i: usize = 0; while (messages.len - i != 0) { - i += try netSendMany(pool, handle, messages[i..], flags); + i += netSendMany(pool, handle, messages[i..], flags) catch |err| return .{ .fail = .{ + .err = err, + .sent = i, + } }; } - return; + return .success; } try pool.checkCancel(); @@ -1391,11 +1403,11 @@ fn netSendMany( .NOMEM => return error.SystemResources, .NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket. .OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type. - .PIPE => return error.SocketNotConnected, + .PIPE => return error.SocketUnconnected, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NETDOWN => return error.NetworkDown, else => |err| return posix.unexpectedErrno(err), } @@ -1405,16 +1417,128 @@ fn netSendMany( fn netReceive( userdata: ?*anyopaque, handle: Io.net.Socket.Handle, - buffer: []u8, + message_buffer: []Io.net.IncomingMessage, + data_buffer: []u8, + flags: Io.net.ReceiveFlags, timeout: Io.Timeout, -) Io.net.Socket.ReceiveTimeoutError!Io.net.ReceivedMessage { +) struct { ?Io.net.Socket.ReceiveTimeoutError, usize } { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - _ = handle; - _ = buffer; - _ = timeout; - @panic("TODO"); + // recvmmsg is useless, here's why: + // * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371) + // * it wants iovecs for each message but we have a better API: one data + // buffer to handle all the messages. The better API cannot be lowered to + // the split vectors though because reducing the buffer size might make + // some messages unreceivable. + + // So the strategy instead is to use poll with timeout and then non-blocking + // recvmsg calls. + const posix_flags: u32 = + @as(u32, if (flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (flags.peek) posix.MSG.PEEK else 0) | + @as(u32, if (flags.trunc) posix.MSG.TRUNC else 0) | + posix.MSG.DONTWAIT | posix.MSG.NOSIGNAL; + + var poll_fds: [1]posix.pollfd = .{ + .{ + .fd = handle, + .events = posix.POLL.IN, + .revents = undefined, + }, + }; + var message_i: usize = 0; + var data_i: usize = 0; + + // TODO: recvmsg first, then poll if EAGAIN. saves syscall in case the messages are already queued. + + const deadline = timeout.toDeadline(pool.io()) catch |err| return .{ err, message_i }; + + poll: while (true) { + pool.checkCancel() catch |err| return .{ err, message_i }; + + if (message_i > 0 or message_buffer.len - message_i == 0) return .{ null, message_i }; + + const max_poll_ms = std.math.maxInt(u31); + const timeout_ms: u31 = if (deadline) |d| t: { + const duration = d.durationFromNow(pool.io()) catch |err| return .{ err, message_i }; + if (duration.nanoseconds <= 0) return .{ error.Timeout, message_i }; + break :t @intCast(@min(max_poll_ms, duration.toMilliseconds())); + } else max_poll_ms; + + const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms); + switch (posix.errno(poll_rc)) { + .SUCCESS => { + if (poll_rc == 0) { + // Possibly spurious timeout. + if (deadline == null) continue; + return .{ error.Timeout, message_i }; + } + + // Proceed to recvmsg. + while (true) { + pool.checkCancel() catch |err| return .{ err, message_i }; + + const message = &message_buffer[message_i]; + const remaining_data_buffer = data_buffer[data_i..]; + var storage: PosixAddress = undefined; + var iov: posix.iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len }; + var msg: posix.msghdr = .{ + .name = &storage.any, + .namelen = @sizeOf(PosixAddress), + .iov = (&iov)[0..1], + .iovlen = 1, + .control = message.control.ptr, + .controllen = message.control.len, + .flags = undefined, + }; + + const rc = posix.system.recvmsg(handle, &msg, posix_flags); + switch (posix.errno(rc)) { + .SUCCESS => { + const data = remaining_data_buffer[0..@intCast(rc)]; + data_i += data.len; + message.* = .{ + .from = addressFromPosix(&storage), + .data = data, + .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, + .flags = .{ + .eor = (msg.flags & posix.MSG.EOR) != 0, + .trunc = (msg.flags & posix.MSG.TRUNC) != 0, + .ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0, + .oob = (msg.flags & posix.MSG.OOB) != 0, + .errqueue = (msg.flags & posix.MSG.ERRQUEUE) != 0, + }, + }; + message_i += 1; + continue; + }, + .AGAIN => continue :poll, + .BADF => |err| return .{ errnoBug(err), message_i }, + .NFILE => return .{ error.SystemFdQuotaExceeded, message_i }, + .MFILE => return .{ error.ProcessFdQuotaExceeded, message_i }, + .INTR => continue, + .FAULT => |err| return .{ errnoBug(err), message_i }, + .INVAL => |err| return .{ errnoBug(err), message_i }, + .NOBUFS => return .{ error.SystemResources, message_i }, + .NOMEM => return .{ error.SystemResources, message_i }, + .NOTCONN => return .{ error.SocketUnconnected, message_i }, + .NOTSOCK => |err| return .{ errnoBug(err), message_i }, + .MSGSIZE => return .{ error.MessageOversize, message_i }, + .PIPE => return .{ error.SocketUnconnected, message_i }, + .OPNOTSUPP => |err| return .{ errnoBug(err), message_i }, + .CONNRESET => return .{ error.ConnectionResetByPeer, message_i }, + .NETDOWN => return .{ error.NetworkDown, message_i }, + else => |err| return .{ posix.unexpectedErrno(err), message_i }, + } + } + }, + .INTR => continue, + .FAULT => |err| return .{ errnoBug(err), message_i }, + .INVAL => |err| return .{ errnoBug(err), message_i }, + .NOMEM => return .{ error.SystemResources, message_i }, + else => |err| return .{ posix.unexpectedErrno(err), message_i }, + } + } } fn netWritePosix( @@ -1653,3 +1777,11 @@ fn posixProtocol(protocol: ?Io.net.Protocol) u32 { fn recoverableOsBugDetected() void { if (builtin.mode == .Debug) unreachable; } + +fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t { + return switch (clock) { + .realtime => posix.CLOCK.REALTIME, + .monotonic => posix.CLOCK.MONOTONIC, + .boottime => posix.CLOCK.BOOTTIME, + }; +} diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index f83dc7e97c..5672abd071 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -695,9 +695,41 @@ pub const Ip6Address = struct { }; }; -pub const ReceivedMessage = struct { +pub const ReceiveFlags = packed struct(u8) { + oob: bool = false, + peek: bool = false, + trunc: bool = false, + _: u5 = 0, +}; + +pub const IncomingMessage = struct { + /// Populated by receive functions. from: IpAddress, - len: usize, + /// Populated by receive functions, points into the caller-supplied buffer. + data: []u8, + /// Supplied by caller before calling receive functions; mutated by receive + /// functions. + control: []u8 = &.{}, + /// Populated by receive functions. + flags: Flags, + + pub const Flags = packed struct(u8) { + /// indicates end-of-record; the data returned completed a record + /// (generally used with sockets of type SOCK_SEQPACKET). + eor: bool, + /// indicates that the trailing portion of a datagram was discarded + /// because the datagram was larger than the buffer supplied. + trunc: bool, + /// indicates that some control data was discarded due to lack of + /// space in the buffer for ancil‐ lary data. + ctrunc: bool, + /// indicates expedited or out-of-band data was received. + oob: bool, + /// indicates that no data was received but an extended error from the + /// socket error queue. + errqueue: bool, + _: u3 = 0, + }; }; pub const OutgoingMessage = struct { @@ -718,6 +750,14 @@ pub const SendFlags = packed struct(u8) { _: u3 = 0, }; +pub const SendResult = union(enum) { + success, + fail: struct { + err: Socket.SendError, + sent: usize, + }, +}; + pub const Interface = struct { /// Value 0 indicates `none`. index: u32, @@ -839,7 +879,7 @@ pub const Socket = struct { ConnectionResetByPeer, /// Local end has been shut down on a connection-oriented socket, or /// the socket was never connected. - SocketNotConnected, + SocketUnconnected, } || Io.UnexpectedError || Io.Cancelable; /// Transfers `data` to `dest`, connectionless, in one packet. @@ -853,14 +893,34 @@ pub const Socket = struct { return io.vtable.netSend(io.userdata, s.handle, messages, flags); } - pub const ReceiveError = error{} || Io.UnexpectedError || Io.Cancelable; + pub const ReceiveError = error{ + /// Insufficient memory or other resource internal to the operating system. + SystemResources, + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. + SocketUnconnected, + /// The socket type requires that message be sent atomically, and the + /// size of the message to be sent made this impossible. The message + /// was not transmitted, or was partially transmitted. + MessageOversize, + /// Network connection was unexpectedly closed by sender. + ConnectionResetByPeer, + /// The local network interface used to reach the destination is offline. + NetworkDown, + } || Io.UnexpectedError || Io.Cancelable; /// Waits for data. Connectionless. /// /// See also: /// * `receiveTimeout` - pub fn receive(s: *const Socket, io: Io, source: *const IpAddress, buffer: []u8) ReceiveError!ReceivedMessage { - return io.vtable.netReceive(io.userdata, s.handle, source, buffer, .none); + pub fn receive(s: *const Socket, io: Io, buffer: []u8) ReceiveError!IncomingMessage { + var message: IncomingMessage = undefined; + assert(1 == try io.vtable.netReceive(io.userdata, s.handle, (&message)[0..1], buffer, .{}, .none)); + return message; } pub const ReceiveTimeoutError = ReceiveError || Io.Timeout.Error; @@ -871,13 +931,36 @@ pub const Socket = struct { /// /// See also: /// * `receive` + /// * `receiveManyTimeout` pub fn receiveTimeout( s: *const Socket, io: Io, buffer: []u8, timeout: Io.Timeout, - ) ReceiveTimeoutError!ReceivedMessage { - return io.vtable.netReceive(io.userdata, s.handle, buffer, timeout); + ) ReceiveTimeoutError!IncomingMessage { + var message: IncomingMessage = undefined; + assert(1 == try io.vtable.netReceive(io.userdata, s.handle, (&message)[0..1], buffer, .{}, timeout)); + return message; + } + + /// Waits until at least one message is delivered, possibly returning more + /// than one message. Connectionless. + /// + /// Returns number of messages received, or `error.Timeout` if no message + /// arrives early enough. + /// + /// See also: + /// * `receive` + /// * `receiveTimeout` + pub fn receiveManyTimeout( + s: *const Socket, + io: Io, + message_buffer: []IncomingMessage, + data_buffer: []u8, + flags: ReceiveFlags, + timeout: Io.Timeout, + ) struct { ?ReceiveTimeoutError, usize } { + return io.vtable.netReceive(io.userdata, s.handle, message_buffer, data_buffer, flags, timeout); } }; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 1a595a0b67..ce17b86633 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -52,7 +52,7 @@ pub const LookupError = error{ InvalidDnsARecord, InvalidDnsAAAARecord, NameServerFailure, -} || Io.NowError || IpAddress.BindError || Io.File.OpenError || Io.File.Reader.Error || Io.Cancelable; +} || Io.Timestamp.Error || IpAddress.BindError || Io.File.OpenError || Io.File.Reader.Error || Io.Cancelable; pub const LookupResult = struct { /// How many `LookupOptions.addresses_buffer` elements are populated. @@ -222,11 +222,11 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio .{ .af = .ip4, .rr = std.posix.RR.AAAA }, }; var query_buffers: [2][280]u8 = undefined; - var answer_buffers: [2][512]u8 = undefined; + var answer_buffer: [2 * 512]u8 = undefined; var queries_buffer: [2][]const u8 = undefined; var answers_buffer: [2][]const u8 = undefined; var nq: usize = 0; - var next_answer_buffer: usize = 0; + var answer_buffer_i: usize = 0; for (family_records) |fr| { if (options.family != fr.af) { @@ -262,79 +262,89 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); const queries = queries_buffer[0..nq]; const answers = answers_buffer[0..queries.len]; + var answers_remaining = answers.len; for (answers) |*answer| answer.len = 0; - var now_ts = try io.now(.MONOTONIC); - const final_ts = now_ts.addDuration(.seconds(rc.timeout_seconds)); + // boottime is chosen because time the computer is suspended should count + // against time spent waiting for external messages to arrive. + var now_ts = try Io.Timestamp.now(io, .boottime); + const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); const attempt_duration: Io.Duration = .{ .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, }; - send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try io.now(.MONOTONIC)) { - var message_buffer: [queries_buffer.len * ResolvConf.max_nameservers]Io.net.OutgoingMessage = undefined; - var message_i: usize = 0; - for (queries, answers) |query, *answer| { - if (answer.len != 0) continue; - for (mapped_nameservers) |*ns| { - message_buffer[message_i] = .{ - .address = ns, - .data_ptr = query.ptr, - .data_len = query.len, - }; - message_i += 1; - } - } - io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{}) catch {}; - - const timeout: Io.Timeout = .{ .deadline = now_ts.addDuration(attempt_duration) }; - - while (true) { - const buf = &answer_buffers[next_answer_buffer]; - const reply = socket.receiveTimeout(io, buf, timeout) catch |err| switch (err) { - error.Canceled => return error.Canceled, - error.Timeout => continue :send, - else => continue, - }; - - // Ignore non-identifiable packets. - if (reply.len < 4) continue; - - // Ignore replies from addresses we didn't send to. - const ns = for (mapped_nameservers) |*ns| { - if (reply.from.eql(ns)) break ns; - } else { - continue; - }; - - const reply_msg = buf[0..reply.len]; - - // Find which query this answer goes with, if any. - const query, const answer = for (queries, answers) |query, *answer| { - if (reply_msg[0] == query[0] and reply_msg[1] == query[1]) break .{ query, answer }; - } else { - continue; - }; - if (answer.len != 0) continue; - - // Only accept positive or negative responses; retry immediately on - // server failure, and ignore all other codes such as refusal. - switch (reply_msg[3] & 15) { - 0, 3 => { - answer.* = reply_msg; - next_answer_buffer += 1; - if (next_answer_buffer == answers.len) break :send; - }, - 2 => { - var message: Io.net.OutgoingMessage = .{ + send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boottime)) { + const max_messages = queries_buffer.len * ResolvConf.max_nameservers; + { + var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; + var message_i: usize = 0; + for (queries, answers) |query, *answer| { + if (answer.len != 0) continue; + for (mapped_nameservers) |*ns| { + message_buffer[message_i] = .{ .address = ns, .data_ptr = query.ptr, .data_len = query.len, }; - io.vtable.netSend(io.userdata, socket.handle, (&message)[0..1], .{}) catch {}; - continue; - }, - else => continue, + message_i += 1; + } } + _ = io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{}); + } + + const timeout: Io.Timeout = .{ .deadline = now_ts.addDuration(attempt_duration) }; + + while (true) { + var message_buffer: [max_messages]Io.net.IncomingMessage = undefined; + const buf = answer_buffer[answer_buffer_i..]; + const recv_err, const recv_n = socket.receiveManyTimeout(io, &message_buffer, buf, .{}, timeout); + for (message_buffer[0..recv_n]) |*received_message| { + const reply = received_message.data; + // Ignore non-identifiable packets. + if (reply.len < 4) continue; + + // Ignore replies from addresses we didn't send to. + const ns = for (mapped_nameservers) |*ns| { + if (received_message.from.eql(ns)) break ns; + } else { + continue; + }; + + // Find which query this answer goes with, if any. + const query, const answer = for (queries, answers) |query, *answer| { + if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer }; + } else { + continue; + }; + if (answer.len != 0) continue; + + // Only accept positive or negative responses; retry immediately on + // server failure, and ignore all other codes such as refusal. + switch (reply[3] & 15) { + 0, 3 => { + answer.* = reply; + answer_buffer_i += reply.len; + answers_remaining -= 1; + if (answer_buffer.len - answer_buffer_i == 0) break :send; + if (answers_remaining == 0) break :send; + }, + 2 => { + var retry_message: Io.net.OutgoingMessage = .{ + .address = ns, + .data_ptr = query.ptr, + .data_len = query.len, + }; + _ = io.vtable.netSend(io.userdata, socket.handle, (&retry_message)[0..1], .{}); + continue; + }, + else => continue, + } + } + if (recv_err) |err| switch (err) { + error.Canceled => return error.Canceled, + error.Timeout => continue :send, + else => continue, + }; } } else { return error.NameServerFailure; diff --git a/lib/std/net.zig b/lib/std/net.zig index 083ee1da93..7863967e63 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -1917,7 +1917,7 @@ pub const Stream = struct { MessageTooBig, NetworkSubsystemFailed, ConnectionResetByPeer, - SocketNotConnected, + SocketUnconnected, }; pub const WriteError = posix.SendMsgError || error{ @@ -1926,7 +1926,7 @@ pub const Stream = struct { MessageTooBig, NetworkSubsystemFailed, SystemResources, - SocketNotConnected, + SocketUnconnected, Unexpected, }; @@ -2004,7 +2004,7 @@ pub const Stream = struct { .WSAEMSGSIZE => return error.MessageTooBig, .WSAENETDOWN => return error.NetworkSubsystemFailed, .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENOTCONN => return error.SocketNotConnected, + .WSAENOTCONN => return error.SocketUnconnected, .WSAEWOULDBLOCK => return error.WouldBlock, .WSANOTINITIALISED => unreachable, // WSAStartup must be called before this function .WSA_IO_PENDING => unreachable, @@ -2171,7 +2171,7 @@ pub const Stream = struct { .WSAENETDOWN => return error.NetworkSubsystemFailed, .WSAENETRESET => return error.ConnectionResetByPeer, .WSAENOBUFS => return error.SystemResources, - .WSAENOTCONN => return error.SocketNotConnected, + .WSAENOTCONN => return error.SocketUnconnected, .WSAENOTSOCK => unreachable, // not a socket .WSAEOPNOTSUPP => unreachable, // only for message-oriented sockets .WSAESHUTDOWN => unreachable, // cannot send on a socket after write shutdown diff --git a/lib/std/posix.zig b/lib/std/posix.zig index f97b0574af..93676f2a74 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -840,7 +840,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NOTCAPABLE => return error.AccessDenied, @@ -869,7 +869,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, else => |err| return unexpectedErrno(err), @@ -909,7 +909,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NOTCAPABLE => return error.AccessDenied, @@ -931,7 +931,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, else => |err| return unexpectedErrno(err), @@ -978,7 +978,7 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, @@ -1011,7 +1011,7 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, @@ -1129,7 +1129,7 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, @@ -1155,7 +1155,7 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize { .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.ConnectionTimedOut, .NXIO => return error.Unseekable, @@ -3711,7 +3711,7 @@ pub const ShutdownError = error{ NetworkSubsystemFailed, /// The socket is not connected (connection-oriented sockets only). - SocketNotConnected, + SocketUnconnected, SystemResources, } || UnexpectedError; @@ -3731,7 +3731,7 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { .WSAEINPROGRESS => return error.BlockingOperationInProgress, .WSAEINVAL => unreachable, .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOTCONN => return error.SocketNotConnected, + .WSAENOTCONN => return error.SocketUnconnected, .WSAENOTSOCK => unreachable, .WSANOTINITIALISED => unreachable, else => |err| return windows.unexpectedWSAError(err), @@ -3746,7 +3746,7 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { .SUCCESS => return, .BADF => unreachable, .INVAL => unreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, .NOBUFS => return error.SystemResources, else => |err| return unexpectedErrno(err), @@ -6181,7 +6181,7 @@ pub const SendMsgError = SendError || error{ NotDir, /// The socket is not connected (connection-oriented sockets only). - SocketNotConnected, + SocketUnconnected, AddressNotAvailable, }; @@ -6212,7 +6212,7 @@ pub fn sendmsg( .WSAENETDOWN => return error.NetworkSubsystemFailed, .WSAENETRESET => return error.ConnectionResetByPeer, .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketNotConnected, + .WSAENOTCONN => return error.SocketUnconnected, .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. .WSAEWOULDBLOCK => return error.WouldBlock, .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. @@ -6248,7 +6248,7 @@ pub fn sendmsg( .NOTDIR => return error.NotDir, .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NETDOWN => return error.NetworkSubsystemFailed, else => |err| return unexpectedErrno(err), } @@ -6315,7 +6315,7 @@ pub fn sendto( .WSAENETDOWN => return error.NetworkSubsystemFailed, .WSAENETRESET => return error.ConnectionResetByPeer, .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketNotConnected, + .WSAENOTCONN => return error.SocketUnconnected, .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. .WSAEWOULDBLOCK => return error.WouldBlock, .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. @@ -6353,7 +6353,7 @@ pub fn sendto( .NOTDIR => return error.NotDir, .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NETDOWN => return error.NetworkSubsystemFailed, else => |err| return unexpectedErrno(err), } @@ -6393,7 +6393,7 @@ pub fn send( error.NotDir => unreachable, error.NetworkUnreachable => unreachable, error.AddressNotAvailable => unreachable, - error.SocketNotConnected => unreachable, + error.SocketUnconnected => unreachable, error.UnreachableAddress => unreachable, else => |e| return e, }; @@ -6579,7 +6579,7 @@ pub const RecvFromError = error{ NetworkSubsystemFailed, /// The socket is not connected (connection-oriented sockets only). - SocketNotConnected, + SocketUnconnected, /// The other end closed the socket unexpectedly or a read is executed on a shut down socket BrokenPipe, @@ -6608,7 +6608,7 @@ pub fn recvfrom( .WSAEINVAL => return error.SocketNotBound, .WSAEMSGSIZE => return error.MessageTooBig, .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENOTCONN => return error.SocketNotConnected, + .WSAENOTCONN => return error.SocketUnconnected, .WSAEWOULDBLOCK => return error.WouldBlock, .WSAETIMEDOUT => return error.ConnectionTimedOut, // TODO: handle more errors @@ -6623,7 +6623,7 @@ pub fn recvfrom( .BADF => unreachable, // always a race condition .FAULT => unreachable, .INVAL => unreachable, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, .INTR => continue, .AGAIN => return error.WouldBlock, @@ -6675,7 +6675,7 @@ pub fn recvmsg( .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, - .NOTCONN => return error.SocketNotConnected, + .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .MSGSIZE => return error.MessageTooBig, .PIPE => return error.BrokenPipe, diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 50ffd7998f..87b101e1e9 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -634,7 +634,7 @@ test "shutdown socket" { } const sock = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0); posix.shutdown(sock, .both) catch |err| switch (err) { - error.SocketNotConnected => {}, + error.SocketUnconnected => {}, else => |e| return e, }; std.net.Stream.close(.{ .handle = sock }); diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index e90c4ae023..47ff3d05fa 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -1283,7 +1283,7 @@ fn preadAtLeast(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usi error.Unseekable => return error.UnableToReadElfFile, error.ConnectionResetByPeer => return error.UnableToReadElfFile, error.ConnectionTimedOut => return error.UnableToReadElfFile, - error.SocketNotConnected => return error.UnableToReadElfFile, + error.SocketUnconnected => return error.UnableToReadElfFile, error.Unexpected => return error.Unexpected, error.InputOutput => return error.FileSystem, error.AccessDenied => return error.Unexpected, From 62d0dd0d36efc6501b8b1c37bf8c7052cfdc3ef2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 2 Oct 2025 16:59:22 -0700 Subject: [PATCH 071/244] std.Io.Threaded.netReceive: recvmsg first, then poll Calling recvmsg first means no poll syscall needed when messages are already in the operating system queue. Empirically, this happens when repeating a DNS query that has been already been made recently. In such case, poll() is never called! --- lib/std/Io/Threaded.zig | 147 ++++++++++++++++++++-------------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f61a75f9ba..1957c7f210 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1431,8 +1431,8 @@ fn netReceive( // the split vectors though because reducing the buffer size might make // some messages unreceivable. - // So the strategy instead is to use poll with timeout and then non-blocking - // recvmsg calls. + // So the strategy instead is to use non-blocking recvmsg calls, calling + // poll() with timeout if the first one returns EAGAIN. const posix_flags: u32 = @as(u32, if (flags.oob) posix.MSG.OOB else 0) | @as(u32, if (flags.peek) posix.MSG.PEEK else 0) | @@ -1449,93 +1449,92 @@ fn netReceive( var message_i: usize = 0; var data_i: usize = 0; - // TODO: recvmsg first, then poll if EAGAIN. saves syscall in case the messages are already queued. - const deadline = timeout.toDeadline(pool.io()) catch |err| return .{ err, message_i }; - poll: while (true) { + recv: while (true) { pool.checkCancel() catch |err| return .{ err, message_i }; - if (message_i > 0 or message_buffer.len - message_i == 0) return .{ null, message_i }; + if (message_buffer.len - message_i == 0) return .{ null, message_i }; + const message = &message_buffer[message_i]; + const remaining_data_buffer = data_buffer[data_i..]; + var storage: PosixAddress = undefined; + var iov: posix.iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len }; + var msg: posix.msghdr = .{ + .name = &storage.any, + .namelen = @sizeOf(PosixAddress), + .iov = (&iov)[0..1], + .iovlen = 1, + .control = message.control.ptr, + .controllen = message.control.len, + .flags = undefined, + }; - const max_poll_ms = std.math.maxInt(u31); - const timeout_ms: u31 = if (deadline) |d| t: { - const duration = d.durationFromNow(pool.io()) catch |err| return .{ err, message_i }; - if (duration.nanoseconds <= 0) return .{ error.Timeout, message_i }; - break :t @intCast(@min(max_poll_ms, duration.toMilliseconds())); - } else max_poll_ms; - - const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms); - switch (posix.errno(poll_rc)) { + const recv_rc = posix.system.recvmsg(handle, &msg, posix_flags); + switch (posix.errno(recv_rc)) { .SUCCESS => { - if (poll_rc == 0) { - // Possibly spurious timeout. - if (deadline == null) continue; - return .{ error.Timeout, message_i }; - } + const data = remaining_data_buffer[0..@intCast(recv_rc)]; + data_i += data.len; + message.* = .{ + .from = addressFromPosix(&storage), + .data = data, + .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, + .flags = .{ + .eor = (msg.flags & posix.MSG.EOR) != 0, + .trunc = (msg.flags & posix.MSG.TRUNC) != 0, + .ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0, + .oob = (msg.flags & posix.MSG.OOB) != 0, + .errqueue = (msg.flags & posix.MSG.ERRQUEUE) != 0, + }, + }; + message_i += 1; + continue; + }, + .AGAIN => while (true) { + pool.checkCancel() catch |err| return .{ err, message_i }; + if (message_i != 0) return .{ null, message_i }; - // Proceed to recvmsg. - while (true) { - pool.checkCancel() catch |err| return .{ err, message_i }; + const max_poll_ms = std.math.maxInt(u31); + const timeout_ms: u31 = if (deadline) |d| t: { + const duration = d.durationFromNow(pool.io()) catch |err| return .{ err, message_i }; + if (duration.nanoseconds <= 0) return .{ error.Timeout, message_i }; + break :t @intCast(@min(max_poll_ms, duration.toMilliseconds())); + } else max_poll_ms; - const message = &message_buffer[message_i]; - const remaining_data_buffer = data_buffer[data_i..]; - var storage: PosixAddress = undefined; - var iov: posix.iovec = .{ .base = remaining_data_buffer.ptr, .len = remaining_data_buffer.len }; - var msg: posix.msghdr = .{ - .name = &storage.any, - .namelen = @sizeOf(PosixAddress), - .iov = (&iov)[0..1], - .iovlen = 1, - .control = message.control.ptr, - .controllen = message.control.len, - .flags = undefined, - }; + const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms); + switch (posix.errno(poll_rc)) { + .SUCCESS => { + if (poll_rc == 0) { + // Although spurious timeouts are OK, when no deadline + // is passed we must not return `error.Timeout`. + if (deadline == null) continue; + return .{ error.Timeout, message_i }; + } + continue :recv; + }, + .INTR => continue, - const rc = posix.system.recvmsg(handle, &msg, posix_flags); - switch (posix.errno(rc)) { - .SUCCESS => { - const data = remaining_data_buffer[0..@intCast(rc)]; - data_i += data.len; - message.* = .{ - .from = addressFromPosix(&storage), - .data = data, - .control = if (msg.control) |ptr| @as([*]u8, @ptrCast(ptr))[0..msg.controllen] else message.control, - .flags = .{ - .eor = (msg.flags & posix.MSG.EOR) != 0, - .trunc = (msg.flags & posix.MSG.TRUNC) != 0, - .ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0, - .oob = (msg.flags & posix.MSG.OOB) != 0, - .errqueue = (msg.flags & posix.MSG.ERRQUEUE) != 0, - }, - }; - message_i += 1; - continue; - }, - .AGAIN => continue :poll, - .BADF => |err| return .{ errnoBug(err), message_i }, - .NFILE => return .{ error.SystemFdQuotaExceeded, message_i }, - .MFILE => return .{ error.ProcessFdQuotaExceeded, message_i }, - .INTR => continue, - .FAULT => |err| return .{ errnoBug(err), message_i }, - .INVAL => |err| return .{ errnoBug(err), message_i }, - .NOBUFS => return .{ error.SystemResources, message_i }, - .NOMEM => return .{ error.SystemResources, message_i }, - .NOTCONN => return .{ error.SocketUnconnected, message_i }, - .NOTSOCK => |err| return .{ errnoBug(err), message_i }, - .MSGSIZE => return .{ error.MessageOversize, message_i }, - .PIPE => return .{ error.SocketUnconnected, message_i }, - .OPNOTSUPP => |err| return .{ errnoBug(err), message_i }, - .CONNRESET => return .{ error.ConnectionResetByPeer, message_i }, - .NETDOWN => return .{ error.NetworkDown, message_i }, - else => |err| return .{ posix.unexpectedErrno(err), message_i }, - } + .FAULT => |err| return .{ errnoBug(err), message_i }, + .INVAL => |err| return .{ errnoBug(err), message_i }, + .NOMEM => return .{ error.SystemResources, message_i }, + else => |err| return .{ posix.unexpectedErrno(err), message_i }, } }, .INTR => continue, + + .BADF => |err| return .{ errnoBug(err), message_i }, + .NFILE => return .{ error.SystemFdQuotaExceeded, message_i }, + .MFILE => return .{ error.ProcessFdQuotaExceeded, message_i }, .FAULT => |err| return .{ errnoBug(err), message_i }, .INVAL => |err| return .{ errnoBug(err), message_i }, + .NOBUFS => return .{ error.SystemResources, message_i }, .NOMEM => return .{ error.SystemResources, message_i }, + .NOTCONN => return .{ error.SocketUnconnected, message_i }, + .NOTSOCK => |err| return .{ errnoBug(err), message_i }, + .MSGSIZE => return .{ error.MessageOversize, message_i }, + .PIPE => return .{ error.SocketUnconnected, message_i }, + .OPNOTSUPP => |err| return .{ errnoBug(err), message_i }, + .CONNRESET => return .{ error.ConnectionResetByPeer, message_i }, + .NETDOWN => return .{ error.NetworkDown, message_i }, else => |err| return .{ posix.unexpectedErrno(err), message_i }, } } From f1a590c876ccf42a932fef8e3c1fe8d69bfe5cec Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 2 Oct 2025 19:12:25 -0700 Subject: [PATCH 072/244] std.Io.net.HostName: implement DNS reply parsing --- lib/std/Io/net/HostName.zig | 47 ++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index ce17b86633..edc8d45965 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -363,17 +363,19 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio continue; }) |record| switch (record.rr) { std.posix.RR.A => { - if (record.data.len != 4) return error.InvalidDnsARecord; + const data = record.packet[record.data_off..][0..record.data_len]; + if (data.len != 4) return error.InvalidDnsARecord; options.addresses_buffer[addresses_len] = .{ .ip4 = .{ - .bytes = record.data[0..4].*, + .bytes = data[0..4].*, .port = options.port, } }; addresses_len += 1; }, std.posix.RR.AAAA => { - if (record.data.len != 16) return error.InvalidDnsAAAARecord; + const data = record.packet[record.data_off..][0..record.data_len]; + if (data.len != 16) return error.InvalidDnsAAAARecord; options.addresses_buffer[addresses_len] = .{ .ip6 = .{ - .bytes = record.data[0..16].*, + .bytes = data[0..16].*, .port = options.port, } }; addresses_len += 1; @@ -575,23 +577,52 @@ pub fn expandDomainName( pub const DnsResponse = struct { bytes: []const u8, + bytes_index: u32, + answers_remaining: u16, pub const Answer = struct { rr: u8, - data: []const u8, packet: []const u8, + data_off: u32, + data_len: u16, }; pub const Error = error{InvalidDnsPacket}; pub fn init(r: []const u8) Error!DnsResponse { if (r.len < 12) return error.InvalidDnsPacket; - return .{ .bytes = r }; + if ((r[3] & 15) != 0) return .{ .bytes = r, .bytes_index = 3, .answers_remaining = 0 }; + var i: u32 = 12; + var query_count = std.mem.readInt(u16, r[4..6], .big); + while (query_count != 0) : (query_count -= 1) { + while (i < r.len and r[i] -% 1 < 127) i += 1; + if (r.len - i < 6) return error.InvalidDnsPacket; + i = i + 5 + @intFromBool(r[i] != 0); + } + return .{ + .bytes = r, + .bytes_index = i, + .answers_remaining = std.mem.readInt(u16, r[6..8], .big), + }; } pub fn next(dr: *DnsResponse) Error!?Answer { - _ = dr; - @panic("TODO"); + if (dr.answers_remaining == 0) return null; + dr.answers_remaining -= 1; + const r = dr.bytes; + var i = dr.bytes_index; + while (i < r.len and r[i] -% 1 < 127) i += 1; + if (r.len - i < 12) return error.InvalidDnsPacket; + i = i + 1 + @intFromBool(r[i] != 0); + const len = std.mem.readInt(u16, r[i + 8 ..][0..2], .big); + if (i + 10 + len > r.len) return error.InvalidDnsPacket; + defer dr.bytes_index = i + 10 + len; + return .{ + .rr = r[i + 1], + .packet = r, + .data_off = i + 10, + .data_len = len, + }; } }; From 85a6fea3be48bdc8e7060eca6d81b3a8906eeff7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 2 Oct 2025 20:45:16 -0700 Subject: [PATCH 073/244] std.Io.net.HostName: implement DNS name expansion --- lib/std/Io/net/HostName.zig | 96 +++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index edc8d45965..c8924e1fa6 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -51,6 +51,7 @@ pub const LookupError = error{ ResolvConfParseFailed, InvalidDnsARecord, InvalidDnsAAAARecord, + InvalidDnsCnameRecord, NameServerFailure, } || Io.Timestamp.Error || IpAddress.BindError || Io.File.OpenError || Io.File.Reader.Error || Io.Cancelable; @@ -381,16 +382,8 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio addresses_len += 1; }, std.posix.RR.CNAME => { - _ = &canonical_name; - @panic("TODO"); - //var tmp: [256]u8 = undefined; - //// Returns len of compressed name. strlen to get canon name. - //_ = try posix.dn_expand(packet, record.data, &tmp); - //const canon_name = mem.sliceTo(&tmp, 0); - //if (isValidHostName(canon_name)) { - // ctx.canon.items.len = 0; - // try ctx.canon.appendSlice(gpa, canon_name); - //} + _, canonical_name = expand(record.packet, record.data_off, options.canonical_name_buffer) catch + return error.InvalidDnsCnameRecord; }, else => continue, }; @@ -525,51 +518,50 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u return n; } -pub const ExpandDomainNameError = error{InvalidDnsPacket}; +pub const ExpandError = error{InvalidDnsPacket} || InitError; -pub fn expandDomainName( - msg: []const u8, - comp_dn: []const u8, - exp_dn: []u8, -) ExpandDomainNameError!usize { - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - var p = comp_dn.ptr; - var len: usize = std.math.maxInt(usize); - const end = msg.ptr + msg.len; - if (p == end or exp_dn.len == 0) return error.InvalidDnsPacket; - var dest = exp_dn.ptr; - const dend = dest + @min(exp_dn.len, 254); - // detect reference loop using an iteration counter - var i: usize = 0; - while (i < msg.len) : (i += 2) { - // loop invariants: p= msg.len) return error.InvalidDnsPacket; - p = msg.ptr + j; - } else if (p[0] != 0) { - if (dest != exp_dn.ptr) { - dest[0] = '.'; - dest += 1; - } - var j = p[0]; - p += 1; - if (j >= @intFromPtr(end) - @intFromPtr(p) or j >= @intFromPtr(dend) - @intFromPtr(dest)) { - return error.InvalidDnsPacket; - } - while (j != 0) { - j -= 1; - dest[0] = p[0]; - dest += 1; - p += 1; +/// Decompresses a DNS name. +/// +/// Returns number of bytes consumed from `packet` starting at `i`, +/// along with the expanded `HostName`. +/// +/// Asserts `buffer` is has length at least `max_len`. +pub fn expand(noalias packet: []const u8, start_i: usize, noalias dest_buffer: []u8) ExpandError!struct { usize, HostName } { + const dest = dest_buffer[0..max_len]; + + var i = start_i; + var dest_i: usize = 0; + var len: ?usize = null; + + // Detect reference loop using an iteration counter. + for (0..packet.len / 2) |_| { + if (i >= packet.len) return error.InvalidDnsPacket; + + const c = packet[i]; + if ((c & 0xc0) != 0) { + if (i + 1 >= packet.len) return error.InvalidDnsPacket; + const j: usize = (@as(usize, c & 0x3F) << 8) | packet[i + 1]; + if (j >= packet.len) return error.InvalidDnsPacket; + if (len == null) len = (i + 2) - start_i; + i = j; + } else if (c != 0) { + if (dest_i != 0) { + dest[dest_i] = '.'; + dest_i += 1; } + const label_len: usize = c; + if (i + 1 + label_len > packet.len) return error.InvalidDnsPacket; + if (dest_i + label_len + 1 > dest.len) return error.InvalidDnsPacket; + @memcpy(dest[dest_i..][0..label_len], packet[i + 1 ..][0..label_len]); + dest_i += label_len; + i += 1 + label_len; } else { - dest[0] = 0; - if (len == std.math.maxInt(usize)) len = @intFromPtr(p) + 1 - @intFromPtr(comp_dn.ptr); - return len; + dest[dest_i] = 0; + dest_i += 1; + return .{ + len orelse i - start_i + 1, + try .init(dest[0..dest_i]), + }; } } return error.InvalidDnsPacket; From 00f26cb0a4d60e908719309f4daa6598316ef74e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 2 Oct 2025 21:39:32 -0700 Subject: [PATCH 074/244] WIP land the std.Io interface fix std lib compilation errors caused by introducing std.Io --- lib/std/Build/WebServer.zig | 9 +- lib/std/Io/File.zig | 35 +- lib/std/Io/net.zig | 27 +- lib/std/{ => Io}/net/test.zig | 80 +- lib/std/Progress.zig | 4 +- lib/std/Thread.zig | 21 +- lib/std/fs.zig | 19 - lib/std/fs/File.zig | 589 +------- lib/std/net.zig | 2424 --------------------------------- lib/std/std.zig | 1 - 10 files changed, 111 insertions(+), 3098 deletions(-) rename lib/std/{ => Io}/net/test.zig (77%) delete mode 100644 lib/std/net.zig diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 3d6e700a5a..a67b96da03 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -2,12 +2,12 @@ gpa: Allocator, thread_pool: *std.Thread.Pool, graph: *const Build.Graph, all_steps: []const *Build.Step, -listen_address: std.net.Address, +listen_address: net.IpAddress, ttyconf: std.Io.tty.Config, root_prog_node: std.Progress.Node, watch: bool, -tcp_server: ?std.net.Server, +tcp_server: ?net.Server, serve_thread: ?std.Thread, base_timestamp: i128, @@ -56,7 +56,7 @@ pub const Options = struct { ttyconf: std.Io.tty.Config, root_prog_node: std.Progress.Node, watch: bool, - listen_address: std.net.Address, + listen_address: net.IpAddress, }; pub fn init(opts: Options) WebServer { // The upcoming `std.Io` interface should allow us to use `Io.async` and `Io.concurrent` @@ -244,7 +244,7 @@ pub fn now(s: *const WebServer) i64 { return @intCast(std.time.nanoTimestamp() - s.base_timestamp); } -fn accept(ws: *WebServer, connection: std.net.Server.Connection) void { +fn accept(ws: *WebServer, connection: net.Server.Connection) void { defer connection.stream.close(); var send_buffer: [4096]u8 = undefined; @@ -851,5 +851,6 @@ const Cache = Build.Cache; const Fuzz = Build.Fuzz; const abi = Build.abi; const http = std.http; +const net = std.Io.net; const WebServer = @This(); diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index abd74bdd98..a3be8f2c11 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -1,6 +1,8 @@ const File = @This(); const builtin = @import("builtin"); +const native_os = builtin.os.tag; +const is_windows = native_os == .windows; const std = @import("../std.zig"); const Io = std.Io; @@ -131,6 +133,18 @@ pub const Stat = struct { } }; +pub fn stdout() File { + return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdOutput else std.posix.STDOUT_FILENO }; +} + +pub fn stderr() File { + return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdError else std.posix.STDERR_FILENO }; +} + +pub fn stdin() File { + return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdInput else std.posix.STDIN_FILENO }; +} + pub const StatError = std.posix.FStatError || Io.Cancelable; /// Returns `Stat` containing basic information about the `File`. @@ -183,6 +197,11 @@ pub fn write(file: File, io: Io, buffer: []const u8) WriteError!usize { return @errorCast(file.pwrite(io, buffer, -1)); } +pub fn writeAll(file: File, io: Io, bytes: []const u8) WriteError!void { + var index: usize = 0; + while (index < bytes.len) index += try file.write(io, bytes[index..]); +} + pub const PWriteError = std.fs.File.PWriteError || Io.Cancelable; pub fn pwrite(file: File, io: Io, buffer: []const u8, offset: std.posix.off_t) PWriteError!usize { @@ -350,7 +369,7 @@ pub const Reader = struct { const io = r.io; switch (r.mode) { .positional, .positional_reading => { - setPosAdjustingBuffer(r, @intCast(@as(i64, @intCast(r.pos)) + offset)); + setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); }, .streaming, .streaming_reading => { if (std.posix.SEEK == void) { @@ -359,7 +378,7 @@ pub const Reader = struct { } const seek_err = r.seek_err orelse e: { if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| { - setPosAdjustingBuffer(r, @intCast(@as(i64, @intCast(r.pos)) + offset)); + setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); return; } else |err| { r.seek_err = err; @@ -384,16 +403,17 @@ pub const Reader = struct { const io = r.io; switch (r.mode) { .positional, .positional_reading => { - setPosAdjustingBuffer(r, offset); + setLogicalPos(r, offset); }, .streaming, .streaming_reading => { - if (offset >= r.pos) return Reader.seekBy(r, @intCast(offset - r.pos)); + const logical_pos = logicalPos(r); + if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos)); if (r.seek_err) |err| return err; io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| { r.seek_err = err; return err; }; - setPosAdjustingBuffer(r, offset); + setLogicalPos(r, offset); }, .failure => return r.seek_err.?, } @@ -403,7 +423,7 @@ pub const Reader = struct { return r.pos - r.interface.bufferedLen(); } - fn setPosAdjustingBuffer(r: *Reader, offset: u64) void { + fn setLogicalPos(r: *Reader, offset: u64) void { const logical_pos = logicalPos(r); if (offset < logical_pos or offset >= r.pos) { r.interface.seek = 0; @@ -544,9 +564,10 @@ pub const Reader = struct { } } + /// Returns whether the stream is at the logical end. pub fn atEnd(r: *Reader) bool { // Even if stat fails, size is set when end is encountered. const size = r.size orelse return false; - return size - r.pos == 0; + return size - logicalPos(r) == 0; } }; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 5672abd071..178aa75ca4 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -43,6 +43,14 @@ pub const Protocol = enum(u32) { mptcp = 262, }; +/// Windows 10 added support for unix sockets in build 17063, redstone 4 is the +/// first release to support them. +pub const has_unix_sockets = switch (native_os) { + .windows => builtin.os.version_range.windows.isAtLeast(.win10_rs4) orelse false, + .wasi => false, + else => true, +}; + pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, @@ -980,7 +988,13 @@ pub const Stream = struct { stream: Stream, err: ?Error, - pub const Error = std.net.Stream.ReadError || Io.Cancelable || Io.Writer.Error || error{EndOfStream}; + pub const Error = std.posix.ReadError || error{ + SocketNotBound, + MessageTooBig, + NetworkSubsystemFailed, + ConnectionResetByPeer, + SocketUnconnected, + } || Io.Cancelable || Io.Writer.Error || error{EndOfStream}; pub fn init(stream: Stream, buffer: []u8) Reader { return .{ @@ -1019,7 +1033,15 @@ pub const Stream = struct { stream: Stream, err: ?Error = null, - pub const Error = std.net.Stream.WriteError || Io.Cancelable; + pub const Error = std.posix.SendMsgError || error{ + ConnectionResetByPeer, + SocketNotBound, + MessageTooBig, + NetworkSubsystemFailed, + SystemResources, + SocketUnconnected, + Unexpected, + } || Io.Cancelable; pub fn init(stream: Stream, buffer: []u8) Writer { return .{ @@ -1096,4 +1118,5 @@ fn testIp6ParseTransform(expected: []const u8, input: []const u8) !void { test { _ = HostName; + _ = @import("net/test.zig"); } diff --git a/lib/std/net/test.zig b/lib/std/Io/net/test.zig similarity index 77% rename from lib/std/net/test.zig rename to lib/std/Io/net/test.zig index cf736591fc..3e561d2db3 100644 --- a/lib/std/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -1,38 +1,38 @@ const std = @import("std"); const builtin = @import("builtin"); -const net = std.net; +const net = std.Io.net; const mem = std.mem; const testing = std.testing; test "parse and render IP addresses at comptime" { comptime { - const ipv6addr = net.Address.parseIp("::1", 0) catch unreachable; + const ipv6addr = net.IpAddress.parseIp("::1", 0) catch unreachable; try std.testing.expectFmt("[::1]:0", "{f}", .{ipv6addr}); - const ipv4addr = net.Address.parseIp("127.0.0.1", 0) catch unreachable; + const ipv4addr = net.IpAddress.parseIp("127.0.0.1", 0) catch unreachable; try std.testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); - try testing.expectError(error.InvalidIpAddressFormat, net.Address.parseIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIpAddressFormat, net.Address.parseIp("127.01.0.1", 0)); - try testing.expectError(error.InvalidIpAddressFormat, net.Address.resolveIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIpAddressFormat, net.Address.resolveIp("127.01.0.1", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.parseIp("::123.123.123.123", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.parseIp("127.01.0.1", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.resolveIp("::123.123.123.123", 0)); + try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.resolveIp("127.01.0.1", 0)); } } test "format IPv6 address with no zero runs" { - const addr = try std.net.Address.parseIp6("2001:db8:1:2:3:4:5:6", 0); + const addr = try std.net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0); try std.testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr}); } test "parse IPv6 addresses and check compressed form" { try std.testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{ - try std.net.Address.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0), + try std.net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0), }); try std.testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{ - try std.net.Address.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0), + try std.net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0), }); try std.testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{ - try std.net.Address.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0), + try std.net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0), }); } @@ -44,7 +44,7 @@ test "parse IPv6 address, check raw bytes" { 0x00, 0x00, 0x00, 0x02, // :0000:0002 }; - const addr = try std.net.Address.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0); + const addr = try std.net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0); const actual_raw = addr.in6.sa.addr[0..]; try std.testing.expectEqualSlices(u8, expected_raw[0..], actual_raw); @@ -77,30 +77,30 @@ test "parse and render IPv6 addresses" { "::ffff:123.5.123.5", }; for (ips, 0..) |ip, i| { - const addr = net.Address.parseIp6(ip, 0) catch unreachable; + const addr = net.IpAddress.parseIp6(ip, 0) catch unreachable; var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; try std.testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3])); if (builtin.os.tag == .linux) { - const addr_via_resolve = net.Address.resolveIp6(ip, 0) catch unreachable; + const addr_via_resolve = net.IpAddress.resolveIp6(ip, 0) catch unreachable; var newResolvedIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr_via_resolve}) catch unreachable; try std.testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3])); } } - try testing.expectError(error.InvalidCharacter, net.Address.parseIp6(":::", 0)); - try testing.expectError(error.Overflow, net.Address.parseIp6("FF001::FB", 0)); - try testing.expectError(error.InvalidCharacter, net.Address.parseIp6("FF01::Fb:zig", 0)); - try testing.expectError(error.InvalidEnd, net.Address.parseIp6("FF01:0:0:0:0:0:0:FB:", 0)); - try testing.expectError(error.Incomplete, net.Address.parseIp6("FF01:", 0)); - try testing.expectError(error.InvalidIpv4Mapping, net.Address.parseIp6("::123.123.123.123", 0)); - try testing.expectError(error.Incomplete, net.Address.parseIp6("1", 0)); + try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6(":::", 0)); + try testing.expectError(error.Overflow, net.IpAddress.parseIp6("FF001::FB", 0)); + try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6("FF01::Fb:zig", 0)); + try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp6("FF01:0:0:0:0:0:0:FB:", 0)); + try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("FF01:", 0)); + try testing.expectError(error.InvalidIpv4Mapping, net.IpAddress.parseIp6("::123.123.123.123", 0)); + try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("1", 0)); // TODO Make this test pass on other operating systems. if (builtin.os.tag == .linux or comptime builtin.os.tag.isDarwin() or builtin.os.tag == .windows) { - try testing.expectError(error.Incomplete, net.Address.resolveIp6("ff01::fb%", 0)); + try testing.expectError(error.Incomplete, net.IpAddress.resolveIp6("ff01::fb%", 0)); // Assumes IFNAMESIZE will always be a multiple of 2 - try testing.expectError(error.Overflow, net.Address.resolveIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); - try testing.expectError(error.Overflow, net.Address.resolveIp6("ff01::fb%12345678901234", 0)); + try testing.expectError(error.Overflow, net.IpAddress.resolveIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); + try testing.expectError(error.Overflow, net.IpAddress.resolveIp6("ff01::fb%12345678901234", 0)); } } @@ -111,7 +111,7 @@ test "invalid but parseable IPv6 scope ids" { return error.SkipZigTest; } - try testing.expectError(error.InterfaceNotFound, net.Address.resolveIp6("ff01::fb%123s45678901234", 0)); + try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6("ff01::fb%123s45678901234", 0)); } test "parse and render IPv4 addresses" { @@ -123,17 +123,17 @@ test "parse and render IPv4 addresses" { "123.255.0.91", "127.0.0.1", }) |ip| { - const addr = net.Address.parseIp4(ip, 0) catch unreachable; + const addr = net.IpAddress.parseIp4(ip, 0) catch unreachable; var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; try std.testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2])); } - try testing.expectError(error.Overflow, net.Address.parseIp4("256.0.0.1", 0)); - try testing.expectError(error.InvalidCharacter, net.Address.parseIp4("x.0.0.1", 0)); - try testing.expectError(error.InvalidEnd, net.Address.parseIp4("127.0.0.1.1", 0)); - try testing.expectError(error.Incomplete, net.Address.parseIp4("127.0.0.", 0)); - try testing.expectError(error.InvalidCharacter, net.Address.parseIp4("100..0.1", 0)); - try testing.expectError(error.NonCanonical, net.Address.parseIp4("127.01.0.1", 0)); + try testing.expectError(error.Overflow, net.IpAddress.parseIp4("256.0.0.1", 0)); + try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("x.0.0.1", 0)); + try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp4("127.0.0.1.1", 0)); + try testing.expectError(error.Incomplete, net.IpAddress.parseIp4("127.0.0.", 0)); + try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp4("100..0.1", 0)); + try testing.expectError(error.NonCanonical, net.IpAddress.parseIp4("127.01.0.1", 0)); } test "parse and render UNIX addresses" { @@ -161,8 +161,8 @@ test "resolve DNS" { // Resolve localhost, this should not fail. { - const localhost_v4 = try net.Address.parseIp("127.0.0.1", 80); - const localhost_v6 = try net.Address.parseIp("::2", 80); + const localhost_v4 = try net.IpAddress.parseIp("127.0.0.1", 80); + const localhost_v6 = try net.IpAddress.parseIp("::2", 80); const result = try net.getAddressList(testing.allocator, "localhost", 80); defer result.deinit(); @@ -198,13 +198,13 @@ test "listen on a port, send bytes, receive bytes" { // Try only the IPv4 variant as some CI builders have no IPv6 localhost // configured. - const localhost = try net.Address.parseIp("127.0.0.1", 0); + const localhost = try net.IpAddress.parseIp("127.0.0.1", 0); var server = try localhost.listen(.{}); defer server.deinit(); const S = struct { - fn clientFn(server_address: net.Address) !void { + fn clientFn(server_address: net.IpAddress) !void { const socket = try net.tcpConnectToAddress(server_address); defer socket.close(); @@ -232,7 +232,7 @@ test "listen on an in use port" { return error.SkipZigTest; } - const localhost = try net.Address.parseIp("127.0.0.1", 0); + const localhost = try net.IpAddress.parseIp("127.0.0.1", 0); var server1 = try localhost.listen(.{ .reuse_address = true }); defer server1.deinit(); @@ -253,7 +253,7 @@ fn testClientToHost(allocator: mem.Allocator, name: []const u8, port: u16) anyer try testing.expect(mem.eql(u8, msg, "hello from server\n")); } -fn testClient(addr: net.Address) anyerror!void { +fn testClient(addr: net.IpAddress) anyerror!void { if (builtin.os.tag == .wasi) return error.SkipZigTest; const socket_file = try net.tcpConnectToAddress(addr); @@ -290,7 +290,7 @@ test "listen on a unix socket, send bytes, receive bytes" { const socket_path = try generateFileName("socket.unix"); defer testing.allocator.free(socket_path); - const socket_addr = try net.Address.initUnix(socket_path); + const socket_addr = try net.IpAddress.initUnix(socket_path); defer std.fs.cwd().deleteFile(socket_path) catch {}; var server = try socket_addr.listen(.{}); @@ -351,7 +351,7 @@ test "non-blocking tcp server" { return error.SkipZigTest; } - const localhost = try net.Address.parseIp("127.0.0.1", 0); + const localhost = try net.IpAddress.parseIp("127.0.0.1", 0); var server = localhost.listen(.{ .force_nonblocking = true }); defer server.deinit(); diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 102b4d8404..d9ec89b893 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -523,9 +523,7 @@ pub fn setStatus(new_status: Status) void { /// Returns whether a resize is needed to learn the terminal size. fn wait(timeout_ns: u64) bool { - const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_| - true - else |err| switch (err) { + const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_| true else |err| switch (err) { error.Timeout => false, }; global_progress.redraw_event.reset(); diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 46698cbe99..6da58e17bc 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -71,7 +71,7 @@ pub const ResetEvent = enum(u32) { /// /// The memory accesses before the set() can be said to happen before /// timedWait() returns without error. - pub fn timedWait(re: *ResetEvent, timeout_ns: u64) void { + pub fn timedWait(re: *ResetEvent, timeout_ns: u64) error{Timeout}!void { if (builtin.single_threaded) switch (re.*) { .unset => { sleep(timeout_ns); @@ -1774,9 +1774,9 @@ test "setName, getName" { if (builtin.single_threaded) return error.SkipZigTest; const Context = struct { - start_wait_event: ResetEvent = .{}, - test_done_event: ResetEvent = .{}, - thread_done_event: ResetEvent = .{}, + start_wait_event: ResetEvent = .unset, + test_done_event: ResetEvent = .unset, + thread_done_event: ResetEvent = .unset, done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), thread: Thread = undefined, @@ -1843,7 +1843,7 @@ test join { if (builtin.single_threaded) return error.SkipZigTest; var value: usize = 0; - var event = ResetEvent{}; + var event: ResetEvent = .unset; const thread = try Thread.spawn(.{}, testIncrementNotify, .{ &value, &event }); thread.join(); @@ -1855,7 +1855,7 @@ test detach { if (builtin.single_threaded) return error.SkipZigTest; var value: usize = 0; - var event = ResetEvent{}; + var event: ResetEvent = .unset; const thread = try Thread.spawn(.{}, testIncrementNotify, .{ &value, &event }); thread.detach(); @@ -1902,8 +1902,7 @@ fn testTls() !void { } test "ResetEvent smoke test" { - // make sure the event is unset - var event = ResetEvent{}; + var event: ResetEvent = .unset; try testing.expectEqual(false, event.isSet()); // make sure the event gets set @@ -1932,8 +1931,8 @@ test "ResetEvent signaling" { } const Context = struct { - in: ResetEvent = .{}, - out: ResetEvent = .{}, + in: ResetEvent = .unset, + out: ResetEvent = .unset, value: usize = 0, fn input(self: *@This()) !void { @@ -1994,7 +1993,7 @@ test "ResetEvent broadcast" { const num_threads = 10; const Barrier = struct { - event: ResetEvent = .{}, + event: ResetEvent = .unset, counter: std.atomic.Value(usize) = std.atomic.Value(usize).init(num_threads), fn wait(self: *@This()) void { diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 0470ee4e2a..7d5ac75276 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -97,25 +97,6 @@ pub const base64_encoder = base64.Base64Encoder.init(base64_alphabet, null); /// Base64 decoder, replacing the standard `+/` with `-_` so that it can be used in a file name on any filesystem. pub const base64_decoder = base64.Base64Decoder.init(base64_alphabet, null); -/// Same as `Dir.updateFile`, except asserts that both `source_path` and `dest_path` -/// are absolute. See `Dir.updateFile` for a function that operates on both -/// absolute and relative paths. -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. -pub fn updateFileAbsolute( - source_path: []const u8, - dest_path: []const u8, - args: Dir.CopyFileOptions, -) !std.Io.Dir.PrevStatus { - assert(path.isAbsolute(source_path)); - assert(path.isAbsolute(dest_path)); - const my_cwd = cwd(); - return Dir.updateFile(my_cwd, source_path, my_cwd, dest_path, args); -} - -test updateFileAbsolute {} - /// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` /// are absolute. See `Dir.copyFile` for a function that operates on both /// absolute and relative paths. diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index b79ef44d3b..7c94fc1df6 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -698,17 +698,6 @@ pub fn read(self: File, buffer: []u8) ReadError!usize { return posix.read(self.handle, buffer); } -/// Deprecated in favor of `Reader`. -pub fn readAll(self: File, buffer: []u8) ReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.read(buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; -} - /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { @@ -719,17 +708,6 @@ pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { return posix.pread(self.handle, buffer, offset); } -/// Deprecated in favor of `Reader`. -pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { - var index: usize = 0; - while (index != buffer.len) { - const amt = try self.pread(buffer[index..], offset + index); - if (amt == 0) break; - index += amt; - } - return index; -} - /// See https://github.com/ziglang/zig/issues/7699 pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { if (is_windows) { @@ -741,36 +719,6 @@ pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { return posix.readv(self.handle, iovecs); } -/// Deprecated in favor of `Reader`. -pub fn readvAll(self: File, iovecs: []posix.iovec) ReadError!usize { - if (iovecs.len == 0) return 0; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.len == 0) v.base = &garbage; - } - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.readv(iovecs[i..]); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - /// See https://github.com/ziglang/zig/issues/7699 /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 @@ -784,28 +732,6 @@ pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!u return posix.preadv(self.handle, iovecs, offset); } -/// Deprecated in favor of `Reader`. -pub fn preadvAll(self: File, iovecs: []posix.iovec, offset: u64) PReadError!usize { - if (iovecs.len == 0) return 0; - - var i: usize = 0; - var off: usize = 0; - while (true) { - var amt = try self.preadv(iovecs[i..], offset + off); - var eof = amt == 0; - off += amt; - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return off; - eof = false; - } - if (eof) return off; - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - pub const WriteError = posix.WriteError; pub const PWriteError = posix.PWriteError; @@ -817,7 +743,6 @@ pub fn write(self: File, bytes: []const u8) WriteError!usize { return posix.write(self.handle, bytes); } -/// Deprecated in favor of `Writer`. pub fn writeAll(self: File, bytes: []const u8) WriteError!void { var index: usize = 0; while (index < bytes.len) { @@ -835,14 +760,6 @@ pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { return posix.pwrite(self.handle, bytes, offset); } -/// Deprecated in favor of `Writer`. -pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.pwrite(bytes[index..], offset + index); - } -} - /// See https://github.com/ziglang/zig/issues/7699 pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { if (is_windows) { @@ -855,31 +772,6 @@ pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize { return posix.writev(self.handle, iovecs); } -/// Deprecated in favor of `Writer`. -pub fn writevAll(self: File, iovecs: []posix.iovec_const) WriteError!void { - if (iovecs.len == 0) return; - - // We use the address of this local variable for all zero-length - // vectors so that the OS does not complain that we are giving it - // addresses outside the application's address space. - var garbage: [1]u8 = undefined; - for (iovecs) |*v| { - if (v.len == 0) v.base = &garbage; - } - - var i: usize = 0; - while (true) { - var amt = try self.writev(iovecs[i..]); - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - /// See https://github.com/ziglang/zig/issues/7699 /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 @@ -893,485 +785,8 @@ pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError return posix.pwritev(self.handle, iovecs, offset); } -/// Deprecated in favor of `Writer`. -pub fn pwritevAll(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!void { - if (iovecs.len == 0) return; - var i: usize = 0; - var off: u64 = 0; - while (true) { - var amt = try self.pwritev(iovecs[i..], offset + off); - off += amt; - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].base += amt; - iovecs[i].len -= amt; - } -} - -pub const CopyRangeError = posix.CopyFileRangeError; - -/// Deprecated in favor of `Writer`. -pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { - const adjusted_len = math.cast(usize, len) orelse maxInt(usize); - const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); - return result; -} - -/// Deprecated in favor of `Writer`. -pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { - var total_bytes_copied: u64 = 0; - var in_off = in_offset; - var out_off = out_offset; - while (total_bytes_copied < len) { - const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); - if (amt_copied == 0) return total_bytes_copied; - total_bytes_copied += amt_copied; - in_off += amt_copied; - out_off += amt_copied; - } - return total_bytes_copied; -} - -/// Memoizes key information about a file handle such as: -/// * The size from calling stat, or the error that occurred therein. -/// * The current seek position. -/// * The error that occurred when trying to seek. -/// * Whether reading should be done positionally or streaming. -/// * Whether reading should be done via fd-to-fd syscalls (e.g. `sendfile`) -/// versus plain variants (e.g. `read`). -/// -/// Fulfills the `std.Io.Reader` interface. -pub const Reader = struct { - file: File, - err: ?ReadError = null, - mode: Reader.Mode = .positional, - /// Tracks the true seek position in the file. To obtain the logical - /// position, use `logicalPos`. - pos: u64 = 0, - size: ?u64 = null, - size_err: ?SizeError = null, - seek_err: ?Reader.SeekError = null, - interface: std.Io.Reader, - - pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{ - /// Occurs if, for example, the file handle is a network socket and therefore does not have a size. - Streaming, - }; - - pub const SeekError = File.SeekError || error{ - /// Seeking fell back to reading, and reached the end before the requested seek position. - /// `pos` remains at the end of the file. - EndOfStream, - /// Seeking fell back to reading, which failed. - ReadFailed, - }; - - pub const Mode = enum { - streaming, - positional, - /// Avoid syscalls other than `read` and `readv`. - streaming_reading, - /// Avoid syscalls other than `pread` and `preadv`. - positional_reading, - /// Indicates reading cannot continue because of a seek failure. - failure, - - pub fn toStreaming(m: @This()) @This() { - return switch (m) { - .positional, .streaming => .streaming, - .positional_reading, .streaming_reading => .streaming_reading, - .failure => .failure, - }; - } - - pub fn toReading(m: @This()) @This() { - return switch (m) { - .positional, .positional_reading => .positional_reading, - .streaming, .streaming_reading => .streaming_reading, - .failure => .failure, - }; - } - }; - - pub fn initInterface(buffer: []u8) std.Io.Reader { - return .{ - .vtable = &.{ - .stream = Reader.stream, - .discard = Reader.discard, - .readVec = Reader.readVec, - }, - .buffer = buffer, - .seek = 0, - .end = 0, - }; - } - - pub fn init(file: File, buffer: []u8) Reader { - return .{ - .file = file, - .interface = initInterface(buffer), - }; - } - - pub fn initSize(file: File, buffer: []u8, size: ?u64) Reader { - return .{ - .file = file, - .interface = initInterface(buffer), - .size = size, - }; - } - - /// Positional is more threadsafe, since the global seek position is not - /// affected, but when such syscalls are not available, preemptively - /// initializing in streaming mode skips a failed syscall. - pub fn initStreaming(file: File, buffer: []u8) Reader { - return .{ - .file = file, - .interface = Reader.initInterface(buffer), - .mode = .streaming, - .seek_err = error.Unseekable, - .size_err = error.Streaming, - }; - } - - pub fn getSize(r: *Reader) SizeError!u64 { - return r.size orelse { - if (r.size_err) |err| return err; - if (is_windows) { - if (windows.GetFileSizeEx(r.file.handle)) |size| { - r.size = size; - return size; - } else |err| { - r.size_err = err; - return err; - } - } - if (posix.Stat == void) { - r.size_err = error.Streaming; - return error.Streaming; - } - if (stat(r.file)) |st| { - if (st.kind == .file) { - r.size = st.size; - return st.size; - } else { - r.mode = r.mode.toStreaming(); - r.size_err = error.Streaming; - return error.Streaming; - } - } else |err| { - r.size_err = err; - return err; - } - }; - } - - pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void { - switch (r.mode) { - .positional, .positional_reading => { - setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); - }, - .streaming, .streaming_reading => { - if (posix.SEEK == void) { - r.seek_err = error.Unseekable; - return error.Unseekable; - } - const seek_err = r.seek_err orelse e: { - if (posix.lseek_CUR(r.file.handle, offset)) |_| { - setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); - return; - } else |err| { - r.seek_err = err; - break :e err; - } - }; - var remaining = std.math.cast(u64, offset) orelse return seek_err; - while (remaining > 0) { - remaining -= discard(&r.interface, .limited64(remaining)) catch |err| { - r.seek_err = err; - return err; - }; - } - r.interface.seek = 0; - r.interface.end = 0; - }, - .failure => return r.seek_err.?, - } - } - - pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void { - switch (r.mode) { - .positional, .positional_reading => { - setLogicalPos(r, offset); - }, - .streaming, .streaming_reading => { - const logical_pos = logicalPos(r); - if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos)); - if (r.seek_err) |err| return err; - posix.lseek_SET(r.file.handle, offset) catch |err| { - r.seek_err = err; - return err; - }; - setLogicalPos(r, offset); - }, - .failure => return r.seek_err.?, - } - } - - pub fn logicalPos(r: *const Reader) u64 { - return r.pos - r.interface.bufferedLen(); - } - - fn setLogicalPos(r: *Reader, offset: u64) void { - const logical_pos = logicalPos(r); - if (offset < logical_pos or offset >= r.pos) { - r.interface.seek = 0; - r.interface.end = 0; - r.pos = offset; - } else { - const logical_delta: usize = @intCast(offset - logical_pos); - r.interface.seek += logical_delta; - } - } - - /// Number of slices to store on the stack, when trying to send as many byte - /// vectors through the underlying read calls as possible. - const max_buffers_len = 16; - - fn stream(io_reader: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - switch (r.mode) { - .positional, .streaming => @panic("TODO"), - .positional_reading => { - const dest = limit.slice(try w.writableSliceGreedy(1)); - var data: [1][]u8 = .{dest}; - const n = try readVecPositional(r, &data); - w.advance(n); - return n; - }, - .streaming_reading => { - const dest = limit.slice(try w.writableSliceGreedy(1)); - var data: [1][]u8 = .{dest}; - const n = try readVecStreaming(r, &data); - w.advance(n); - return n; - }, - .failure => return error.ReadFailed, - } - } - - fn readVec(io_reader: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - switch (r.mode) { - .positional, .positional_reading => return readVecPositional(r, data), - .streaming, .streaming_reading => return readVecStreaming(r, data), - .failure => return error.ReadFailed, - } - } - - fn readVecPositional(r: *Reader, data: [][]u8) std.Io.Reader.Error!usize { - const io_reader = &r.interface; - if (is_windows) { - // Unfortunately, `ReadFileScatter` cannot be used since it - // requires page alignment. - if (io_reader.seek == io_reader.end) { - io_reader.seek = 0; - io_reader.end = 0; - } - const first = data[0]; - if (first.len >= io_reader.buffer.len - io_reader.end) { - return readPositional(r, first); - } else { - io_reader.end += try readPositional(r, io_reader.buffer[io_reader.end..]); - return 0; - } - } - var iovecs_buffer: [max_buffers_len]posix.iovec = undefined; - const dest_n, const data_size = try io_reader.writableVectorPosix(&iovecs_buffer, data); - const dest = iovecs_buffer[0..dest_n]; - assert(dest[0].len > 0); - const n = posix.preadv(r.file.handle, dest, r.pos) catch |err| switch (err) { - error.Unseekable => { - r.mode = r.mode.toStreaming(); - const pos = r.pos; - if (pos != 0) { - r.pos = 0; - r.seekBy(@intCast(pos)) catch { - r.mode = .failure; - return error.ReadFailed; - }; - } - return 0; - }, - else => |e| { - r.err = e; - return error.ReadFailed; - }, - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - if (n > data_size) { - io_reader.end += n - data_size; - return data_size; - } - return n; - } - - fn readVecStreaming(r: *Reader, data: [][]u8) std.Io.Reader.Error!usize { - const io_reader = &r.interface; - if (is_windows) { - // Unfortunately, `ReadFileScatter` cannot be used since it - // requires page alignment. - if (io_reader.seek == io_reader.end) { - io_reader.seek = 0; - io_reader.end = 0; - } - const first = data[0]; - if (first.len >= io_reader.buffer.len - io_reader.end) { - return readStreaming(r, first); - } else { - io_reader.end += try readStreaming(r, io_reader.buffer[io_reader.end..]); - return 0; - } - } - var iovecs_buffer: [max_buffers_len]posix.iovec = undefined; - const dest_n, const data_size = try io_reader.writableVectorPosix(&iovecs_buffer, data); - const dest = iovecs_buffer[0..dest_n]; - assert(dest[0].len > 0); - const n = posix.readv(r.file.handle, dest) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - if (n > data_size) { - io_reader.end += n - data_size; - return data_size; - } - return n; - } - - fn discard(io_reader: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - const file = r.file; - const pos = r.pos; - switch (r.mode) { - .positional, .positional_reading => { - const size = r.getSize() catch { - r.mode = r.mode.toStreaming(); - return 0; - }; - const delta = @min(@intFromEnum(limit), size - pos); - r.pos = pos + delta; - return delta; - }, - .streaming, .streaming_reading => { - // Unfortunately we can't seek forward without knowing the - // size because the seek syscalls provided to us will not - // return the true end position if a seek would exceed the - // end. - fallback: { - if (r.size_err == null and r.seek_err == null) break :fallback; - var trash_buffer: [128]u8 = undefined; - if (is_windows) { - const n = windows.ReadFile(file.handle, limit.slice(&trash_buffer), null) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = pos; - return error.EndOfStream; - } - r.pos = pos + n; - return n; - } - var iovecs: [max_buffers_len]std.posix.iovec = undefined; - var iovecs_i: usize = 0; - var remaining = @intFromEnum(limit); - while (remaining > 0 and iovecs_i < iovecs.len) { - iovecs[iovecs_i] = .{ .base = &trash_buffer, .len = @min(trash_buffer.len, remaining) }; - remaining -= iovecs[iovecs_i].len; - iovecs_i += 1; - } - const n = posix.readv(file.handle, iovecs[0..iovecs_i]) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = pos; - return error.EndOfStream; - } - r.pos = pos + n; - return n; - } - const size = r.getSize() catch return 0; - const n = @min(size - pos, maxInt(i64), @intFromEnum(limit)); - file.seekBy(n) catch |err| { - r.seek_err = err; - return 0; - }; - r.pos = pos + n; - return n; - }, - .failure => return error.ReadFailed, - } - } - - fn readPositional(r: *Reader, dest: []u8) std.Io.Reader.Error!usize { - const n = r.file.pread(dest, r.pos) catch |err| switch (err) { - error.Unseekable => { - r.mode = r.mode.toStreaming(); - const pos = r.pos; - if (pos != 0) { - r.pos = 0; - r.seekBy(@intCast(pos)) catch { - r.mode = .failure; - return error.ReadFailed; - }; - } - return 0; - }, - else => |e| { - r.err = e; - return error.ReadFailed; - }, - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - return n; - } - - fn readStreaming(r: *Reader, dest: []u8) std.Io.Reader.Error!usize { - const n = r.file.read(dest) catch |err| { - r.err = err; - return error.ReadFailed; - }; - if (n == 0) { - r.size = r.pos; - return error.EndOfStream; - } - r.pos += n; - return n; - } - - pub fn atEnd(r: *Reader) bool { - // Even if stat fails, size is set when end is encountered. - const size = r.size orelse return false; - return size - r.pos == 0; - } -}; +/// Deprecated in favor of `std.Io.File.Reader`. +pub const Reader = std.Io.File.Reader; pub const Writer = struct { file: File, diff --git a/lib/std/net.zig b/lib/std/net.zig deleted file mode 100644 index 7863967e63..0000000000 --- a/lib/std/net.zig +++ /dev/null @@ -1,2424 +0,0 @@ -//! Cross-platform networking abstractions. - -const std = @import("std.zig"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const net = @This(); -const mem = std.mem; -const posix = std.posix; -const fs = std.fs; -const Io = std.Io; -const native_endian = builtin.target.cpu.arch.endian(); -const native_os = builtin.os.tag; -const windows = std.os.windows; -const Allocator = std.mem.Allocator; -const ArrayList = std.ArrayListUnmanaged; -const File = std.fs.File; - -// Windows 10 added support for unix sockets in build 17063, redstone 4 is the -// first release to support them. -pub const has_unix_sockets = switch (native_os) { - .windows => builtin.os.version_range.windows.isAtLeast(.win10_rs4) orelse false, - .wasi => false, - else => true, -}; - -pub const IPParseError = error{ - Overflow, - InvalidEnd, - InvalidCharacter, - Incomplete, -}; - -pub const IPv4ParseError = IPParseError || error{NonCanonical}; - -pub const IPv6ParseError = IPParseError || error{InvalidIpv4Mapping}; -pub const IPv6InterfaceError = posix.SocketError || posix.IoCtl_SIOCGIFINDEX_Error || error{NameTooLong}; -pub const IPv6ResolveError = IPv6ParseError || IPv6InterfaceError; - -pub const Address = extern union { - any: posix.sockaddr, - in: Ip4Address, - in6: Ip6Address, - un: if (has_unix_sockets) posix.sockaddr.un else void, - - /// Parse an IP address which may include a port. For IPv4, this is just written `address:port`. - /// For IPv6, RFC 3986 defines this as an "IP literal", and the port is differentiated from the - /// address by surrounding the address part in brackets '[addr]:port'. Even if the port is not - /// given, the brackets are mandatory. - pub fn parseIpAndPort(str: []const u8) error{ InvalidAddress, InvalidPort }!Address { - if (str.len == 0) return error.InvalidAddress; - if (str[0] == '[') { - const addr_end = std.mem.indexOfScalar(u8, str, ']') orelse - return error.InvalidAddress; - const addr_str = str[1..addr_end]; - const port: u16 = p: { - if (addr_end == str.len - 1) break :p 0; - if (str[addr_end + 1] != ':') return error.InvalidAddress; - break :p parsePort(str[addr_end + 2 ..]) orelse return error.InvalidPort; - }; - return parseIp6(addr_str, port) catch error.InvalidAddress; - } else { - if (std.mem.indexOfScalar(u8, str, ':')) |idx| { - // hold off on `error.InvalidPort` since `error.InvalidAddress` might make more sense - const port: ?u16 = parsePort(str[idx + 1 ..]); - const addr = parseIp4(str[0..idx], port orelse 0) catch return error.InvalidAddress; - if (port == null) return error.InvalidPort; - return addr; - } else { - return parseIp4(str, 0) catch error.InvalidAddress; - } - } - } - fn parsePort(str: []const u8) ?u16 { - var p: u16 = 0; - for (str) |c| switch (c) { - '0'...'9' => { - const shifted = std.math.mul(u16, p, 10) catch return null; - p = std.math.add(u16, shifted, c - '0') catch return null; - }, - else => return null, - }; - if (p == 0) return null; - return p; - } - - /// Parse the given IP address string into an Address value. - /// It is recommended to use `resolveIp` instead, to handle - /// IPv6 link-local unix addresses. - pub fn parseIp(name: []const u8, port: u16) !Address { - if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.NonCanonical, - => {}, - } - - if (parseIp6(name, port)) |ip6| return ip6 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.InvalidIpv4Mapping, - => {}, - } - - return error.InvalidIpAddressFormat; - } - - pub fn resolveIp(name: []const u8, port: u16) !Address { - if (parseIp4(name, port)) |ip4| return ip4 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.NonCanonical, - => {}, - } - - if (resolveIp6(name, port)) |ip6| return ip6 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.InvalidIpv4Mapping, - => {}, - else => return err, - } - - return error.InvalidIpAddressFormat; - } - - pub fn parseExpectingFamily(name: []const u8, family: posix.sa_family_t, port: u16) !Address { - switch (family) { - posix.AF.INET => return parseIp4(name, port), - posix.AF.INET6 => return parseIp6(name, port), - posix.AF.UNSPEC => return parseIp(name, port), - else => unreachable, - } - } - - pub fn parseIp6(buf: []const u8, port: u16) IPv6ParseError!Address { - return .{ .in6 = try Ip6Address.parse(buf, port) }; - } - - pub fn resolveIp6(buf: []const u8, port: u16) IPv6ResolveError!Address { - return .{ .in6 = try Ip6Address.resolve(buf, port) }; - } - - pub fn parseIp4(buf: []const u8, port: u16) IPv4ParseError!Address { - return .{ .in = try Ip4Address.parse(buf, port) }; - } - - pub fn initIp4(addr: [4]u8, port: u16) Address { - return .{ .in = Ip4Address.init(addr, port) }; - } - - pub fn initIp6(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Address { - return .{ .in6 = Ip6Address.init(addr, port, flowinfo, scope_id) }; - } - - pub fn initUnix(path: []const u8) !Address { - var sock_addr = posix.sockaddr.un{ - .family = posix.AF.UNIX, - .path = undefined, - }; - - // Add 1 to ensure a terminating 0 is present in the path array for maximum portability. - if (path.len + 1 > sock_addr.path.len) return error.NameTooLong; - - @memset(&sock_addr.path, 0); - @memcpy(sock_addr.path[0..path.len], path); - - return .{ .un = sock_addr }; - } - - /// Returns the port in native endian. - /// Asserts that the address is ip4 or ip6. - pub fn getPort(self: Address) u16 { - return switch (self.any.family) { - posix.AF.INET => self.in.getPort(), - posix.AF.INET6 => self.in6.getPort(), - else => unreachable, - }; - } - - /// `port` is native-endian. - /// Asserts that the address is ip4 or ip6. - pub fn setPort(self: *Address, port: u16) void { - switch (self.any.family) { - posix.AF.INET => self.in.setPort(port), - posix.AF.INET6 => self.in6.setPort(port), - else => unreachable, - } - } - - /// Asserts that `addr` is an IP address. - /// This function will read past the end of the pointer, with a size depending - /// on the address family. - pub fn initPosix(addr: *align(4) const posix.sockaddr) Address { - switch (addr.family) { - posix.AF.INET => return Address{ .in = Ip4Address{ .sa = @as(*const posix.sockaddr.in, @ptrCast(addr)).* } }, - posix.AF.INET6 => return Address{ .in6 = Ip6Address{ .sa = @as(*const posix.sockaddr.in6, @ptrCast(addr)).* } }, - else => unreachable, - } - } - - pub fn format(self: Address, w: *Io.Writer) Io.Writer.Error!void { - switch (self.any.family) { - posix.AF.INET => try self.in.format(w), - posix.AF.INET6 => try self.in6.format(w), - posix.AF.UNIX => { - if (!has_unix_sockets) unreachable; - try w.writeAll(std.mem.sliceTo(&self.un.path, 0)); - }, - else => unreachable, - } - } - - pub fn eql(a: Address, b: Address) bool { - const a_bytes = @as([*]const u8, @ptrCast(&a.any))[0..a.getOsSockLen()]; - const b_bytes = @as([*]const u8, @ptrCast(&b.any))[0..b.getOsSockLen()]; - return mem.eql(u8, a_bytes, b_bytes); - } - - pub fn getOsSockLen(self: Address) posix.socklen_t { - switch (self.any.family) { - posix.AF.INET => return self.in.getOsSockLen(), - posix.AF.INET6 => return self.in6.getOsSockLen(), - posix.AF.UNIX => { - if (!has_unix_sockets) { - unreachable; - } - - // Using the full length of the structure here is more portable than returning - // the number of bytes actually used by the currently stored path. - // This also is correct regardless if we are passing a socket address to the kernel - // (e.g. in bind, connect, sendto) since we ensure the path is 0 terminated in - // initUnix() or if we are receiving a socket address from the kernel and must - // provide the full buffer size (e.g. getsockname, getpeername, recvfrom, accept). - // - // To access the path, std.mem.sliceTo(&address.un.path, 0) should be used. - return @as(posix.socklen_t, @intCast(@sizeOf(posix.sockaddr.un))); - }, - - else => unreachable, - } - } - - pub const ListenError = posix.SocketError || posix.BindError || posix.ListenError || - posix.SetSockOptError || posix.GetSockNameError; - - pub const ListenOptions = struct { - /// How many connections the kernel will accept on the application's behalf. - /// If more than this many connections pool in the kernel, clients will start - /// seeing "Connection refused". - kernel_backlog: u31 = 128, - /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. - /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. - reuse_address: bool = false, - /// Sets O_NONBLOCK. - force_nonblocking: bool = false, - }; - - /// The returned `Server` has an open `stream`. - pub fn listen(address: Address, options: ListenOptions) ListenError!Server { - const nonblock: u32 = if (options.force_nonblocking) posix.SOCK.NONBLOCK else 0; - const sock_flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | nonblock; - const proto: u32 = if (address.any.family == posix.AF.UNIX) 0 else posix.IPPROTO.TCP; - - const sockfd = try posix.socket(address.any.family, sock_flags, proto); - var s: Server = .{ - .listen_address = undefined, - .stream = .{ .handle = sockfd }, - }; - errdefer s.stream.close(); - - if (options.reuse_address) { - try posix.setsockopt( - sockfd, - posix.SOL.SOCKET, - posix.SO.REUSEADDR, - &mem.toBytes(@as(c_int, 1)), - ); - if (@hasDecl(posix.SO, "REUSEPORT") and address.any.family != posix.AF.UNIX) { - try posix.setsockopt( - sockfd, - posix.SOL.SOCKET, - posix.SO.REUSEPORT, - &mem.toBytes(@as(c_int, 1)), - ); - } - } - - var socklen = address.getOsSockLen(); - try posix.bind(sockfd, &address.any, socklen); - try posix.listen(sockfd, options.kernel_backlog); - try posix.getsockname(sockfd, &s.listen_address.any, &socklen); - return s; - } -}; - -pub const Ip4Address = extern struct { - sa: posix.sockaddr.in, - - pub fn parse(buf: []const u8, port: u16) IPv4ParseError!Ip4Address { - var result: Ip4Address = .{ - .sa = .{ - .port = mem.nativeToBig(u16, port), - .addr = undefined, - }, - }; - const out_ptr = mem.asBytes(&result.sa.addr); - - var x: u8 = 0; - var index: u8 = 0; - var saw_any_digits = false; - var has_zero_prefix = false; - for (buf) |c| { - if (c == '.') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - if (index == 3) { - return error.InvalidEnd; - } - out_ptr[index] = x; - index += 1; - x = 0; - saw_any_digits = false; - has_zero_prefix = false; - } else if (c >= '0' and c <= '9') { - if (c == '0' and !saw_any_digits) { - has_zero_prefix = true; - } else if (has_zero_prefix) { - return error.NonCanonical; - } - saw_any_digits = true; - x = try std.math.mul(u8, x, 10); - x = try std.math.add(u8, x, c - '0'); - } else { - return error.InvalidCharacter; - } - } - if (index == 3 and saw_any_digits) { - out_ptr[index] = x; - return result; - } - - return error.Incomplete; - } - - pub fn resolveIp(name: []const u8, port: u16) !Ip4Address { - if (parse(name, port)) |ip4| return ip4 else |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.NonCanonical, - => {}, - } - return error.InvalidIpAddressFormat; - } - - pub fn init(addr: [4]u8, port: u16) Ip4Address { - return Ip4Address{ - .sa = posix.sockaddr.in{ - .port = mem.nativeToBig(u16, port), - .addr = @as(*align(1) const u32, @ptrCast(&addr)).*, - }, - }; - } - - /// Returns the port in native endian. - /// Asserts that the address is ip4 or ip6. - pub fn getPort(self: Ip4Address) u16 { - return mem.bigToNative(u16, self.sa.port); - } - - /// `port` is native-endian. - /// Asserts that the address is ip4 or ip6. - pub fn setPort(self: *Ip4Address, port: u16) void { - self.sa.port = mem.nativeToBig(u16, port); - } - - pub fn format(self: Ip4Address, w: *Io.Writer) Io.Writer.Error!void { - const bytes: *const [4]u8 = @ptrCast(&self.sa.addr); - try w.print("{d}.{d}.{d}.{d}:{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3], self.getPort() }); - } - - pub fn getOsSockLen(self: Ip4Address) posix.socklen_t { - _ = self; - return @sizeOf(posix.sockaddr.in); - } -}; - -pub const Ip6Address = extern struct { - sa: posix.sockaddr.in6, - - /// Parse a given IPv6 address string into an Address. - /// Assumes the Scope ID of the address is fully numeric. - /// For non-numeric addresses, see `resolveIp6`. - pub fn parse(buf: []const u8, port: u16) IPv6ParseError!Ip6Address { - var result = Ip6Address{ - .sa = posix.sockaddr.in6{ - .scope_id = 0, - .port = mem.nativeToBig(u16, port), - .flowinfo = 0, - .addr = undefined, - }, - }; - var ip_slice: *[16]u8 = result.sa.addr[0..]; - - var tail: [16]u8 = undefined; - - var x: u16 = 0; - var saw_any_digits = false; - var index: u8 = 0; - var scope_id = false; - var abbrv = false; - for (buf, 0..) |c, i| { - if (scope_id) { - if (c >= '0' and c <= '9') { - const digit = c - '0'; - { - const ov = @mulWithOverflow(result.sa.scope_id, 10); - if (ov[1] != 0) return error.Overflow; - result.sa.scope_id = ov[0]; - } - { - const ov = @addWithOverflow(result.sa.scope_id, digit); - if (ov[1] != 0) return error.Overflow; - result.sa.scope_id = ov[0]; - } - } else { - return error.InvalidCharacter; - } - } else if (c == ':') { - if (!saw_any_digits) { - if (abbrv) return error.InvalidCharacter; // ':::' - if (i != 0) abbrv = true; - @memset(ip_slice[index..], 0); - ip_slice = tail[0..]; - index = 0; - continue; - } - if (index == 14) { - return error.InvalidEnd; - } - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - - x = 0; - saw_any_digits = false; - } else if (c == '%') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - scope_id = true; - saw_any_digits = false; - } else if (c == '.') { - if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) { - // must start with '::ffff:' - return error.InvalidIpv4Mapping; - } - const start_index = mem.lastIndexOfScalar(u8, buf[0..i], ':').? + 1; - const addr = (Ip4Address.parse(buf[start_index..], 0) catch { - return error.InvalidIpv4Mapping; - }).sa.addr; - ip_slice = result.sa.addr[0..]; - ip_slice[10] = 0xff; - ip_slice[11] = 0xff; - - const ptr = mem.sliceAsBytes(@as(*const [1]u32, &addr)[0..]); - - ip_slice[12] = ptr[0]; - ip_slice[13] = ptr[1]; - ip_slice[14] = ptr[2]; - ip_slice[15] = ptr[3]; - return result; - } else { - const digit = try std.fmt.charToDigit(c, 16); - { - const ov = @mulWithOverflow(x, 16); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - { - const ov = @addWithOverflow(x, digit); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - saw_any_digits = true; - } - } - - if (!saw_any_digits and !abbrv) { - return error.Incomplete; - } - if (!abbrv and index < 14) { - return error.Incomplete; - } - - if (index == 14) { - ip_slice[14] = @as(u8, @truncate(x >> 8)); - ip_slice[15] = @as(u8, @truncate(x)); - return result; - } else { - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - @memcpy(result.sa.addr[16 - index ..][0..index], ip_slice[0..index]); - return result; - } - } - - pub fn resolve(buf: []const u8, port: u16) IPv6ResolveError!Ip6Address { - // TODO: Unify the implementations of resolveIp6 and parseIp6. - var result = Ip6Address{ - .sa = posix.sockaddr.in6{ - .scope_id = 0, - .port = mem.nativeToBig(u16, port), - .flowinfo = 0, - .addr = undefined, - }, - }; - var ip_slice: *[16]u8 = result.sa.addr[0..]; - - var tail: [16]u8 = undefined; - - var x: u16 = 0; - var saw_any_digits = false; - var index: u8 = 0; - var abbrv = false; - - var scope_id = false; - var scope_id_value: [posix.IFNAMESIZE - 1]u8 = undefined; - var scope_id_index: usize = 0; - - for (buf, 0..) |c, i| { - if (scope_id) { - // Handling of percent-encoding should be for an URI library. - if ((c >= '0' and c <= '9') or - (c >= 'A' and c <= 'Z') or - (c >= 'a' and c <= 'z') or - (c == '-') or (c == '.') or (c == '_') or (c == '~')) - { - if (scope_id_index >= scope_id_value.len) { - return error.Overflow; - } - - scope_id_value[scope_id_index] = c; - scope_id_index += 1; - } else { - return error.InvalidCharacter; - } - } else if (c == ':') { - if (!saw_any_digits) { - if (abbrv) return error.InvalidCharacter; // ':::' - if (i != 0) abbrv = true; - @memset(ip_slice[index..], 0); - ip_slice = tail[0..]; - index = 0; - continue; - } - if (index == 14) { - return error.InvalidEnd; - } - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - - x = 0; - saw_any_digits = false; - } else if (c == '%') { - if (!saw_any_digits) { - return error.InvalidCharacter; - } - scope_id = true; - saw_any_digits = false; - } else if (c == '.') { - if (!abbrv or ip_slice[0] != 0xff or ip_slice[1] != 0xff) { - // must start with '::ffff:' - return error.InvalidIpv4Mapping; - } - const start_index = mem.lastIndexOfScalar(u8, buf[0..i], ':').? + 1; - const addr = (Ip4Address.parse(buf[start_index..], 0) catch { - return error.InvalidIpv4Mapping; - }).sa.addr; - ip_slice = result.sa.addr[0..]; - ip_slice[10] = 0xff; - ip_slice[11] = 0xff; - - const ptr = mem.sliceAsBytes(@as(*const [1]u32, &addr)[0..]); - - ip_slice[12] = ptr[0]; - ip_slice[13] = ptr[1]; - ip_slice[14] = ptr[2]; - ip_slice[15] = ptr[3]; - return result; - } else { - const digit = try std.fmt.charToDigit(c, 16); - { - const ov = @mulWithOverflow(x, 16); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - { - const ov = @addWithOverflow(x, digit); - if (ov[1] != 0) return error.Overflow; - x = ov[0]; - } - saw_any_digits = true; - } - } - - if (!saw_any_digits and !abbrv) { - return error.Incomplete; - } - - if (scope_id and scope_id_index == 0) { - return error.Incomplete; - } - - var resolved_scope_id: u32 = 0; - if (scope_id_index > 0) { - const scope_id_str = scope_id_value[0..scope_id_index]; - resolved_scope_id = std.fmt.parseInt(u32, scope_id_str, 10) catch |err| blk: { - if (err != error.InvalidCharacter) return err; - break :blk try if_nametoindex(scope_id_str); - }; - } - - result.sa.scope_id = resolved_scope_id; - - if (index == 14) { - ip_slice[14] = @as(u8, @truncate(x >> 8)); - ip_slice[15] = @as(u8, @truncate(x)); - return result; - } else { - ip_slice[index] = @as(u8, @truncate(x >> 8)); - index += 1; - ip_slice[index] = @as(u8, @truncate(x)); - index += 1; - @memcpy(result.sa.addr[16 - index ..][0..index], ip_slice[0..index]); - return result; - } - } - - pub fn init(addr: [16]u8, port: u16, flowinfo: u32, scope_id: u32) Ip6Address { - return Ip6Address{ - .sa = posix.sockaddr.in6{ - .addr = addr, - .port = mem.nativeToBig(u16, port), - .flowinfo = flowinfo, - .scope_id = scope_id, - }, - }; - } - - /// Returns the port in native endian. - /// Asserts that the address is ip4 or ip6. - pub fn getPort(self: Ip6Address) u16 { - return mem.bigToNative(u16, self.sa.port); - } - - /// `port` is native-endian. - /// Asserts that the address is ip4 or ip6. - pub fn setPort(self: *Ip6Address, port: u16) void { - self.sa.port = mem.nativeToBig(u16, port); - } - - pub fn format(self: Ip6Address, w: *Io.Writer) Io.Writer.Error!void { - const port = mem.bigToNative(u16, self.sa.port); - if (mem.eql(u8, self.sa.addr[0..12], &[_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff })) { - try w.print("[::ffff:{d}.{d}.{d}.{d}]:{d}", .{ - self.sa.addr[12], - self.sa.addr[13], - self.sa.addr[14], - self.sa.addr[15], - port, - }); - return; - } - const big_endian_parts = @as(*align(1) const [8]u16, @ptrCast(&self.sa.addr)); - const native_endian_parts = switch (native_endian) { - .big => big_endian_parts.*, - .little => blk: { - var buf: [8]u16 = undefined; - for (big_endian_parts, 0..) |part, i| { - buf[i] = mem.bigToNative(u16, part); - } - break :blk buf; - }, - }; - - // Find the longest zero run - var longest_start: usize = 8; - var longest_len: usize = 0; - var current_start: usize = 0; - var current_len: usize = 0; - - for (native_endian_parts, 0..) |part, i| { - if (part == 0) { - if (current_len == 0) { - current_start = i; - } - current_len += 1; - if (current_len > longest_len) { - longest_start = current_start; - longest_len = current_len; - } - } else { - current_len = 0; - } - } - - // Only compress if the longest zero run is 2 or more - if (longest_len < 2) { - longest_start = 8; - longest_len = 0; - } - - try w.writeAll("["); - var i: usize = 0; - var abbrv = false; - while (i < native_endian_parts.len) : (i += 1) { - if (i == longest_start) { - // Emit "::" for the longest zero run - if (!abbrv) { - try w.writeAll(if (i == 0) "::" else ":"); - abbrv = true; - } - i += longest_len - 1; // Skip the compressed range - continue; - } - if (abbrv) { - abbrv = false; - } - try w.print("{x}", .{native_endian_parts[i]}); - if (i != native_endian_parts.len - 1) { - try w.writeAll(":"); - } - } - if (self.sa.scope_id != 0) { - try w.print("%{}", .{self.sa.scope_id}); - } - try w.print("]:{}", .{port}); - } - - pub fn getOsSockLen(self: Ip6Address) posix.socklen_t { - _ = self; - return @sizeOf(posix.sockaddr.in6); - } -}; - -pub fn connectUnixSocket(path: []const u8) !Stream { - const opt_non_block = 0; - const sockfd = try posix.socket( - posix.AF.UNIX, - posix.SOCK.STREAM | posix.SOCK.CLOEXEC | opt_non_block, - 0, - ); - errdefer Stream.close(.{ .handle = sockfd }); - - var addr = try Address.initUnix(path); - try posix.connect(sockfd, &addr.any, addr.getOsSockLen()); - - return .{ .handle = sockfd }; -} - -fn if_nametoindex(name: []const u8) IPv6InterfaceError!u32 { - if (native_os == .linux) { - var ifr: posix.ifreq = undefined; - const sockfd = try posix.socket(posix.AF.UNIX, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0); - defer Stream.close(.{ .handle = sockfd }); - - @memcpy(ifr.ifrn.name[0..name.len], name); - ifr.ifrn.name[name.len] = 0; - - // TODO investigate if this needs to be integrated with evented I/O. - try posix.ioctl_SIOCGIFINDEX(sockfd, &ifr); - - return @bitCast(ifr.ifru.ivalue); - } - - if (native_os.isDarwin()) { - if (name.len >= posix.IFNAMESIZE) - return error.NameTooLong; - - var if_name: [posix.IFNAMESIZE:0]u8 = undefined; - @memcpy(if_name[0..name.len], name); - if_name[name.len] = 0; - const if_slice = if_name[0..name.len :0]; - const index = std.c.if_nametoindex(if_slice); - if (index == 0) - return error.InterfaceNotFound; - return @as(u32, @bitCast(index)); - } - - if (native_os == .windows) { - if (name.len >= posix.IFNAMESIZE) - return error.NameTooLong; - - var interface_name: [posix.IFNAMESIZE:0]u8 = undefined; - @memcpy(interface_name[0..name.len], name); - interface_name[name.len] = 0; - const index = std.os.windows.ws2_32.if_nametoindex(@as([*:0]const u8, &interface_name)); - if (index == 0) - return error.InterfaceNotFound; - return index; - } - - @compileError("std.net.if_nametoindex unimplemented for this OS"); -} - -pub const AddressList = struct { - arena: std.heap.ArenaAllocator, - addrs: []Address, - canon_name: ?[]u8, - - pub fn deinit(self: *AddressList) void { - // Here we copy the arena allocator into stack memory, because - // otherwise it would destroy itself while it was still working. - var arena = self.arena; - arena.deinit(); - // self is destroyed - } -}; - -pub const TcpConnectToHostError = GetAddressListError || TcpConnectToAddressError; - -/// All memory allocated with `allocator` will be freed before this function returns. -pub fn tcpConnectToHost(allocator: Allocator, name: []const u8, port: u16) TcpConnectToHostError!Stream { - const list = try getAddressList(allocator, name, port); - defer list.deinit(); - - if (list.addrs.len == 0) return error.UnknownHostName; - - for (list.addrs) |addr| { - return tcpConnectToAddress(addr) catch |err| switch (err) { - error.ConnectionRefused => { - continue; - }, - else => return err, - }; - } - return posix.ConnectError.ConnectionRefused; -} - -pub const TcpConnectToAddressError = posix.SocketError || posix.ConnectError; - -pub fn tcpConnectToAddress(address: Address) TcpConnectToAddressError!Stream { - const nonblock = 0; - const sock_flags = posix.SOCK.STREAM | nonblock | - (if (native_os == .windows) 0 else posix.SOCK.CLOEXEC); - const sockfd = try posix.socket(address.any.family, sock_flags, posix.IPPROTO.TCP); - errdefer Stream.close(.{ .handle = sockfd }); - - try posix.connect(sockfd, &address.any, address.getOsSockLen()); - - return Stream{ .handle = sockfd }; -} - -// TODO: Instead of having a massive error set, make the error set have categories, and then -// store the sub-error as a diagnostic value. -const GetAddressListError = Allocator.Error || File.OpenError || File.ReadError || posix.SocketError || posix.BindError || posix.SetSockOptError || error{ - TemporaryNameServerFailure, - NameServerFailure, - AddressFamilyNotSupported, - UnknownHostName, - ServiceUnavailable, - Unexpected, - - HostLacksNetworkAddresses, - - InvalidCharacter, - InvalidEnd, - NonCanonical, - Overflow, - Incomplete, - InvalidIpv4Mapping, - InvalidIpAddressFormat, - - InterfaceNotFound, - FileSystem, - ResolveConfParseFailed, -}; - -/// Call `AddressList.deinit` on the result. -pub fn getAddressList(gpa: Allocator, name: []const u8, port: u16) GetAddressListError!*AddressList { - const result = blk: { - var arena = std.heap.ArenaAllocator.init(gpa); - errdefer arena.deinit(); - - const result = try arena.allocator().create(AddressList); - result.* = AddressList{ - .arena = arena, - .addrs = undefined, - .canon_name = null, - }; - break :blk result; - }; - const arena = result.arena.allocator(); - errdefer result.deinit(); - - if (native_os == .windows) { - const name_c = try gpa.dupeZ(u8, name); - defer gpa.free(name_c); - - const port_c = try std.fmt.allocPrintSentinel(gpa, "{d}", .{port}, 0); - defer gpa.free(port_c); - - const ws2_32 = windows.ws2_32; - const hints: posix.addrinfo = .{ - .flags = .{ .NUMERICSERV = true }, - .family = posix.AF.UNSPEC, - .socktype = posix.SOCK.STREAM, - .protocol = posix.IPPROTO.TCP, - .canonname = null, - .addr = null, - .addrlen = 0, - .next = null, - }; - var res: ?*posix.addrinfo = null; - var first = true; - while (true) { - const rc = ws2_32.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res); - switch (@as(windows.ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(rc))))) { - @as(windows.ws2_32.WinsockError, @enumFromInt(0)) => break, - .WSATRY_AGAIN => return error.TemporaryNameServerFailure, - .WSANO_RECOVERY => return error.NameServerFailure, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, - .WSA_NOT_ENOUGH_MEMORY => return error.OutOfMemory, - .WSAHOST_NOT_FOUND => return error.UnknownHostName, - .WSATYPE_NOT_FOUND => return error.ServiceUnavailable, - .WSAEINVAL => unreachable, - .WSAESOCKTNOSUPPORT => unreachable, - .WSANOTINITIALISED => { - if (!first) return error.Unexpected; - first = false; - try windows.callWSAStartup(); - continue; - }, - else => |err| return windows.unexpectedWSAError(err), - } - } - defer ws2_32.freeaddrinfo(res); - - const addr_count = blk: { - var count: usize = 0; - var it = res; - while (it) |info| : (it = info.next) { - if (info.addr != null) { - count += 1; - } - } - break :blk count; - }; - result.addrs = try arena.alloc(Address, addr_count); - - var it = res; - var i: usize = 0; - while (it) |info| : (it = info.next) { - const addr = info.addr orelse continue; - result.addrs[i] = Address.initPosix(@alignCast(addr)); - - if (info.canonname) |n| { - if (result.canon_name == null) { - result.canon_name = try arena.dupe(u8, mem.sliceTo(n, 0)); - } - } - i += 1; - } - - return result; - } - - if (builtin.link_libc) { - const name_c = try gpa.dupeZ(u8, name); - defer gpa.free(name_c); - - const port_c = try std.fmt.allocPrintSentinel(gpa, "{d}", .{port}, 0); - defer gpa.free(port_c); - - const hints: posix.addrinfo = .{ - .flags = .{ .NUMERICSERV = true }, - .family = posix.AF.UNSPEC, - .socktype = posix.SOCK.STREAM, - .protocol = posix.IPPROTO.TCP, - .canonname = null, - .addr = null, - .addrlen = 0, - .next = null, - }; - var res: ?*posix.addrinfo = null; - switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) { - @as(posix.system.EAI, @enumFromInt(0)) => {}, - .ADDRFAMILY => return error.HostLacksNetworkAddresses, - .AGAIN => return error.TemporaryNameServerFailure, - .BADFLAGS => unreachable, // Invalid hints - .FAIL => return error.NameServerFailure, - .FAMILY => return error.AddressFamilyNotSupported, - .MEMORY => return error.OutOfMemory, - .NODATA => return error.HostLacksNetworkAddresses, - .NONAME => return error.UnknownHostName, - .SERVICE => return error.ServiceUnavailable, - .SOCKTYPE => unreachable, // Invalid socket type requested in hints - .SYSTEM => switch (posix.errno(-1)) { - else => |e| return posix.unexpectedErrno(e), - }, - else => unreachable, - } - defer if (res) |some| posix.system.freeaddrinfo(some); - - const addr_count = blk: { - var count: usize = 0; - var it = res; - while (it) |info| : (it = info.next) { - if (info.addr != null) { - count += 1; - } - } - break :blk count; - }; - result.addrs = try arena.alloc(Address, addr_count); - - var it = res; - var i: usize = 0; - while (it) |info| : (it = info.next) { - const addr = info.addr orelse continue; - result.addrs[i] = Address.initPosix(@alignCast(addr)); - - if (info.canonname) |n| { - if (result.canon_name == null) { - result.canon_name = try arena.dupe(u8, mem.sliceTo(n, 0)); - } - } - i += 1; - } - - return result; - } - - if (native_os == .linux) { - const family = posix.AF.UNSPEC; - var lookup_addrs: ArrayList(LookupAddr) = .empty; - defer lookup_addrs.deinit(gpa); - - var canon: ArrayList(u8) = .empty; - defer canon.deinit(gpa); - - try linuxLookupName(gpa, &lookup_addrs, &canon, name, family, .{ .NUMERICSERV = true }, port); - - result.addrs = try arena.alloc(Address, lookup_addrs.items.len); - if (canon.items.len != 0) { - result.canon_name = try arena.dupe(u8, canon.items); - } - - for (lookup_addrs.items, 0..) |lookup_addr, i| { - result.addrs[i] = lookup_addr.addr; - assert(result.addrs[i].getPort() == port); - } - - return result; - } - @compileError("std.net.getAddressList unimplemented for this OS"); -} - -const LookupAddr = struct { - addr: Address, - sortkey: i32 = 0, -}; - -const DAS_USABLE = 0x40000000; -const DAS_MATCHINGSCOPE = 0x20000000; -const DAS_MATCHINGLABEL = 0x10000000; -const DAS_PREC_SHIFT = 20; -const DAS_SCOPE_SHIFT = 16; -const DAS_PREFIX_SHIFT = 8; -const DAS_ORDER_SHIFT = 0; - -fn linuxLookupName( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - opt_name: ?[]const u8, - family: posix.sa_family_t, - flags: posix.AI, - port: u16, -) !void { - if (opt_name) |name| { - // reject empty name and check len so it fits into temp bufs - canon.items.len = 0; - try canon.appendSlice(gpa, name); - if (Address.parseExpectingFamily(name, family, port)) |addr| { - try addrs.append(gpa, .{ .addr = addr }); - } else |name_err| if (flags.NUMERICHOST) { - return name_err; - } else { - try linuxLookupNameFromHosts(gpa, addrs, canon, name, family, port); - if (addrs.items.len == 0) { - // RFC 6761 Section 6.3.3 - // Name resolution APIs and libraries SHOULD recognize localhost - // names as special and SHOULD always return the IP loopback address - // for address queries and negative responses for all other query - // types. - - // Check for equal to "localhost(.)" or ends in ".localhost(.)" - const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; - if (mem.endsWith(u8, name, localhost) and (name.len == localhost.len or name[name.len - localhost.len] == '.')) { - try addrs.append(gpa, .{ .addr = .{ .in = Ip4Address.parse("127.0.0.1", port) catch unreachable } }); - try addrs.append(gpa, .{ .addr = .{ .in6 = Ip6Address.parse("::1", port) catch unreachable } }); - return; - } - - try linuxLookupNameFromDnsSearch(gpa, addrs, canon, name, family, port); - } - } - } else { - try canon.resize(gpa, 0); - try addrs.ensureUnusedCapacity(gpa, 2); - linuxLookupNameFromNull(addrs, family, flags, port); - } - if (addrs.items.len == 0) return error.UnknownHostName; - - // No further processing is needed if there are fewer than 2 - // results or if there are only IPv4 results. - if (addrs.items.len == 1 or family == posix.AF.INET) return; - const all_ip4 = for (addrs.items) |addr| { - if (addr.addr.any.family != posix.AF.INET) break false; - } else true; - if (all_ip4) return; - - // The following implements a subset of RFC 3484/6724 destination - // address selection by generating a single 31-bit sort key for - // each address. Rules 3, 4, and 7 are omitted for having - // excessive runtime and code size cost and dubious benefit. - // So far the label/precedence table cannot be customized. - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - for (addrs.items, 0..) |*addr, i| { - var key: i32 = 0; - var sa6: posix.sockaddr.in6 = undefined; - @memset(@as([*]u8, @ptrCast(&sa6))[0..@sizeOf(posix.sockaddr.in6)], 0); - var da6 = posix.sockaddr.in6{ - .family = posix.AF.INET6, - .scope_id = addr.addr.in6.sa.scope_id, - .port = 65535, - .flowinfo = 0, - .addr = [1]u8{0} ** 16, - }; - var sa4: posix.sockaddr.in = undefined; - @memset(@as([*]u8, @ptrCast(&sa4))[0..@sizeOf(posix.sockaddr.in)], 0); - var da4 = posix.sockaddr.in{ - .family = posix.AF.INET, - .port = 65535, - .addr = 0, - .zero = [1]u8{0} ** 8, - }; - var sa: *align(4) posix.sockaddr = undefined; - var da: *align(4) posix.sockaddr = undefined; - var salen: posix.socklen_t = undefined; - var dalen: posix.socklen_t = undefined; - if (addr.addr.any.family == posix.AF.INET6) { - da6.addr = addr.addr.in6.sa.addr; - da = @ptrCast(&da6); - dalen = @sizeOf(posix.sockaddr.in6); - sa = @ptrCast(&sa6); - salen = @sizeOf(posix.sockaddr.in6); - } else { - sa6.addr[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - da6.addr[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - mem.writeInt(u32, da6.addr[12..], addr.addr.in.sa.addr, native_endian); - da4.addr = addr.addr.in.sa.addr; - da = @ptrCast(&da4); - dalen = @sizeOf(posix.sockaddr.in); - sa = @ptrCast(&sa4); - salen = @sizeOf(posix.sockaddr.in); - } - const dpolicy = policyOf(da6.addr); - const dscope: i32 = scopeOf(da6.addr); - const dlabel = dpolicy.label; - const dprec: i32 = dpolicy.prec; - const MAXADDRS = 3; - var prefixlen: i32 = 0; - const sock_flags = posix.SOCK.DGRAM | posix.SOCK.CLOEXEC; - if (posix.socket(addr.addr.any.family, sock_flags, posix.IPPROTO.UDP)) |fd| syscalls: { - defer Stream.close(.{ .handle = fd }); - posix.connect(fd, da, dalen) catch break :syscalls; - key |= DAS_USABLE; - posix.getsockname(fd, sa, &salen) catch break :syscalls; - if (addr.addr.any.family == posix.AF.INET) { - mem.writeInt(u32, sa6.addr[12..16], sa4.addr, native_endian); - } - if (dscope == @as(i32, scopeOf(sa6.addr))) key |= DAS_MATCHINGSCOPE; - if (dlabel == labelOf(sa6.addr)) key |= DAS_MATCHINGLABEL; - prefixlen = prefixMatch(sa6.addr, da6.addr); - } else |_| {} - key |= dprec << DAS_PREC_SHIFT; - key |= (15 - dscope) << DAS_SCOPE_SHIFT; - key |= prefixlen << DAS_PREFIX_SHIFT; - key |= (MAXADDRS - @as(i32, @intCast(i))) << DAS_ORDER_SHIFT; - addr.sortkey = key; - } - mem.sort(LookupAddr, addrs.items, {}, addrCmpLessThan); -} - -const Policy = struct { - addr: [16]u8, - len: u8, - mask: u8, - prec: u8, - label: u8, -}; - -const defined_policies = [_]Policy{ - Policy{ - .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01".*, - .len = 15, - .mask = 0xff, - .prec = 50, - .label = 0, - }, - Policy{ - .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00".*, - .len = 11, - .mask = 0xff, - .prec = 35, - .label = 4, - }, - Policy{ - .addr = "\x20\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 1, - .mask = 0xff, - .prec = 30, - .label = 2, - }, - Policy{ - .addr = "\x20\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 3, - .mask = 0xff, - .prec = 5, - .label = 5, - }, - Policy{ - .addr = "\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 0, - .mask = 0xfe, - .prec = 3, - .label = 13, - }, - // These are deprecated and/or returned to the address - // pool, so despite the RFC, treating them as special - // is probably wrong. - // { "", 11, 0xff, 1, 3 }, - // { "\xfe\xc0", 1, 0xc0, 1, 11 }, - // { "\x3f\xfe", 1, 0xff, 1, 12 }, - // Last rule must match all addresses to stop loop. - Policy{ - .addr = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".*, - .len = 0, - .mask = 0, - .prec = 40, - .label = 1, - }, -}; - -fn policyOf(a: [16]u8) *const Policy { - for (&defined_policies) |*policy| { - if (!mem.eql(u8, a[0..policy.len], policy.addr[0..policy.len])) continue; - if ((a[policy.len] & policy.mask) != policy.addr[policy.len]) continue; - return policy; - } - unreachable; -} - -fn scopeOf(a: [16]u8) u8 { - if (IN6_IS_ADDR_MULTICAST(a)) return a[1] & 15; - if (IN6_IS_ADDR_LINKLOCAL(a)) return 2; - if (IN6_IS_ADDR_LOOPBACK(a)) return 2; - if (IN6_IS_ADDR_SITELOCAL(a)) return 5; - return 14; -} - -fn prefixMatch(s: [16]u8, d: [16]u8) u8 { - // TODO: This FIXME inherited from porting from musl libc. - // I don't want this to go into zig std lib 1.0.0. - - // FIXME: The common prefix length should be limited to no greater - // than the nominal length of the prefix portion of the source - // address. However the definition of the source prefix length is - // not clear and thus this limiting is not yet implemented. - var i: u8 = 0; - while (i < 128 and ((s[i / 8] ^ d[i / 8]) & (@as(u8, 128) >> @as(u3, @intCast(i % 8)))) == 0) : (i += 1) {} - return i; -} - -fn labelOf(a: [16]u8) u8 { - return policyOf(a).label; -} - -fn IN6_IS_ADDR_MULTICAST(a: [16]u8) bool { - return a[0] == 0xff; -} - -fn IN6_IS_ADDR_LINKLOCAL(a: [16]u8) bool { - return a[0] == 0xfe and (a[1] & 0xc0) == 0x80; -} - -fn IN6_IS_ADDR_LOOPBACK(a: [16]u8) bool { - return a[0] == 0 and a[1] == 0 and - a[2] == 0 and - a[12] == 0 and a[13] == 0 and - a[14] == 0 and a[15] == 1; -} - -fn IN6_IS_ADDR_SITELOCAL(a: [16]u8) bool { - return a[0] == 0xfe and (a[1] & 0xc0) == 0xc0; -} - -// Parameters `b` and `a` swapped to make this descending. -fn addrCmpLessThan(context: void, b: LookupAddr, a: LookupAddr) bool { - _ = context; - return a.sortkey < b.sortkey; -} - -fn linuxLookupNameFromNull( - addrs: *ArrayList(LookupAddr), - family: posix.sa_family_t, - flags: posix.AI, - port: u16, -) void { - if (flags.PASSIVE) { - if (family != posix.AF.INET6) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp4([1]u8{0} ** 4, port), - }); - } - if (family != posix.AF.INET) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp6([1]u8{0} ** 16, port, 0, 0), - }); - } - } else { - if (family != posix.AF.INET6) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp4([4]u8{ 127, 0, 0, 1 }, port), - }); - } - if (family != posix.AF.INET) { - addrs.appendAssumeCapacity(.{ - .addr = Address.initIp6(([1]u8{0} ** 15) ++ [1]u8{1}, port, 0, 0), - }); - } - } -} - -fn linuxLookupNameFromHosts( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - port: u16, -) !void { - const file = fs.openFileAbsoluteZ("/etc/hosts", .{}) catch |err| switch (err) { - error.FileNotFound, - error.NotDir, - error.AccessDenied, - => return, - else => |e| return e, - }; - defer file.close(); - - var line_buf: [512]u8 = undefined; - var file_reader = file.reader(&line_buf); - return parseHosts(gpa, addrs, canon, name, family, port, &file_reader.interface) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.ReadFailed => return file_reader.err.?, - }; -} - -fn parseHosts( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - port: u16, - br: *Io.Reader, -) error{ OutOfMemory, ReadFailed }!void { - while (true) { - const line = br.takeDelimiter('\n') catch |err| switch (err) { - error.StreamTooLong => { - // Skip lines that are too long. - _ = br.discardDelimiterInclusive('\n') catch |e| switch (e) { - error.EndOfStream => break, - error.ReadFailed => return error.ReadFailed, - }; - continue; - }, - error.ReadFailed => return error.ReadFailed, - } orelse { - break; // end of stream - }; - var split_it = mem.splitScalar(u8, line, '#'); - const no_comment_line = split_it.first(); - - var line_it = mem.tokenizeAny(u8, no_comment_line, " \t"); - const ip_text = line_it.next() orelse continue; - var first_name_text: ?[]const u8 = null; - while (line_it.next()) |name_text| { - if (first_name_text == null) first_name_text = name_text; - if (mem.eql(u8, name_text, name)) { - break; - } - } else continue; - - const addr = Address.parseExpectingFamily(ip_text, family, port) catch |err| switch (err) { - error.Overflow, - error.InvalidEnd, - error.InvalidCharacter, - error.Incomplete, - error.InvalidIpAddressFormat, - error.InvalidIpv4Mapping, - error.NonCanonical, - => continue, - }; - try addrs.append(gpa, .{ .addr = addr }); - - // first name is canonical name - const name_text = first_name_text.?; - if (isValidHostName(name_text)) { - canon.items.len = 0; - try canon.appendSlice(gpa, name_text); - } - } -} - -test parseHosts { - if (builtin.os.tag == .wasi) { - // TODO parsing addresses should not have OS dependencies - return error.SkipZigTest; - } - var reader: Io.Reader = .fixed( - \\127.0.0.1 localhost - \\::1 localhost - \\127.0.0.2 abcd - ); - var addrs: ArrayList(LookupAddr) = .empty; - defer addrs.deinit(std.testing.allocator); - var canon: ArrayList(u8) = .empty; - defer canon.deinit(std.testing.allocator); - try parseHosts(std.testing.allocator, &addrs, &canon, "abcd", posix.AF.UNSPEC, 1234, &reader); - try std.testing.expectEqual(1, addrs.items.len); - try std.testing.expectFmt("127.0.0.2:1234", "{f}", .{addrs.items[0].addr}); -} - -pub fn isValidHostName(bytes: []const u8) bool { - _ = std.Io.net.HostName.init(bytes) catch return false; - return true; -} - -fn linuxLookupNameFromDnsSearch( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - port: u16, -) !void { - var rc: ResolvConf = undefined; - rc.init(gpa) catch return error.ResolveConfParseFailed; - defer rc.deinit(); - - // Count dots, suppress search when >=ndots or name ends in - // a dot, which is an explicit request for global scope. - var dots: usize = 0; - for (name) |byte| { - if (byte == '.') dots += 1; - } - - const search = if (dots >= rc.ndots or mem.endsWith(u8, name, ".")) - "" - else - rc.search.items; - - var canon_name = name; - - // Strip final dot for canon, fail if multiple trailing dots. - if (mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; - if (mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; - - // Name with search domain appended is setup in canon[]. This both - // provides the desired default canonical name (if the requested - // name is not a CNAME record) and serves as a buffer for passing - // the full requested name to name_from_dns. - try canon.resize(gpa, canon_name.len); - @memcpy(canon.items, canon_name); - try canon.append(gpa, '.'); - - var tok_it = mem.tokenizeAny(u8, search, " \t"); - while (tok_it.next()) |tok| { - canon.shrinkRetainingCapacity(canon_name.len + 1); - try canon.appendSlice(gpa, tok); - try linuxLookupNameFromDns(gpa, addrs, canon, canon.items, family, rc, port); - if (addrs.items.len != 0) return; - } - - canon.shrinkRetainingCapacity(canon_name.len); - return linuxLookupNameFromDns(gpa, addrs, canon, name, family, rc, port); -} - -const dpc_ctx = struct { - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - port: u16, -}; - -fn linuxLookupNameFromDns( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - canon: *ArrayList(u8), - name: []const u8, - family: posix.sa_family_t, - rc: ResolvConf, - port: u16, -) !void { - const ctx: dpc_ctx = .{ - .gpa = gpa, - .addrs = addrs, - .canon = canon, - .port = port, - }; - const AfRr = struct { - af: posix.sa_family_t, - rr: u8, - }; - const afrrs = [_]AfRr{ - .{ .af = posix.AF.INET6, .rr = posix.RR.A }, - .{ .af = posix.AF.INET, .rr = posix.RR.AAAA }, - }; - var qbuf: [2][280]u8 = undefined; - var abuf: [2][512]u8 = undefined; - var qp: [2][]const u8 = undefined; - const apbuf = [2][]u8{ &abuf[0], &abuf[1] }; - var nq: usize = 0; - - for (afrrs) |afrr| { - if (family != afrr.af) { - const len = posix.res_mkquery(0, name, 1, afrr.rr, &[_]u8{}, null, &qbuf[nq]); - qp[nq] = qbuf[nq][0..len]; - nq += 1; - } - } - - var ap = [2][]u8{ apbuf[0], apbuf[1] }; - ap[0].len = 0; - ap[1].len = 0; - - try rc.resMSendRc(qp[0..nq], ap[0..nq], apbuf[0..nq]); - - var i: usize = 0; - while (i < nq) : (i += 1) { - dnsParse(ap[i], ctx, dnsParseCallback) catch {}; - } - - if (addrs.items.len != 0) return; - if (ap[0].len < 4 or (ap[0][3] & 15) == 2) return error.TemporaryNameServerFailure; - if ((ap[0][3] & 15) == 0) return error.UnknownHostName; - if ((ap[0][3] & 15) == 3) return; - return error.NameServerFailure; -} - -const ResolvConf = struct { - gpa: Allocator, - attempts: u32, - ndots: u32, - timeout: u32, - search: ArrayList(u8), - /// TODO there are actually only allowed to be maximum 3 nameservers, no need - /// for an array list. - ns: ArrayList(LookupAddr), - - /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. - /// TODO: https://github.com/ziglang/zig/issues/2765 and https://github.com/ziglang/zig/issues/2761 - fn init(rc: *ResolvConf, gpa: Allocator) !void { - rc.* = .{ - .gpa = gpa, - .ns = .empty, - .search = .empty, - .ndots = 1, - .timeout = 5, - .attempts = 2, - }; - errdefer rc.deinit(); - - const file = fs.openFileAbsoluteZ("/etc/resolv.conf", .{}) catch |err| switch (err) { - error.FileNotFound, - error.NotDir, - error.AccessDenied, - => return linuxLookupNameFromNumericUnspec(gpa, &rc.ns, "127.0.0.1", 53), - else => |e| return e, - }; - defer file.close(); - - var line_buf: [512]u8 = undefined; - var file_reader = file.reader(&line_buf); - return parse(rc, &file_reader.interface) catch |err| switch (err) { - error.ReadFailed => return file_reader.err.?, - else => |e| return e, - }; - } - - const Directive = enum { options, nameserver, domain, search }; - const Option = enum { ndots, attempts, timeout }; - - fn parse(rc: *ResolvConf, reader: *Io.Reader) !void { - const gpa = rc.gpa; - while (reader.takeSentinel('\n')) |line_with_comment| { - const line = line: { - var split = mem.splitScalar(u8, line_with_comment, '#'); - break :line split.first(); - }; - var line_it = mem.tokenizeAny(u8, line, " \t"); - - const token = line_it.next() orelse continue; - switch (std.meta.stringToEnum(Directive, token) orelse continue) { - .options => while (line_it.next()) |sub_tok| { - var colon_it = mem.splitScalar(u8, sub_tok, ':'); - const name = colon_it.first(); - const value_txt = colon_it.next() orelse continue; - const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) { - error.Overflow => 255, - error.InvalidCharacter => continue, - }; - switch (std.meta.stringToEnum(Option, name) orelse continue) { - .ndots => rc.ndots = @min(value, 15), - .attempts => rc.attempts = @min(value, 10), - .timeout => rc.timeout = @min(value, 60), - } - }, - .nameserver => { - const ip_txt = line_it.next() orelse continue; - try linuxLookupNameFromNumericUnspec(gpa, &rc.ns, ip_txt, 53); - }, - .domain, .search => { - rc.search.items.len = 0; - try rc.search.appendSlice(gpa, line_it.rest()); - }, - } - } else |err| switch (err) { - error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream, - else => |e| return e, - } - - if (rc.ns.items.len == 0) { - return linuxLookupNameFromNumericUnspec(gpa, &rc.ns, "127.0.0.1", 53); - } - } - - fn resMSendRc( - rc: ResolvConf, - queries: []const []const u8, - answers: [][]u8, - answer_bufs: []const []u8, - ) !void { - const gpa = rc.gpa; - const timeout = 1000 * rc.timeout; - const attempts = rc.attempts; - - var sl: posix.socklen_t = @sizeOf(posix.sockaddr.in); - var family: posix.sa_family_t = posix.AF.INET; - - var ns_list: ArrayList(Address) = .empty; - defer ns_list.deinit(gpa); - - try ns_list.resize(gpa, rc.ns.items.len); - - for (ns_list.items, rc.ns.items) |*ns, iplit| { - ns.* = iplit.addr; - assert(ns.getPort() == 53); - if (iplit.addr.any.family != posix.AF.INET) { - family = posix.AF.INET6; - } - } - - const flags = posix.SOCK.DGRAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK; - const fd = posix.socket(family, flags, 0) catch |err| switch (err) { - error.AddressFamilyNotSupported => blk: { - // Handle case where system lacks IPv6 support - if (family == posix.AF.INET6) { - family = posix.AF.INET; - break :blk try posix.socket(posix.AF.INET, flags, 0); - } - return err; - }, - else => |e| return e, - }; - defer Stream.close(.{ .handle = fd }); - - // Past this point, there are no errors. Each individual query will - // yield either no reply (indicated by zero length) or an answer - // packet which is up to the caller to interpret. - - // Convert any IPv4 addresses in a mixed environment to v4-mapped - if (family == posix.AF.INET6) { - try posix.setsockopt( - fd, - posix.SOL.IPV6, - std.os.linux.IPV6.V6ONLY, - &mem.toBytes(@as(c_int, 0)), - ); - for (ns_list.items) |*ns| { - if (ns.any.family != posix.AF.INET) continue; - mem.writeInt(u32, ns.in6.sa.addr[12..], ns.in.sa.addr, native_endian); - ns.in6.sa.addr[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - ns.any.family = posix.AF.INET6; - ns.in6.sa.flowinfo = 0; - ns.in6.sa.scope_id = 0; - } - sl = @sizeOf(posix.sockaddr.in6); - } - - // Get local address and open/bind a socket - var sa: Address = undefined; - @memset(@as([*]u8, @ptrCast(&sa))[0..@sizeOf(Address)], 0); - sa.any.family = family; - try posix.bind(fd, &sa.any, sl); - - var pfd = [1]posix.pollfd{posix.pollfd{ - .fd = fd, - .events = posix.POLL.IN, - .revents = undefined, - }}; - const retry_interval = timeout / attempts; - var next: u32 = 0; - var t2: u64 = @bitCast(std.time.milliTimestamp()); - const t0 = t2; - var t1 = t2 - retry_interval; - - var servfail_retry: usize = undefined; - - outer: while (t2 - t0 < timeout) : (t2 = @as(u64, @bitCast(std.time.milliTimestamp()))) { - if (t2 - t1 >= retry_interval) { - // Query all configured nameservers in parallel - var i: usize = 0; - while (i < queries.len) : (i += 1) { - if (answers[i].len == 0) { - for (ns_list.items) |*ns| { - _ = posix.sendto(fd, queries[i], posix.MSG.NOSIGNAL, &ns.any, sl) catch undefined; - } - } - } - t1 = t2; - servfail_retry = 2 * queries.len; - } - - // Wait for a response, or until time to retry - const clamped_timeout = @min(@as(u31, std.math.maxInt(u31)), t1 + retry_interval - t2); - const nevents = posix.poll(&pfd, clamped_timeout) catch 0; - if (nevents == 0) continue; - - while (true) { - var sl_copy = sl; - const rlen = posix.recvfrom(fd, answer_bufs[next], 0, &sa.any, &sl_copy) catch break; - - // Ignore non-identifiable packets - if (rlen < 4) continue; - - // Ignore replies from addresses we didn't send to - const ns = for (ns_list.items) |*ns| { - if (ns.eql(sa)) break ns; - } else continue; - - // Find which query this answer goes with, if any - var i: usize = next; - while (i < queries.len and (answer_bufs[next][0] != queries[i][0] or - answer_bufs[next][1] != queries[i][1])) : (i += 1) - {} - - if (i == queries.len) continue; - if (answers[i].len != 0) continue; - - // Only accept positive or negative responses; - // retry immediately on server failure, and ignore - // all other codes such as refusal. - switch (answer_bufs[next][3] & 15) { - 0, 3 => {}, - 2 => if (servfail_retry != 0) { - servfail_retry -= 1; - _ = posix.sendto(fd, queries[i], posix.MSG.NOSIGNAL, &ns.any, sl) catch undefined; - }, - else => continue, - } - - // Store answer in the right slot, or update next - // available temp slot if it's already in place. - answers[i].len = rlen; - if (i == next) { - while (next < queries.len and answers[next].len != 0) : (next += 1) {} - } else { - @memcpy(answer_bufs[i][0..rlen], answer_bufs[next][0..rlen]); - } - - if (next == queries.len) break :outer; - } - } - } - - fn deinit(rc: *ResolvConf) void { - const gpa = rc.gpa; - rc.ns.deinit(gpa); - rc.search.deinit(gpa); - rc.* = undefined; - } -}; - -fn linuxLookupNameFromNumericUnspec( - gpa: Allocator, - addrs: *ArrayList(LookupAddr), - name: []const u8, - port: u16, -) !void { - const addr = try Address.resolveIp(name, port); - try addrs.append(gpa, .{ .addr = addr }); -} - -fn dnsParse( - r: []const u8, - ctx: anytype, - comptime callback: anytype, -) !void { - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - if (r.len < 12) return error.InvalidDnsPacket; - if ((r[3] & 15) != 0) return; - var p = r.ptr + 12; - var qdcount = r[4] * @as(usize, 256) + r[5]; - var ancount = r[6] * @as(usize, 256) + r[7]; - if (qdcount + ancount > 64) return error.InvalidDnsPacket; - while (qdcount != 0) { - qdcount -= 1; - while (@intFromPtr(p) - @intFromPtr(r.ptr) < r.len and p[0] -% 1 < 127) p += 1; - if (p[0] > 193 or (p[0] == 193 and p[1] > 254) or @intFromPtr(p) > @intFromPtr(r.ptr) + r.len - 6) - return error.InvalidDnsPacket; - p += @as(usize, 5) + @intFromBool(p[0] != 0); - } - while (ancount != 0) { - ancount -= 1; - while (@intFromPtr(p) - @intFromPtr(r.ptr) < r.len and p[0] -% 1 < 127) p += 1; - if (p[0] > 193 or (p[0] == 193 and p[1] > 254) or @intFromPtr(p) > @intFromPtr(r.ptr) + r.len - 6) - return error.InvalidDnsPacket; - p += @as(usize, 1) + @intFromBool(p[0] != 0); - const len = p[8] * @as(usize, 256) + p[9]; - if (@intFromPtr(p) + len > @intFromPtr(r.ptr) + r.len) return error.InvalidDnsPacket; - try callback(ctx, p[1], p[10..][0..len], r); - p += 10 + len; - } -} - -fn dnsParseCallback(ctx: dpc_ctx, rr: u8, data: []const u8, packet: []const u8) !void { - const gpa = ctx.gpa; - switch (rr) { - posix.RR.A => { - if (data.len != 4) return error.InvalidDnsARecord; - try ctx.addrs.append(gpa, .{ - .addr = Address.initIp4(data[0..4].*, ctx.port), - }); - }, - posix.RR.AAAA => { - if (data.len != 16) return error.InvalidDnsAAAARecord; - try ctx.addrs.append(gpa, .{ - .addr = Address.initIp6(data[0..16].*, ctx.port, 0, 0), - }); - }, - posix.RR.CNAME => { - var tmp: [256]u8 = undefined; - // Returns len of compressed name. strlen to get canon name. - _ = try posix.dn_expand(packet, data, &tmp); - const canon_name = mem.sliceTo(&tmp, 0); - if (isValidHostName(canon_name)) { - ctx.canon.items.len = 0; - try ctx.canon.appendSlice(gpa, canon_name); - } - }, - else => return, - } -} - -pub const Stream = struct { - /// Underlying platform-defined type which may or may not be - /// interchangeable with a file system file descriptor. - handle: Handle, - - pub const Handle = switch (native_os) { - .windows => windows.ws2_32.SOCKET, - else => posix.fd_t, - }; - - pub fn close(s: Stream) void { - switch (native_os) { - .windows => windows.closesocket(s.handle) catch unreachable, - else => posix.close(s.handle), - } - } - - pub const ReadError = posix.ReadError || error{ - SocketNotBound, - MessageTooBig, - NetworkSubsystemFailed, - ConnectionResetByPeer, - SocketUnconnected, - }; - - pub const WriteError = posix.SendMsgError || error{ - ConnectionResetByPeer, - SocketNotBound, - MessageTooBig, - NetworkSubsystemFailed, - SystemResources, - SocketUnconnected, - Unexpected, - }; - - pub const Reader = switch (native_os) { - .windows => struct { - /// Use `interface` for portable code. - interface_state: Io.Reader, - /// Use `getStream` for portable code. - net_stream: Stream, - /// Use `getError` for portable code. - error_state: ?Error, - - pub const Error = ReadError; - - pub fn getStream(r: *const Reader) Stream { - return r.net_stream; - } - - pub fn getError(r: *const Reader) ?Error { - return r.error_state; - } - - pub fn interface(r: *Reader) *Io.Reader { - return &r.interface_state; - } - - pub fn init(net_stream: Stream, buffer: []u8) Reader { - return .{ - .interface_state = .{ - .vtable = &.{ - .stream = stream, - .readVec = readVec, - }, - .buffer = buffer, - .seek = 0, - .end = 0, - }, - .net_stream = net_stream, - .error_state = null, - }; - } - - fn stream(io_r: *Io.Reader, io_w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { - const dest = limit.slice(try io_w.writableSliceGreedy(1)); - var bufs: [1][]u8 = .{dest}; - const n = try readVec(io_r, &bufs); - io_w.advance(n); - return n; - } - - fn readVec(io_r: *std.Io.Reader, data: [][]u8) Io.Reader.Error!usize { - const r: *Reader = @alignCast(@fieldParentPtr("interface_state", io_r)); - var iovecs: [max_buffers_len]windows.ws2_32.WSABUF = undefined; - const bufs_n, const data_size = try io_r.writableVectorWsa(&iovecs, data); - const bufs = iovecs[0..bufs_n]; - assert(bufs[0].len != 0); - const n = streamBufs(r, bufs) catch |err| { - r.error_state = err; - return error.ReadFailed; - }; - if (n == 0) return error.EndOfStream; - if (n > data_size) { - io_r.end += n - data_size; - return data_size; - } - return n; - } - - fn handleRecvError(winsock_error: windows.ws2_32.WinsockError) Error!void { - switch (winsock_error) { - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEFAULT => unreachable, // a pointer is not completely contained in user address space. - .WSAEINPROGRESS, .WSAEINTR => unreachable, // deprecated and removed in WSA 2.2 - .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // WSAStartup must be called before this function - .WSA_IO_PENDING => unreachable, - .WSA_OPERATION_ABORTED => unreachable, // not using overlapped I/O - else => |err| return windows.unexpectedWSAError(err), - } - } - - fn streamBufs(r: *Reader, bufs: []windows.ws2_32.WSABUF) Error!u32 { - var flags: u32 = 0; - var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); - - var n: u32 = undefined; - if (windows.ws2_32.WSARecv( - r.net_stream.handle, - bufs.ptr, - @intCast(bufs.len), - &n, - &flags, - &overlapped, - null, - ) == windows.ws2_32.SOCKET_ERROR) switch (windows.ws2_32.WSAGetLastError()) { - .WSA_IO_PENDING => { - var result_flags: u32 = undefined; - if (windows.ws2_32.WSAGetOverlappedResult( - r.net_stream.handle, - &overlapped, - &n, - windows.TRUE, - &result_flags, - ) == windows.FALSE) try handleRecvError(windows.ws2_32.WSAGetLastError()); - }, - else => |winsock_error| try handleRecvError(winsock_error), - }; - - return n; - } - }, - else => struct { - /// Use `getStream`, `interface`, and `getError` for portable code. - file_reader: File.Reader, - - pub const Error = ReadError; - - pub fn interface(r: *Reader) *Io.Reader { - return &r.file_reader.interface; - } - - pub fn init(net_stream: Stream, buffer: []u8) Reader { - return .{ - .file_reader = .{ - .interface = File.Reader.initInterface(buffer), - .file = .{ .handle = net_stream.handle }, - .mode = .streaming, - .seek_err = error.Unseekable, - .size_err = error.Streaming, - }, - }; - } - - pub fn getStream(r: *const Reader) Stream { - return .{ .handle = r.file_reader.file.handle }; - } - - pub fn getError(r: *const Reader) ?Error { - return r.file_reader.err; - } - }, - }; - - pub const Writer = switch (native_os) { - .windows => struct { - /// This field is present on all systems. - interface: Io.Writer, - /// Use `getStream` for cross-platform support. - stream: Stream, - /// This field is present on all systems. - err: ?Error = null, - - pub const Error = WriteError; - - pub fn init(stream: Stream, buffer: []u8) Writer { - return .{ - .stream = stream, - .interface = .{ - .vtable = &.{ .drain = drain }, - .buffer = buffer, - }, - }; - } - - pub fn getStream(w: *const Writer) Stream { - return w.stream; - } - - fn addWsaBuf(v: []windows.ws2_32.WSABUF, i: *u32, bytes: []const u8) void { - const cap = std.math.maxInt(u32); - var remaining = bytes; - while (remaining.len > cap) { - if (v.len - i.* == 0) return; - v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = cap }; - i.* += 1; - remaining = remaining[cap..]; - } else { - @branchHint(.likely); - if (v.len - i.* == 0) return; - v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = @intCast(remaining.len) }; - i.* += 1; - } - } - - fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - const buffered = io_w.buffered(); - comptime assert(native_os == .windows); - var iovecs: [max_buffers_len]windows.ws2_32.WSABUF = undefined; - var len: u32 = 0; - addWsaBuf(&iovecs, &len, buffered); - for (data[0 .. data.len - 1]) |bytes| addWsaBuf(&iovecs, &len, bytes); - const pattern = data[data.len - 1]; - if (iovecs.len - len != 0) switch (splat) { - 0 => {}, - 1 => addWsaBuf(&iovecs, &len, pattern), - else => switch (pattern.len) { - 0 => {}, - 1 => { - const splat_buffer_candidate = io_w.buffer[io_w.end..]; - var backup_buffer: [64]u8 = undefined; - const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len) - splat_buffer_candidate - else - &backup_buffer; - const memset_len = @min(splat_buffer.len, splat); - const buf = splat_buffer[0..memset_len]; - @memset(buf, pattern[0]); - addWsaBuf(&iovecs, &len, buf); - var remaining_splat = splat - buf.len; - while (remaining_splat > splat_buffer.len and len < iovecs.len) { - addWsaBuf(&iovecs, &len, splat_buffer); - remaining_splat -= splat_buffer.len; - } - addWsaBuf(&iovecs, &len, splat_buffer[0..remaining_splat]); - }, - else => for (0..@min(splat, iovecs.len - len)) |_| { - addWsaBuf(&iovecs, &len, pattern); - }, - }, - }; - const n = sendBufs(w.stream.handle, iovecs[0..len]) catch |err| { - w.err = err; - return error.WriteFailed; - }; - return io_w.consume(n); - } - - fn handleSendError(winsock_error: windows.ws2_32.WinsockError) Error!void { - switch (winsock_error) { - .WSAECONNABORTED => return error.ConnectionResetByPeer, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEFAULT => unreachable, // a pointer is not completely contained in user address space. - .WSAEINPROGRESS, .WSAEINTR => unreachable, // deprecated and removed in WSA 2.2 - .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENETDOWN => return error.NetworkSubsystemFailed, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAENOTSOCK => unreachable, // not a socket - .WSAEOPNOTSUPP => unreachable, // only for message-oriented sockets - .WSAESHUTDOWN => unreachable, // cannot send on a socket after write shutdown - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // WSAStartup must be called before this function - .WSA_IO_PENDING => unreachable, - .WSA_OPERATION_ABORTED => unreachable, // not using overlapped I/O - else => |err| return windows.unexpectedWSAError(err), - } - } - - fn sendBufs(handle: Stream.Handle, bufs: []windows.ws2_32.WSABUF) Error!u32 { - var n: u32 = undefined; - var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); - if (windows.ws2_32.WSASend( - handle, - bufs.ptr, - @intCast(bufs.len), - &n, - 0, - &overlapped, - null, - ) == windows.ws2_32.SOCKET_ERROR) switch (windows.ws2_32.WSAGetLastError()) { - .WSA_IO_PENDING => { - var result_flags: u32 = undefined; - if (windows.ws2_32.WSAGetOverlappedResult( - handle, - &overlapped, - &n, - windows.TRUE, - &result_flags, - ) == windows.FALSE) try handleSendError(windows.ws2_32.WSAGetLastError()); - }, - else => |winsock_error| try handleSendError(winsock_error), - }; - - return n; - } - }, - else => struct { - /// This field is present on all systems. - interface: Io.Writer, - - err: ?Error = null, - file_writer: File.Writer, - - pub const Error = WriteError; - - pub fn init(stream: Stream, buffer: []u8) Writer { - return .{ - .interface = .{ - .vtable = &.{ - .drain = drain, - .sendFile = sendFile, - }, - .buffer = buffer, - }, - .file_writer = .initStreaming(.{ .handle = stream.handle }, &.{}), - }; - } - - pub fn getStream(w: *const Writer) Stream { - return .{ .handle = w.file_writer.file.handle }; - } - - fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { - // OS checks ptr addr before length so zero length vectors must be omitted. - if (bytes.len == 0) return; - if (v.len - i.* == 0) return; - v[i.*] = .{ .base = bytes.ptr, .len = bytes.len }; - i.* += 1; - } - - fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - const buffered = io_w.buffered(); - var iovecs: [max_buffers_len]posix.iovec_const = undefined; - var msg: posix.msghdr_const = .{ - .name = null, - .namelen = 0, - .iov = &iovecs, - .iovlen = 0, - .control = null, - .controllen = 0, - .flags = 0, - }; - addBuf(&iovecs, &msg.iovlen, buffered); - for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &msg.iovlen, bytes); - const pattern = data[data.len - 1]; - if (iovecs.len - msg.iovlen != 0) switch (splat) { - 0 => {}, - 1 => addBuf(&iovecs, &msg.iovlen, pattern), - else => switch (pattern.len) { - 0 => {}, - 1 => { - const splat_buffer_candidate = io_w.buffer[io_w.end..]; - var backup_buffer: [64]u8 = undefined; - const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len) - splat_buffer_candidate - else - &backup_buffer; - const memset_len = @min(splat_buffer.len, splat); - const buf = splat_buffer[0..memset_len]; - @memset(buf, pattern[0]); - addBuf(&iovecs, &msg.iovlen, buf); - var remaining_splat = splat - buf.len; - while (remaining_splat > splat_buffer.len and iovecs.len - msg.iovlen != 0) { - assert(buf.len == splat_buffer.len); - addBuf(&iovecs, &msg.iovlen, splat_buffer); - remaining_splat -= splat_buffer.len; - } - addBuf(&iovecs, &msg.iovlen, splat_buffer[0..remaining_splat]); - }, - else => for (0..@min(splat, iovecs.len - msg.iovlen)) |_| { - addBuf(&iovecs, &msg.iovlen, pattern); - }, - }, - }; - const flags = posix.MSG.NOSIGNAL; - return io_w.consume(posix.sendmsg(w.file_writer.file.handle, &msg, flags) catch |err| { - w.err = err; - return error.WriteFailed; - }); - } - - fn sendFile(io_w: *Io.Writer, file_reader: *File.Reader, limit: Io.Limit) Io.Writer.FileError!usize { - const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); - const n = try w.file_writer.interface.sendFileHeader(io_w.buffered(), file_reader, limit); - return io_w.consume(n); - } - }, - }; - - pub fn reader(stream: Stream, buffer: []u8) Reader { - return .init(stream, buffer); - } - - pub fn writer(stream: Stream, buffer: []u8) Writer { - return .init(stream, buffer); - } - - const max_buffers_len = 8; - - /// Deprecated in favor of `Reader`. - pub fn read(self: Stream, buffer: []u8) ReadError!usize { - if (native_os == .windows) { - return windows.ReadFile(self.handle, buffer, null); - } - - return posix.read(self.handle, buffer); - } - - /// Deprecated in favor of `Reader`. - pub fn readv(s: Stream, iovecs: []const posix.iovec) ReadError!usize { - if (native_os == .windows) { - if (iovecs.len == 0) return 0; - const first = iovecs[0]; - return windows.ReadFile(s.handle, first.base[0..first.len], null); - } - - return posix.readv(s.handle, iovecs); - } - - /// Deprecated in favor of `Reader`. - pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize { - assert(len <= buffer.len); - var index: usize = 0; - while (index < len) { - const amt = try s.read(buffer[index..]); - if (amt == 0) break; - index += amt; - } - return index; - } - - /// Deprecated in favor of `Writer`. - pub fn write(self: Stream, buffer: []const u8) WriteError!usize { - var stream_writer = self.writer(&.{}); - return stream_writer.interface.writeVec(&.{buffer}) catch return stream_writer.err.?; - } - - /// Deprecated in favor of `Writer`. - pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void { - var index: usize = 0; - while (index < bytes.len) { - index += try self.write(bytes[index..]); - } - } - - /// Deprecated in favor of `Writer`. - pub fn writev(self: Stream, iovecs: []const posix.iovec_const) WriteError!usize { - return @errorCast(posix.writev(self.handle, iovecs)); - } - - /// Deprecated in favor of `Writer`. - pub fn writevAll(self: Stream, iovecs: []posix.iovec_const) WriteError!void { - if (iovecs.len == 0) return; - - var i: usize = 0; - while (true) { - var amt = try self.writev(iovecs[i..]); - while (amt >= iovecs[i].len) { - amt -= iovecs[i].len; - i += 1; - if (i >= iovecs.len) return; - } - iovecs[i].base += amt; - iovecs[i].len -= amt; - } - } -}; - -/// A bound, listening TCP socket, ready to accept new connections. -pub const Server = struct { - listen_address: Address, - stream: Stream, - - pub const Connection = struct { - stream: Stream, - address: Address, - }; - - pub fn deinit(s: *Server) void { - s.stream.close(); - s.* = undefined; - } - - pub const AcceptError = posix.AcceptError; - - /// Blocks until a client connects to the server. The returned `Connection` has - /// an open stream. - pub fn accept(s: *Server) AcceptError!Connection { - var accepted_addr: Address = undefined; - var addr_len: posix.socklen_t = @sizeOf(Address); - const fd = try posix.accept(s.stream.handle, &accepted_addr.any, &addr_len, posix.SOCK.CLOEXEC); - return .{ - .stream = .{ .handle = fd }, - .address = accepted_addr, - }; - } -}; - -test { - if (builtin.os.tag != .wasi) { - _ = Server; - _ = Stream; - _ = Address; - _ = @import("net/test.zig"); - } -} diff --git a/lib/std/std.zig b/lib/std/std.zig index 6853109f26..d1727ef3be 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -85,7 +85,6 @@ pub const macho = @import("macho.zig"); pub const math = @import("math.zig"); pub const mem = @import("mem.zig"); pub const meta = @import("meta.zig"); -pub const net = @import("net.zig"); pub const os = @import("os.zig"); pub const once = @import("once.zig").once; pub const pdb = @import("pdb.zig"); From 774df26835069039ba739828a7619393de01a5f2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 3 Oct 2025 16:15:37 -0700 Subject: [PATCH 075/244] WIP: hack at std.Io on a plane --- lib/std/Build/Cache.zig | 14 +- lib/std/Io.zig | 55 ++++-- lib/std/Io/Threaded.zig | 383 +++++++++++++++++++++++++++++++++------- lib/std/Io/net.zig | 53 ++++-- lib/std/Io/net/test.zig | 22 +-- lib/std/Thread.zig | 81 +-------- lib/std/fs/test.zig | 4 +- lib/std/http/test.zig | 156 ++++++++-------- lib/std/posix/test.zig | 2 +- lib/std/time.zig | 72 +------- 10 files changed, 513 insertions(+), 329 deletions(-) diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 14063001a2..fe9714296d 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -1317,6 +1317,8 @@ fn testGetCurrentFileTimestamp(dir: fs.Dir) !i128 { } test "cache file and then recall it" { + const io = std.testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1328,7 +1330,7 @@ test "cache file and then recall it" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - std.Thread.sleep(1); + try std.Io.Duration.sleep(.fromNanoseconds(1), io); } var digest1: HexDigest = undefined; @@ -1378,6 +1380,8 @@ test "cache file and then recall it" { } test "check that changing a file makes cache fail" { + const io = std.testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1391,7 +1395,7 @@ test "check that changing a file makes cache fail" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - std.Thread.sleep(1); + try std.Io.Duration.sleep(.fromNanoseconds(1), io); } var digest1: HexDigest = undefined; @@ -1490,6 +1494,8 @@ test "no file inputs" { } test "Manifest with files added after initial hash work" { + const io = std.testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1503,7 +1509,7 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - std.Thread.sleep(1); + try std.Io.Duration.sleep(.fromNanoseconds(1), io); } var digest1: HexDigest = undefined; @@ -1553,7 +1559,7 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time2 = try testGetCurrentFileTimestamp(tmp.dir); while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time2) { - std.Thread.sleep(1); + try std.Io.Duration.sleep(.fromNanoseconds(1), io); } { diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 4ae1e25c04..c3e003c725 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -670,8 +670,9 @@ pub const VTable = struct { listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, - ipBind: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, - netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) net.SendResult, + ipBind: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, + ipConnect: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream, + netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) struct { ?net.Socket.SendError, usize }, netReceive: *const fn (?*anyopaque, net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, net.ReceiveFlags, Timeout) struct { ?net.Socket.ReceiveTimeoutError, usize }, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, @@ -710,10 +711,14 @@ pub const Timestamp = struct { /// time (e.g., if the system administrator manually changes the /// clock), and by frequency adjust‐ ments performed by NTP and similar /// applications. - /// This clock normally counts the number of seconds since - /// 1970-01-01 00:00:00 Coordinated Universal Time (UTC) except that it - /// ignores leap seconds; near a leap second it is typically - /// adjusted by NTP to stay roughly in sync with UTC. + /// + /// This clock normally counts the number of seconds since 1970-01-01 + /// 00:00:00 Coordinated Universal Time (UTC) except that it ignores + /// leap seconds; near a leap second it is typically adjusted by NTP to + /// stay roughly in sync with UTC. + /// + /// The epoch is implementation-defined. For example NTFS/Windows uses + /// 1601-01-01. realtime, /// A nonsettable system-wide clock that represents time since some /// unspecified point in the past. @@ -729,10 +734,16 @@ pub const Timestamp = struct { /// Guarantees that the time returned by consecutive calls will not go /// backwards, but successive calls may return identical /// (not-increased) time values. + /// + /// May or may not include time the system is suspended, but + /// implementations should exclude that time if possible. monotonic, /// Identical to `monotonic` except it also includes any time that the - /// system is suspended. + /// system is suspended, if possible. However, it may be implemented + /// identically to `monotonic`. boottime, + process_cputime_id, + thread_cputime_id, }; pub fn durationTo(from: Timestamp, to: Timestamp) Duration { @@ -791,6 +802,12 @@ pub const Timestamp = struct { pub const Duration = struct { nanoseconds: i96, + pub const max: Duration = .{ .nanoseconds = std.math.maxInt(i96) }; + + pub fn fromNanoseconds(x: i96) Duration { + return .{ .nanoseconds = x }; + } + pub fn fromMilliseconds(x: i64) Duration { return .{ .nanoseconds = @as(i96, x) * std.time.ns_per_ms }; } @@ -806,6 +823,10 @@ pub const Duration = struct { pub fn toSeconds(d: Duration) i64 { return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_s)); } + + pub fn sleep(duration: Duration, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .monotonic } }); + } }; /// Declares under what conditions an operation should return `error.Timeout`. @@ -828,6 +849,18 @@ pub const Timeout = union(enum) { .deadline => |d| d, }; } + + pub fn toDurationFromNow(t: Timeout, io: Io) Timestamp.Error!?ClockAndDuration { + return switch (t) { + .none => null, + .duration => |d| d, + .deadline => |d| .{ .clock = d.clock, .duration = try d.durationFromNow(io) }, + }; + } + + pub fn sleep(timeout: Timeout, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, timeout); + } }; pub const AnyFuture = opaque {}; @@ -1322,14 +1355,6 @@ pub fn cancelRequested(io: Io) bool { pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable; -pub fn sleep(io: Io, timeout: Timeout) SleepError!void { - return io.vtable.sleep(io.userdata, timeout); -} - -pub fn sleepDuration(io: Io, duration: Duration) SleepError!void { - return io.vtable.sleep(io.userdata, .MONOTONIC, .{ .duration = duration }); -} - /// Given a struct with each field a `*Future`, returns a union with the same /// fields, each field type the future's result. pub fn SelectUnion(S: type) type { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 1957c7f210..93e110def3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -145,8 +145,17 @@ pub fn io(pool: *Pool) Io { .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, - .now = now, - .sleep = sleep, + .now = switch (builtin.os.tag) { + .windows => nowWindows, + .wasi => nowWasi, + else => nowPosix, + }, + .sleep = switch (builtin.os.tag) { + .windows => sleepWindows, + .wasi => sleepWasi, + .linux => sleepLinux, + else => sleepPosix, + }, .listen = switch (builtin.os.tag) { .windows => @panic("TODO"), @@ -160,6 +169,10 @@ pub fn io(pool: *Pool) Io { .windows => @panic("TODO"), else => ipBindPosix, }, + .ipConnect = switch (builtin.os.tag) { + .windows => @panic("TODO"), + else => ipConnectPosix, + }, .netClose = netClose, .netRead = switch (builtin.os.tag) { .windows => @panic("TODO"), @@ -797,16 +810,15 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File const dest = iovecs_buffer[0..i]; assert(dest[0].len > 0); - if (native_os == .wasi and !builtin.link_libc) { + if (native_os == .wasi and !builtin.link_libc) while (true) { try pool.checkCancel(); var nread: usize = undefined; switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) { .SUCCESS => return nread, - .INTR => unreachable, - .INVAL => unreachable, + .INTR => continue, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), - .AGAIN => unreachable, // currently not support in WASI - .BADF => return error.NotOpenForReading, // can be a race condition + .BADF => |err| return errnoBug(err), .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -817,15 +829,15 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .NOTCAPABLE => return error.AccessDenied, else => |err| return posix.unexpectedErrno(err), } - } + }; while (true) { try pool.checkCancel(); - const rc = posix.system.readv(file.handle, dest.ptr, dest.len); + const rc = posix.system.readv(file.handle, dest.ptr, @intCast(dest.len)); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), .INTR => continue, - .INVAL => unreachable, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, @@ -845,14 +857,6 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); - const have_pread_but_not_preadv = switch (native_os) { - .windows, .macos, .ios, .watchos, .tvos, .visionos, .haiku, .serenity => true, - else => false, - }; - if (have_pread_but_not_preadv) { - @compileError("TODO"); - } - if (is_windows) { const DWORD = windows.DWORD; const OVERLAPPED = windows.OVERLAPPED; @@ -907,6 +911,14 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset return total; } + const have_pread_but_not_preadv = switch (native_os) { + .windows, .haiku, .serenity => true, + else => false, + }; + if (have_pread_but_not_preadv) { + @compileError("TODO"); + } + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; for (data) |buf| { @@ -919,15 +931,15 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset const dest = iovecs_buffer[0..i]; assert(dest[0].len > 0); - if (native_os == .wasi and !builtin.link_libc) { + if (native_os == .wasi and !builtin.link_libc) while (true) { try pool.checkCancel(); var nread: usize = undefined; switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) { .SUCCESS => return nread, - .INTR => unreachable, - .INVAL => unreachable, + .INTR => continue, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), - .AGAIN => unreachable, + .AGAIN => |err| return errnoBug(err), .BADF => return error.NotOpenForReading, // can be a race condition .IO => return error.InputOutput, .ISDIR => return error.IsDir, @@ -942,16 +954,16 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .NOTCAPABLE => return error.AccessDenied, else => |err| return posix.unexpectedErrno(err), } - } + }; const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; while (true) { try pool.checkCancel(); - const rc = preadv_sym(file.handle, dest.ptr, dest.len, @bitCast(offset)); + const rc = preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset)); switch (posix.errno(rc)) { .SUCCESS => return @bitCast(rc), .INTR => continue, - .INVAL => unreachable, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, @@ -999,7 +1011,7 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posi }; } -fn now(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { +fn nowPosix(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const clock_id: posix.clockid_t = clockToPosix(clock); @@ -1011,7 +1023,35 @@ fn now(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 } } -fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { +fn nowWindows(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + _ = pool; + switch (clock) { + .realtime => { + // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds + // and uses the NTFS/Windows epoch, which is 1601-01-01. + return @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100; + }, + .monotonic, .boottime => { + // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. + return .{ .timestamp = windows.QueryPerformanceCounter() }; + }, + .process_cputime_id, + .thread_cputime_id, + => return error.UnsupportedClock, + } +} + +fn nowWasi(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + _ = pool; + var ns: std.os.wasi.timestamp_t = undefined; + const err = std.os.wasi.clock_time_get(clockToWasi(clock), 1, &ns); + if (err != .SUCCESS) return error.Unexpected; + return ns; +} + +fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); const clock_id: posix.clockid_t = clockToPosix(switch (timeout) { .none => .monotonic, @@ -1041,6 +1081,73 @@ fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } } +fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + const ms = ms: { + const duration_and_clock = (try timeout.toDurationFromNow(pool.io())) orelse + break :ms std.math.maxInt(windows.DWORD); + if (duration_and_clock.clock != .monotonic) return error.UnsupportedClock; + break :ms std.math.lossyCast(windows.DWORD, duration_and_clock.duration.toMilliseconds()); + }; + windows.kernel32.Sleep(ms); +} + +fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + const w = std.os.wasi; + + const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(pool.io())) |d| .{ + .id = clockToWasi(d.clock), + .timeout = std.math.lossyCast(u64, d.duration.nanoseconds), + .precision = 0, + .flags = 0, + } else .{ + .id = .MONOTONIC, + .timeout = std.math.maxInt(u64), + .precision = 0, + .flags = 0, + }; + const in: w.subscription_t = .{ + .userdata = 0, + .u = .{ + .tag = .CLOCK, + .u = .{ .clock = clock }, + }, + }; + var event: w.event_t = undefined; + var nevents: usize = undefined; + _ = w.poll_oneoff(&in, &event, 1, &nevents); +} + +fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type; + const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type; + + var timespec: posix.timespec = t: { + const d = (try timeout.toDurationFromNow(pool.io())) orelse break :t .{ + .sec = std.math.maxInt(sec_type), + .nsec = std.math.maxInt(nsec_type), + }; + if (d.clock != .monotonic) return error.UnsupportedClock; + const ns = d.duration.nanoseconds; + break :t .{ + .sec = @intCast(@divFloor(ns, std.time.ns_per_s)), + .nsec = @intCast(@mod(ns, std.time.ns_per_s)), + }; + }; + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.nanosleep(×pec, ×pec))) { + .INTR => continue, + else => return, // This prong handles success as well as unexpected errors. + } + } +} + fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; @@ -1091,7 +1198,7 @@ fn listenPosix( errdefer posix.close(fd); if (socket_flags_unsupported) while (true) { try pool.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC))) { + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, else => |err| return posix.unexpectedErrno(err), @@ -1158,6 +1265,37 @@ fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr } } +fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { + .SUCCESS => return, + .INTR => continue, + .ADDRINUSE => return error.AddressInUse, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .AGAIN, .INPROGRESS => |err| return errnoBug(err), + .ALREADY => return error.ConnectionPending, + .BADF => |err| return errnoBug(err), + .CONNREFUSED => return error.ConnectionRefused, + .CONNRESET => return error.ConnectionResetByPeer, + .FAULT => |err| return errnoBug(err), + .ISCONN => return error.AlreadyConnected, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTSOCK => |err| return errnoBug(err), + .PROTOTYPE => |err| return errnoBug(err), + .TIMEDOUT => return error.ConnectionTimedOut, + .CONNABORTED => |err| return errnoBug(err), + // UNIX socket error codes: + .ACCES => |err| return errnoBug(err), + .PERM => |err| return errnoBug(err), + .NOENT => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn posixGetSockName(pool: *Pool, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { while (true) { try pool.checkCancel(); @@ -1190,14 +1328,45 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio } } +fn ipConnectPosix( + userdata: ?*anyopaque, + address: *const Io.net.IpAddress, + options: Io.net.IpAddress.BindOptions, +) Io.net.IpAddress.ConnectError!Io.net.Stream { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_fd = try openSocketPosix(pool, family, options); + var storage: PosixAddress = undefined; + var addr_len = addressToPosix(address, &storage); + try posixConnect(pool, socket_fd, &storage.any, addr_len); + try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); + return .{ .socket = .{ + .handle = socket_fd, + .address = addressFromPosix(&storage), + } }; +} + fn ipBindPosix( userdata: ?*anyopaque, - address: Io.net.IpAddress, + address: *const Io.net.IpAddress, options: Io.net.IpAddress.BindOptions, ) Io.net.IpAddress.BindError!Io.net.Socket { const pool: *Pool = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_fd = try openSocketPosix(pool, family, options); + errdefer posix.close(socket_fd); + var storage: PosixAddress = undefined; + var addr_len = addressToPosix(address, &storage); + try posixBind(pool, socket_fd, &storage.any, addr_len); + try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); + return .{ + .handle = socket_fd, + .address = addressFromPosix(&storage), + }; +} + +fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: Io.net.IpAddress.BindOptions) !posix.socket_t { const mode = posixSocketMode(options.mode); - const family = posixAddressFamily(&address); const protocol = posixProtocol(options.protocol); const socket_fd = while (true) { try pool.checkCancel(); @@ -1209,7 +1378,7 @@ fn ipBindPosix( errdefer posix.close(fd); if (socket_flags_unsupported) while (true) { try pool.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC))) { + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, else => |err| return posix.unexpectedErrno(err), @@ -1229,19 +1398,14 @@ fn ipBindPosix( else => |err| return posix.unexpectedErrno(err), } }; + errdefer posix.close(socket_fd); if (options.ip6_only) { + if (posix.IPV6 == void) return error.OptionUnsupported; try setSocketOption(pool, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); } - var storage: PosixAddress = undefined; - var addr_len = addressToPosix(&address, &storage); - try posixBind(pool, socket_fd, &storage.any, addr_len); - try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); - return .{ - .handle = socket_fd, - .address = addressFromPosix(&storage), - }; + return socket_fd; } const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 @@ -1264,7 +1428,7 @@ fn acceptPosix(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.Acce errdefer posix.close(fd); if (!have_accept4) while (true) { try pool.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, posix.FD_CLOEXEC))) { + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, else => |err| return posix.unexpectedErrno(err), @@ -1322,29 +1486,118 @@ fn netSend( handle: Io.net.Socket.Handle, messages: []Io.net.OutgoingMessage, flags: Io.net.SendFlags, -) Io.net.SendResult { +) struct { ?Io.net.Socket.SendError, usize } { const pool: *Pool = @ptrCast(@alignCast(userdata)); - if (have_sendmmsg) { - var i: usize = 0; - while (messages.len - i != 0) { - i += netSendMany(pool, handle, messages[i..], flags) catch |err| return .{ .fail = .{ - .err = err, - .sent = i, - } }; - } - return .success; - } + const posix_flags: u32 = + @as(u32, if (flags.confirm) posix.MSG.CONFIRM else 0) | + @as(u32, if (flags.dont_route) posix.MSG.DONTROUTE else 0) | + @as(u32, if (flags.eor) posix.MSG.EOR else 0) | + @as(u32, if (flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (flags.fastopen) posix.MSG.FASTOPEN else 0) | + posix.MSG.NOSIGNAL; - try pool.checkCancel(); - @panic("TODO"); + var i: usize = 0; + while (messages.len - i != 0) { + if (have_sendmmsg) { + i += netSendMany(pool, handle, messages[i..], posix_flags) catch |err| return .{ err, i }; + continue; + } + netSendOne(pool, handle, &messages[i], posix_flags) catch |err| return .{ err, i }; + i += 1; + } + return .{ null, i }; +} + +fn netSendOne( + pool: *Pool, + handle: Io.net.Socket.Handle, + message: *Io.net.OutgoingMessage, + flags: u32, +) Io.net.Socket.SendError!void { + var addr: PosixAddress = undefined; + var iovec: posix.iovec = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + const msg: posix.msghdr = .{ + .name = &addr.any, + .namelen = addressToPosix(message.address, &addr), + .iov = iovec[0..1], + .iovlen = 1, + .control = @constCast(message.control.ptr), + .controllen = message.control.len, + .flags = 0, + }; + while (true) { + try pool.checkCancel(); + const rc = posix.system.sendmsg(handle, msg, flags); + if (is_windows) { + if (rc == windows.ws2_32.SOCKET_ERROR) { + switch (windows.ws2_32.WSAGetLastError()) { + .WSAEACCES => return error.AccessDenied, + .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, + .WSAECONNRESET => return error.ConnectionResetByPeer, + .WSAEMSGSIZE => return error.MessageTooBig, + .WSAENOBUFS => return error.SystemResources, + .WSAENOTSOCK => return error.FileDescriptorNotASocket, + .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, + .WSAEDESTADDRREQ => unreachable, // A destination address is required. + .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .WSAEHOSTUNREACH => return error.NetworkUnreachable, + // TODO: WSAEINPROGRESS, WSAEINTR + .WSAEINVAL => unreachable, + .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETRESET => return error.ConnectionResetByPeer, + .WSAENETUNREACH => return error.NetworkUnreachable, + .WSAENOTCONN => return error.SocketUnconnected, + .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. + .WSAEWOULDBLOCK => return error.WouldBlock, + .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + else => |err| return windows.unexpectedWSAError(err), + } + } else { + message.data_len = @intCast(rc); + return; + } + } + switch (posix.errno(rc)) { + .SUCCESS => { + message.data_len = @intCast(rc); + return; + }, + .ACCES => return error.AccessDenied, + .AGAIN => return error.WouldBlock, + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .MSGSIZE => return error.MessageTooBig, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), + .OPNOTSUPP => |err| return errnoBug(err), + .PIPE => return error.BrokenPipe, + .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .LOOP => return error.SymLinkLoop, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .HOSTUNREACH => return error.NetworkUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkSubsystemFailed, + else => |err| return posix.unexpectedErrno(err), + } + } } fn netSendMany( pool: *Pool, handle: Io.net.Socket.Handle, messages: []Io.net.OutgoingMessage, - flags: Io.net.SendFlags, + flags: u32, ) Io.net.Socket.SendError!usize { var msg_buffer: [64]std.os.linux.mmsghdr = undefined; var addr_buffer: [msg_buffer.len]PosixAddress = undefined; @@ -1371,17 +1624,9 @@ fn netSendMany( }; } - const posix_flags: u32 = - @as(u32, if (flags.confirm) posix.MSG.CONFIRM else 0) | - @as(u32, if (flags.dont_route) posix.MSG.DONTROUTE else 0) | - @as(u32, if (flags.eor) posix.MSG.EOR else 0) | - @as(u32, if (flags.oob) posix.MSG.OOB else 0) | - @as(u32, if (flags.fastopen) posix.MSG.FASTOPEN else 0) | - posix.MSG.NOSIGNAL; - while (true) { try pool.checkCancel(); - const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), posix_flags); + const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), flags); switch (posix.errno(rc)) { .SUCCESS => { for (clamped_messages[0..rc], clamped_msgs[0..rc]) |*message, *msg| { @@ -1782,5 +2027,17 @@ fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t { .realtime => posix.CLOCK.REALTIME, .monotonic => posix.CLOCK.MONOTONIC, .boottime => posix.CLOCK.BOOTTIME, + .process_cputime_id => posix.CLOCK.PROCESS_CPUTIME_ID, + .thread_cputime_id => posix.CLOCK.THREAD_CPUTIME_ID, + }; +} + +fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t { + return switch (clock) { + .realtime => .REALTIME, + .monotonic => .MONOTONIC, + .boottime => .MONOTONIC, + .process_cputime_id => .PROCESS_CPUTIME_ID, + .thread_cputime_id => .THREAD_CPUTIME_ID, }; } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 178aa75ca4..75e3f1fab7 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -208,6 +208,9 @@ pub const IpAddress = union(enum) { /// System-wide limit on the total number of open files has been reached. SystemFdQuotaExceeded, SocketModeUnsupported, + /// One of the `BindOptions` is not supported by the Io + /// implementation. + OptionUnsupported, } || Io.UnexpectedError || Io.Cancelable; pub const BindOptions = struct { @@ -228,6 +231,29 @@ pub const IpAddress = union(enum) { pub fn bind(address: IpAddress, io: Io, options: BindOptions) BindError!Socket { return io.vtable.ipBind(io.userdata, address, options); } + + pub const ConnectError = error{ + AddressInUse, + AddressUnavailable, + AddressFamilyUnsupported, + ConnectionPending, + ConnectionRefused, + ConnectionResetByPeer, + AlreadyConnected, + HostUnreachable, + NetworkUnreachable, + ConnectionTimedOut, + /// One of the `ConnectOptions` is not supported by the Io + /// implementation. + OptionUnsupported, + } || Io.UnexpectedError || Io.Cancelable; + + pub const ConnectOptions = BindOptions; + + /// Initiates a connection-oriented network stream. + pub fn connect(address: IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { + return io.vtable.ipConnect(io.userdata, address, options); + } }; /// An IPv4 address in binary memory layout. @@ -758,14 +784,6 @@ pub const SendFlags = packed struct(u8) { _: u3 = 0, }; -pub const SendResult = union(enum) { - success, - fail: struct { - err: Socket.SendError, - sent: usize, - }, -}; - pub const Interface = struct { /// Value 0 indicates `none`. index: u32, @@ -978,8 +996,9 @@ pub const Socket = struct { pub const Stream = struct { socket: Socket, - pub fn close(s: Stream, io: Io) void { - return io.vtable.netClose(io.userdata, s.socket); + pub fn close(s: *Stream, io: Io) void { + io.vtable.netClose(io.userdata, s.socket); + s.* = undefined; } pub const Reader = struct { @@ -996,8 +1015,9 @@ pub const Stream = struct { SocketUnconnected, } || Io.Cancelable || Io.Writer.Error || error{EndOfStream}; - pub fn init(stream: Stream, buffer: []u8) Reader { + pub fn init(stream: Stream, io: Io, buffer: []u8) Reader { return .{ + .io = io, .interface = .{ .vtable = &.{ .stream = streamImpl, @@ -1043,8 +1063,9 @@ pub const Stream = struct { Unexpected, } || Io.Cancelable; - pub fn init(stream: Stream, buffer: []u8) Writer { + pub fn init(stream: Stream, io: Io, buffer: []u8) Writer { return .{ + .io = io, .stream = stream, .interface = .{ .vtable = &.{ .drain = drain }, @@ -1062,12 +1083,12 @@ pub const Stream = struct { } }; - pub fn reader(stream: Stream, buffer: []u8) Reader { - return .init(stream, buffer); + pub fn reader(stream: Stream, io: Io, buffer: []u8) Reader { + return .init(stream, io, buffer); } - pub fn writer(stream: Stream, buffer: []u8) Writer { - return .init(stream, buffer); + pub fn writer(stream: Stream, io: Io, buffer: []u8) Writer { + return .init(stream, io, buffer); } }; diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 3e561d2db3..c1003c3a29 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -6,16 +6,16 @@ const testing = std.testing; test "parse and render IP addresses at comptime" { comptime { - const ipv6addr = net.IpAddress.parseIp("::1", 0) catch unreachable; + const ipv6addr = net.IpAddress.parse("::1", 0) catch unreachable; try std.testing.expectFmt("[::1]:0", "{f}", .{ipv6addr}); - const ipv4addr = net.IpAddress.parseIp("127.0.0.1", 0) catch unreachable; + const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable; try std.testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); - try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.parseIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.parseIp("127.01.0.1", 0)); - try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.resolveIp("::123.123.123.123", 0)); - try testing.expectError(error.InvalidIpAddressFormat, net.IpAddress.resolveIp("127.01.0.1", 0)); + try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0)); + try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0)); + try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp("::123.123.123.123", 0)); + try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp("127.01.0.1", 0)); } } @@ -161,8 +161,8 @@ test "resolve DNS" { // Resolve localhost, this should not fail. { - const localhost_v4 = try net.IpAddress.parseIp("127.0.0.1", 80); - const localhost_v6 = try net.IpAddress.parseIp("::2", 80); + const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80); + const localhost_v6 = try net.IpAddress.parse("::2", 80); const result = try net.getAddressList(testing.allocator, "localhost", 80); defer result.deinit(); @@ -198,7 +198,7 @@ test "listen on a port, send bytes, receive bytes" { // Try only the IPv4 variant as some CI builders have no IPv6 localhost // configured. - const localhost = try net.IpAddress.parseIp("127.0.0.1", 0); + const localhost = try net.IpAddress.parse("127.0.0.1", 0); var server = try localhost.listen(.{}); defer server.deinit(); @@ -232,7 +232,7 @@ test "listen on an in use port" { return error.SkipZigTest; } - const localhost = try net.IpAddress.parseIp("127.0.0.1", 0); + const localhost = try net.IpAddress.parse("127.0.0.1", 0); var server1 = try localhost.listen(.{ .reuse_address = true }); defer server1.deinit(); @@ -351,7 +351,7 @@ test "non-blocking tcp server" { return error.SkipZigTest; } - const localhost = try net.IpAddress.parseIp("127.0.0.1", 0); + const localhost = try net.IpAddress.parse("127.0.0.1", 0); var server = localhost.listen(.{ .force_nonblocking = true }); defer server.deinit(); diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 6da58e17bc..618f1209ef 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -73,10 +73,7 @@ pub const ResetEvent = enum(u32) { /// timedWait() returns without error. pub fn timedWait(re: *ResetEvent, timeout_ns: u64) error{Timeout}!void { if (builtin.single_threaded) switch (re.*) { - .unset => { - sleep(timeout_ns); - return error.Timeout; - }, + .unset => return error.Timeout, .waiting => unreachable, // Invalid state. .is_set => return, }; @@ -142,82 +139,6 @@ pub const ResetEvent = enum(u32) { } }; -/// Spurious wakeups are possible and no precision of timing is guaranteed. -pub fn sleep(nanoseconds: u64) void { - if (builtin.os.tag == .windows) { - const big_ms_from_ns = nanoseconds / std.time.ns_per_ms; - const ms = math.cast(windows.DWORD, big_ms_from_ns) orelse math.maxInt(windows.DWORD); - windows.kernel32.Sleep(ms); - return; - } - - if (builtin.os.tag == .wasi) { - const w = std.os.wasi; - const userdata: w.userdata_t = 0x0123_45678; - const clock: w.subscription_clock_t = .{ - .id = .MONOTONIC, - .timeout = nanoseconds, - .precision = 0, - .flags = 0, - }; - const in: w.subscription_t = .{ - .userdata = userdata, - .u = .{ - .tag = .CLOCK, - .u = .{ .clock = clock }, - }, - }; - - var event: w.event_t = undefined; - var nevents: usize = undefined; - _ = w.poll_oneoff(&in, &event, 1, &nevents); - return; - } - - if (builtin.os.tag == .uefi) { - const boot_services = std.os.uefi.system_table.boot_services.?; - const us_from_ns = nanoseconds / std.time.ns_per_us; - const us = math.cast(usize, us_from_ns) orelse math.maxInt(usize); - boot_services.stall(us) catch unreachable; - return; - } - - const s = nanoseconds / std.time.ns_per_s; - const ns = nanoseconds % std.time.ns_per_s; - - // Newer kernel ports don't have old `nanosleep()` and `clock_nanosleep()` has been around - // since Linux 2.6 and glibc 2.1 anyway. - if (builtin.os.tag == .linux) { - const linux = std.os.linux; - - var req: linux.timespec = .{ - .sec = std.math.cast(linux.time_t, s) orelse std.math.maxInt(linux.time_t), - .nsec = std.math.cast(linux.time_t, ns) orelse std.math.maxInt(linux.time_t), - }; - var rem: linux.timespec = undefined; - - while (true) { - switch (linux.E.init(linux.clock_nanosleep(.MONOTONIC, .{ .ABSTIME = false }, &req, &rem))) { - .SUCCESS => return, - .INTR => { - req = rem; - continue; - }, - .FAULT => unreachable, - .INVAL => unreachable, - .OPNOTSUPP => unreachable, - else => return, - } - } - } - - posix.nanosleep(s, ns); -} - -test sleep { - sleep(1); -} - const Thread = @This(); const Impl = if (native_os == .windows) WindowsThreadImpl diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 0949d71a2d..dee900f30e 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -2265,6 +2265,8 @@ test "seekTo flushes buffered data" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); + const io = std.testing.io; + const contents = "data"; const file = try tmp.dir.createFile("seek.bin", .{ .read = true }); @@ -2279,7 +2281,7 @@ test "seekTo flushes buffered data" { } var read_buffer: [16]u8 = undefined; - var file_reader: std.fs.File.Reader = .init(file, &read_buffer); + var file_reader: std.Io.File.Reader = .init(file, io, &read_buffer); var buf: [4]u8 = undefined; try file_reader.interface.readSliceAll(&buf); diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index fac241ede4..765e16003b 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -1,27 +1,31 @@ const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + const std = @import("std"); const http = std.http; const mem = std.mem; -const native_endian = builtin.cpu.arch.endian(); +const net = std.Io.net; +const Io = std.Io; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const expectEqualStrings = std.testing.expectEqualStrings; const expectError = std.testing.expectError; test "trailers" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [1024]u8 = undefined; var remaining: usize = 1; while (remaining != 0) : (remaining -= 1) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); try expectEqual(.ready, server.reader.state); var request = try server.receiveHead(); @@ -92,17 +96,18 @@ test "trailers" { } test "HTTP server handles a chunked transfer coding request" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [8192]u8 = undefined; var send_buffer: [500]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); try expect(request.head.transfer_encoding == .chunked); @@ -137,8 +142,8 @@ test "HTTP server handles a chunked transfer coding request" { "\r\n"; const gpa = std.testing.allocator; - const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); - defer stream.close(); + var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); + defer stream.close(io); var stream_writer = stream.writer(&.{}); try stream_writer.interface.writeAll(request_bytes); @@ -156,19 +161,20 @@ test "HTTP server handles a chunked transfer coding request" { } test "echo content server" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [100]u8 = undefined; accept: while (!test_server.shutting_down) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface); while (http_server.reader.state == .ready) { var request = http_server.receiveHead() catch |err| switch (err) { @@ -243,6 +249,8 @@ test "echo content server" { } test "Server.Request.respondStreaming non-chunked, unknown content-length" { + const io = std.testing.io; + if (builtin.os.tag == .windows) { // https://github.com/ziglang/zig/issues/21457 return error.SkipZigTest; @@ -250,19 +258,19 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { // In this case, the response is expected to stream until the connection is // closed, indicating the end of the body. - const test_server = try createTestServer(struct { + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1000]u8 = undefined; var send_buffer: [500]u8 = undefined; var remaining: usize = 1; while (remaining != 0) : (remaining -= 1) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); try expectEqual(.ready, server.reader.state); var request = try server.receiveHead(); @@ -287,8 +295,8 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { const request_bytes = "GET /foo HTTP/1.1\r\n\r\n"; const gpa = std.testing.allocator; - const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); - defer stream.close(); + var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); + defer stream.close(io); var stream_writer = stream.writer(&.{}); try stream_writer.interface.writeAll(request_bytes); @@ -316,19 +324,21 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { } test "receiving arbitrary http headers from the client" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [666]u8 = undefined; var send_buffer: [777]u8 = undefined; var remaining: usize = 1; while (remaining != 0) : (remaining -= 1) { - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); try expectEqual(.ready, server.reader.state); var request = try server.receiveHead(); @@ -357,8 +367,8 @@ test "receiving arbitrary http headers from the client" { "aoeu: asdf \r\n" ++ "\r\n"; const gpa = std.testing.allocator; - const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); - defer stream.close(); + var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); + defer stream.close(io); var stream_writer = stream.writer(&.{}); try stream_writer.interface.writeAll(request_bytes); @@ -376,24 +386,26 @@ test "receiving arbitrary http headers from the client" { } test "general client/server API coverage" { + const io = std.testing.io; + if (builtin.os.tag == .windows) { // This test was never passing on Windows. return error.SkipZigTest; } - const test_server = try createTestServer(struct { + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [100]u8 = undefined; outer: while (!test_server.shutting_down) { - var connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var http_server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface); while (http_server.reader.state == .ready) { var request = http_server.receiveHead() catch |err| switch (err) { @@ -530,7 +542,7 @@ test "general client/server API coverage" { } fn getUnusedTcpPort() !u16 { - const addr = try std.net.Address.parseIp("127.0.0.1", 0); + const addr = try net.IpAddress.parse("127.0.0.1", 0); var s = try addr.listen(.{}); defer s.deinit(); return s.listen_address.in.getPort(); @@ -867,18 +879,20 @@ test "general client/server API coverage" { } test "Server streams both reading and writing" { - const test_server = try createTestServer(struct { + const io = std.testing.io; + + const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [1024]u8 = undefined; var send_buffer: [777]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); var read_buffer: [100]u8 = undefined; var br = try request.readerExpectContinue(&read_buffer); @@ -1077,11 +1091,11 @@ fn echoTests(client: *http.Client, port: u16) !void { const TestServer = struct { shutting_down: bool, server_thread: std.Thread, - net_server: std.net.Server, + net_server: net.Server, fn destroy(self: *@This()) void { self.shutting_down = true; - const conn = std.net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure"); + const conn = net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure"); conn.close(); self.server_thread.join(); @@ -1090,21 +1104,21 @@ const TestServer = struct { } fn port(self: @This()) u16 { - return self.net_server.listen_address.in.getPort(); + return self.net_server.socket.address.getPort(); } }; -fn createTestServer(S: type) !*TestServer { +fn createTestServer(io: Io, S: type) !*TestServer { if (builtin.single_threaded) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and native_endian == .big) { // https://github.com/ziglang/zig/issues/13782 return error.SkipZigTest; } - const address = try std.net.Address.parseIp("127.0.0.1", 0); + const address = try net.IpAddress.parse("127.0.0.1", 0); const test_server = try std.testing.allocator.create(TestServer); test_server.* = .{ - .net_server = try address.listen(.{ .reuse_address = true }), + .net_server = try address.listen(io, .{ .reuse_address = true }), .shutting_down = false, .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}), }; @@ -1112,18 +1126,19 @@ fn createTestServer(S: type) !*TestServer { } test "redirect to different connection" { - const test_server_new = try createTestServer(struct { + const io = std.testing.io; + const test_server_new = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [888]u8 = undefined; var send_buffer: [777]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); try expectEqualStrings(request.head.target, "/ok"); try request.respond("good job, you pass", .{}); @@ -1136,23 +1151,23 @@ test "redirect to different connection" { }; global.other_port = test_server_new.port(); - const test_server_orig = try createTestServer(struct { + const test_server_orig = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void { const net_server = &test_server.net_server; var recv_buffer: [999]u8 = undefined; var send_buffer: [100]u8 = undefined; - const connection = try net_server.accept(); - defer connection.stream.close(); + var stream = try net_server.accept(io); + defer stream.close(io); var loc_buf: [50]u8 = undefined; const new_loc = try std.fmt.bufPrint(&loc_buf, "http://127.0.0.1:{d}/ok", .{ global.other_port.?, }); - var connection_br = connection.stream.reader(&recv_buffer); - var connection_bw = connection.stream.writer(&send_buffer); - var server = http.Server.init(connection_br.interface(), &connection_bw.interface); + var connection_br = stream.reader(io, &recv_buffer); + var connection_bw = stream.writer(io, &send_buffer); + var server = http.Server.init(&connection_br.interface, &connection_bw.interface); var request = try server.receiveHead(); try expectEqualStrings(request.head.target, "/help"); try request.respond("", .{ @@ -1167,7 +1182,10 @@ test "redirect to different connection" { const gpa = std.testing.allocator; - var client: http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ + .allocator = gpa, + .io = io, + }; defer client.deinit(); var loc_buf: [100]u8 = undefined; diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 87b101e1e9..f8206b8a5a 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -637,7 +637,7 @@ test "shutdown socket" { error.SocketUnconnected => {}, else => |e| return e, }; - std.net.Stream.close(.{ .handle = sock }); + std.posix.close(sock); } test "sigrtmin/max" { diff --git a/lib/std/time.zig b/lib/std/time.zig index 504257a852..b821775f4e 100644 --- a/lib/std/time.zig +++ b/lib/std/time.zig @@ -8,74 +8,6 @@ const posix = std.posix; pub const epoch = @import("time/epoch.zig"); -/// Get a calendar timestamp, in seconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn timestamp() i64 { - return @divFloor(milliTimestamp(), ms_per_s); -} - -/// Get a calendar timestamp, in milliseconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn milliTimestamp() i64 { - return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_ms))); -} - -/// Get a calendar timestamp, in microseconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn microTimestamp() i64 { - return @as(i64, @intCast(@divFloor(nanoTimestamp(), ns_per_us))); -} - -/// Get a calendar timestamp, in nanoseconds, relative to UTC 1970-01-01. -/// Precision of timing depends on the hardware and operating system. -/// On Windows this has a maximum granularity of 100 nanoseconds. -/// The return value is signed because it is possible to have a date that is -/// before the epoch. -/// See `posix.clock_gettime` for a POSIX timestamp. -pub fn nanoTimestamp() i128 { - switch (builtin.os.tag) { - .windows => { - // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds and uses the NTFS/Windows epoch, - // which is 1601-01-01. - const epoch_adj = epoch.windows * (ns_per_s / 100); - return @as(i128, windows.ntdll.RtlGetSystemTimePrecise() + epoch_adj) * 100; - }, - .wasi => { - var ns: std.os.wasi.timestamp_t = undefined; - const err = std.os.wasi.clock_time_get(.REALTIME, 1, &ns); - assert(err == .SUCCESS); - return ns; - }, - .uefi => { - const value, _ = std.os.uefi.system_table.runtime_services.getTime() catch return 0; - return value.toEpoch(); - }, - else => { - const ts = posix.clock_gettime(.REALTIME) catch |err| switch (err) { - error.UnsupportedClock, error.Unexpected => return 0, // "Precision of timing depends on hardware and OS". - }; - return (@as(i128, ts.sec) * ns_per_s) + ts.nsec; - }, - } -} - -test milliTimestamp { - const time_0 = milliTimestamp(); - std.Thread.sleep(ns_per_ms); - const time_1 = milliTimestamp(); - const interval = time_1 - time_0; - try testing.expect(interval > 0); -} - // Divisions of a nanosecond. pub const ns_per_us = 1000; pub const ns_per_ms = 1000 * ns_per_us; @@ -268,9 +200,11 @@ pub const Timer = struct { }; test Timer { + const io = std.testing.io; + var timer = try Timer.start(); - std.Thread.sleep(10 * ns_per_ms); + try std.Io.Duration.sleep(.fromMilliseconds(10), io); const time_0 = timer.read(); try testing.expect(time_0 > 0); From b428612a202a76f7a0aee18bde00c104753f3e60 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 5 Oct 2025 20:27:09 -0700 Subject: [PATCH 076/244] WIP: hack away at std.Io return flight --- lib/std/Io.zig | 49 +++++----- lib/std/Io/File.zig | 5 + lib/std/Io/Threaded.zig | 35 ++++--- lib/std/Io/net.zig | 37 ++++++-- lib/std/Io/net/HostName.zig | 64 ++++++++++--- lib/std/Io/net/test.zig | 177 ++++++++++++++++++------------------ lib/std/Uri.zig | 38 +++++--- lib/std/fs/test.zig | 2 +- lib/std/http/Client.zig | 124 +++++++++++-------------- lib/std/http/test.zig | 62 +++++++------ 10 files changed, 341 insertions(+), 252 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index c3e003c725..5f3c9811b8 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -719,31 +719,38 @@ pub const Timestamp = struct { /// /// The epoch is implementation-defined. For example NTFS/Windows uses /// 1601-01-01. - realtime, + real, /// A nonsettable system-wide clock that represents time since some /// unspecified point in the past. /// - /// On Linux, corresponds to how long the system has been running since - /// it booted. - /// - /// Not affected by discontinuous jumps in the system time (e.g., if - /// the system administrator manually changes the clock), but is - /// affected by frequency adjustments. **This clock does not count time - /// that the system is suspended.** - /// - /// Guarantees that the time returned by consecutive calls will not go - /// backwards, but successive calls may return identical + /// Monotonic: Guarantees that the time returned by consecutive calls + /// will not go backwards, but successive calls may return identical /// (not-increased) time values. /// - /// May or may not include time the system is suspended, but - /// implementations should exclude that time if possible. - monotonic, - /// Identical to `monotonic` except it also includes any time that the - /// system is suspended, if possible. However, it may be implemented - /// identically to `monotonic`. - boottime, - process_cputime_id, - thread_cputime_id, + /// Not affected by discontinuous jumps in the system time (e.g., if + /// the system administrator manually changes the clock), but may be + /// affected by frequency adjustments. + /// + /// This clock expresses intent to **exclude time that the system is + /// suspended**. However, implementations may be unable to satisify + /// this, and may include that time. + /// + /// * On Linux, corresponds `CLOCK_MONOTONIC`. + /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`. + awake, + /// Identical to `awake` except it expresses intent to include time + /// that the system is suspended, however, it may be implemented + /// identically to `awake`. + /// + /// * On Linux, corresponds `CLOCK_BOOTTIME`. + /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`. + boot, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// process. + cpu_process, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// thread. + cpu_thread, }; pub fn durationTo(from: Timestamp, to: Timestamp) Duration { @@ -825,7 +832,7 @@ pub const Duration = struct { } pub fn sleep(duration: Duration, io: Io) SleepError!void { - return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .monotonic } }); + return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .awake } }); } }; diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index a3be8f2c11..242d96d52f 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -319,6 +319,11 @@ pub const Reader = struct { }; } + /// Takes a legacy `std.fs.File` to help with upgrading. + pub fn initAdapted(file: std.fs.File, io: Io, buffer: []u8) Reader { + return .init(.{ .handle = file.handle }, io, buffer); + } + pub fn initSize(file: File, io: Io, buffer: []u8, size: ?u64) Reader { return .{ .io = io, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 93e110def3..67f3f553ec 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1032,7 +1032,7 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Err // and uses the NTFS/Windows epoch, which is 1601-01-01. return @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100; }, - .monotonic, .boottime => { + .monotonic, .uptime => { // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. return .{ .timestamp = windows.QueryPerformanceCounter() }; }, @@ -1132,7 +1132,8 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { .sec = std.math.maxInt(sec_type), .nsec = std.math.maxInt(nsec_type), }; - if (d.clock != .monotonic) return error.UnsupportedClock; + // TODO check which clock nanosleep uses on this host + // and return error.UnsupportedClock if it does not match const ns = d.duration.nanoseconds; break :t .{ .sec = @intCast(@divFloor(ns, std.time.ns_per_s)), @@ -1331,11 +1332,15 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio fn ipConnectPosix( userdata: ?*anyopaque, address: *const Io.net.IpAddress, - options: Io.net.IpAddress.BindOptions, + options: Io.net.IpAddress.ConnectOptions, ) Io.net.IpAddress.ConnectError!Io.net.Stream { + if (options.timeout != .none) @panic("TODO"); const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); - const socket_fd = try openSocketPosix(pool, family, options); + const socket_fd = try openSocketPosix(pool, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); var storage: PosixAddress = undefined; var addr_len = addressToPosix(address, &storage); try posixConnect(pool, socket_fd, &storage.any, addr_len); @@ -1490,11 +1495,11 @@ fn netSend( const pool: *Pool = @ptrCast(@alignCast(userdata)); const posix_flags: u32 = - @as(u32, if (flags.confirm) posix.MSG.CONFIRM else 0) | + @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) | @as(u32, if (flags.dont_route) posix.MSG.DONTROUTE else 0) | @as(u32, if (flags.eor) posix.MSG.EOR else 0) | @as(u32, if (flags.oob) posix.MSG.OOB else 0) | - @as(u32, if (flags.fastopen) posix.MSG.FASTOPEN else 0) | + @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) | posix.MSG.NOSIGNAL; var i: usize = 0; @@ -2024,11 +2029,17 @@ fn recoverableOsBugDetected() void { fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t { return switch (clock) { - .realtime => posix.CLOCK.REALTIME, - .monotonic => posix.CLOCK.MONOTONIC, - .boottime => posix.CLOCK.BOOTTIME, - .process_cputime_id => posix.CLOCK.PROCESS_CPUTIME_ID, - .thread_cputime_id => posix.CLOCK.THREAD_CPUTIME_ID, + .real => posix.CLOCK.REALTIME, + .awake => switch (builtin.os.tag) { + .macos, .ios, .watchos, .tvos => posix.CLOCK.UPTIME_RAW, + else => posix.CLOCK.MONOTONIC, + }, + .boot => switch (builtin.os.tag) { + .macos, .ios, .watchos, .tvos => posix.CLOCK.MONOTONIC_RAW, + else => posix.CLOCK.BOOTTIME, + }, + .cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID, + .cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID, }; } @@ -2036,7 +2047,7 @@ fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t { return switch (clock) { .realtime => .REALTIME, .monotonic => .MONOTONIC, - .boottime => .MONOTONIC, + .uptime => .MONOTONIC, .process_cputime_id => .PROCESS_CPUTIME_ID, .thread_cputime_id => .THREAD_CPUTIME_ID, }; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 75e3f1fab7..8fdc64987c 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -186,7 +186,7 @@ pub const IpAddress = union(enum) { /// Waits for a TCP connection. When using this API, `bind` does not need /// to be called. The returned `Server` has an open `stream`. pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { - return io.vtable.tcpListen(io.userdata, address, options); + return io.vtable.listen(io.userdata, address, options); } pub const BindError = error{ @@ -236,6 +236,8 @@ pub const IpAddress = union(enum) { AddressInUse, AddressUnavailable, AddressFamilyUnsupported, + /// Insufficient memory or other resource internal to the operating system. + SystemResources, ConnectionPending, ConnectionRefused, ConnectionResetByPeer, @@ -246,12 +248,23 @@ pub const IpAddress = union(enum) { /// One of the `ConnectOptions` is not supported by the Io /// implementation. OptionUnsupported, - } || Io.UnexpectedError || Io.Cancelable; + /// Per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// System-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + } || Io.Timeout.Error || Io.UnexpectedError || Io.Cancelable; - pub const ConnectOptions = BindOptions; + pub const ConnectOptions = struct { + mode: Socket.Mode, + protocol: ?Protocol = null, + timeout: Io.Timeout = .none, + }; /// Initiates a connection-oriented network stream. - pub fn connect(address: IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { + pub fn connect(address: *const IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { return io.vtable.ipConnect(io.userdata, address, options); } }; @@ -997,7 +1010,7 @@ pub const Stream = struct { socket: Socket, pub fn close(s: *Stream, io: Io) void { - io.vtable.netClose(io.userdata, s.socket); + io.vtable.netClose(io.userdata, s.socket.handle); s.* = undefined; } @@ -1040,10 +1053,13 @@ pub const Stream = struct { return n; } - fn readVec(io_r: *Reader, data: [][]u8) Io.Reader.Error!usize { + fn readVec(io_r: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); const io = r.io; - return io.vtable.netReadVec(io.vtable.userdata, r.stream, io_r, data); + return io.vtable.netRead(io.userdata, r.stream, data) catch |err| { + r.err = err; + return error.ReadFailed; + }; } }; @@ -1078,7 +1094,10 @@ pub const Stream = struct { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); const io = w.io; const buffered = io_w.buffered(); - const n = try io.vtable.netWrite(io.vtable.userdata, w.stream, buffered, data, splat); + const n = io.vtable.netWrite(io.userdata, w.stream, buffered, data, splat) catch |err| { + w.err = err; + return error.WriteFailed; + }; return io_w.consume(n); } }; @@ -1104,7 +1123,7 @@ pub const Server = struct { /// Blocks until a client connects to the server. pub fn accept(s: *Server, io: Io) AcceptError!Stream { - return io.vtable.accept(io, s); + return io.vtable.accept(io.userdata, s); } }; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index c8924e1fa6..d8c3a3a903 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -19,12 +19,12 @@ bytes: []const u8, pub const max_len = 255; -pub const InitError = error{ +pub const ValidateError = error{ NameTooLong, InvalidHostName, }; -pub fn init(bytes: []const u8) InitError!HostName { +pub fn validate(bytes: []const u8) ValidateError!void { if (bytes.len > max_len) return error.NameTooLong; if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; for (bytes) |byte| { @@ -33,10 +33,34 @@ pub fn init(bytes: []const u8) InitError!HostName { } return error.InvalidHostName; } +} + +pub fn init(bytes: []const u8) ValidateError!HostName { + try validate(bytes); return .{ .bytes = bytes }; } -/// TODO add a retry field here +pub fn sameParentDomain(parent_host: HostName, child_host: HostName) bool { + const parent_bytes = parent_host.bytes; + const child_bytes = child_host.bytes; + if (!std.ascii.endsWithIgnoreCase(child_bytes, parent_bytes)) return false; + if (child_bytes.len == parent_bytes.len) return true; + if (parent_bytes.len > child_bytes.len) return false; + return child_bytes[child_bytes.len - parent_bytes.len - 1] == '.'; +} + +test sameParentDomain { + try std.testing.expect(!sameParentDomain(try .init("foo.com"), try .init("bar.com"))); + try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("foo.com"))); + try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("bar.foo.com"))); + try std.testing.expect(!sameParentDomain(try .init("bar.foo.com"), try .init("foo.com"))); +} + +/// Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) +pub fn eql(a: HostName, b: HostName) bool { + return std.ascii.eqlIgnoreCase(a.bytes, b.bytes); +} + pub const LookupOptions = struct { port: u16, /// Must have at least length 2. @@ -266,15 +290,15 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio var answers_remaining = answers.len; for (answers) |*answer| answer.len = 0; - // boottime is chosen because time the computer is suspended should count + // boot clock is chosen because time the computer is suspended should count // against time spent waiting for external messages to arrive. - var now_ts = try Io.Timestamp.now(io, .boottime); + var now_ts = try Io.Timestamp.now(io, .boot); const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); const attempt_duration: Io.Duration = .{ .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, }; - send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boottime)) { + send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boot)) { const max_messages = queries_buffer.len * ResolvConf.max_nameservers; { var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; @@ -518,7 +542,7 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u return n; } -pub const ExpandError = error{InvalidDnsPacket} || InitError; +pub const ExpandError = error{InvalidDnsPacket} || ValidateError; /// Decompresses a DNS name. /// @@ -618,22 +642,36 @@ pub const DnsResponse = struct { } }; -pub const ConnectTcpError = LookupError || IpAddress.ConnectTcpError; +pub const ConnectError = LookupError || IpAddress.ConnectError; -pub fn connectTcp(host_name: HostName, io: Io, port: u16) ConnectTcpError!Stream { +pub fn connect( + host_name: HostName, + io: Io, + port: u16, + options: IpAddress.ConnectOptions, +) ConnectError!Stream { var addresses_buffer: [32]IpAddress = undefined; + var canonical_name_buffer: [HostName.max_len]u8 = undefined; - const results = try lookup(host_name, .{ + const results = try lookup(host_name, io, .{ .port = port, .addresses_buffer = &addresses_buffer, - .canonical_name_buffer = &.{}, + .canonical_name_buffer = &canonical_name_buffer, }); const addresses = addresses_buffer[0..results.addresses_len]; if (addresses.len == 0) return error.UnknownHostName; - for (addresses) |addr| { - return addr.connectTcp(io) catch |err| switch (err) { + // TODO instead of serially, use a Select API to send out + // the connections simultaneously and then keep the first + // successful one, canceling the rest. + + // TODO On Linux this should additionally use an Io.Queue based + // DNS resolution API in order to send out a connection after + // each DNS response before waiting for the rest of them. + + for (addresses) |*addr| { + return addr.connect(io, options) catch |err| switch (err) { error.ConnectionRefused => continue, else => |e| return e, }; diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index c1003c3a29..01c4b213a8 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -7,32 +7,30 @@ const testing = std.testing; test "parse and render IP addresses at comptime" { comptime { const ipv6addr = net.IpAddress.parse("::1", 0) catch unreachable; - try std.testing.expectFmt("[::1]:0", "{f}", .{ipv6addr}); + try testing.expectFmt("[::1]:0", "{f}", .{ipv6addr}); const ipv4addr = net.IpAddress.parse("127.0.0.1", 0) catch unreachable; - try std.testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); + try testing.expectFmt("127.0.0.1:0", "{f}", .{ipv4addr}); try testing.expectError(error.ParseFailed, net.IpAddress.parse("::123.123.123.123", 0)); try testing.expectError(error.ParseFailed, net.IpAddress.parse("127.01.0.1", 0)); - try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp("::123.123.123.123", 0)); - try testing.expectError(error.ParseFailed, net.IpAddress.resolveIp("127.01.0.1", 0)); } } test "format IPv6 address with no zero runs" { - const addr = try std.net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0); - try std.testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr}); + const addr = try net.IpAddress.parseIp6("2001:db8:1:2:3:4:5:6", 0); + try testing.expectFmt("[2001:db8:1:2:3:4:5:6]:0", "{f}", .{addr}); } test "parse IPv6 addresses and check compressed form" { - try std.testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{ - try std.net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0), + try testing.expectFmt("[2001:db8::1:0:0:2]:0", "{f}", .{ + try net.IpAddress.parseIp6("2001:0db8:0000:0000:0001:0000:0000:0002", 0), }); - try std.testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{ - try std.net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0), + try testing.expectFmt("[2001:db8::1:2]:0", "{f}", .{ + try net.IpAddress.parseIp6("2001:0db8:0000:0000:0000:0000:0001:0002", 0), }); - try std.testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{ - try std.net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0), + try testing.expectFmt("[2001:db8:1:0:1::2]:0", "{f}", .{ + try net.IpAddress.parseIp6("2001:0db8:0001:0000:0001:0000:0000:0002", 0), }); } @@ -43,14 +41,14 @@ test "parse IPv6 address, check raw bytes" { 0x00, 0x01, 0x00, 0x00, // :0001:0000 0x00, 0x00, 0x00, 0x02, // :0000:0002 }; - - const addr = try std.net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0); - - const actual_raw = addr.in6.sa.addr[0..]; - try std.testing.expectEqualSlices(u8, expected_raw[0..], actual_raw); + const addr = try net.IpAddress.parseIp6("2001:db8:0000:0000:0001:0000:0000:0002", 0); + try testing.expectEqualSlices(u8, &expected_raw, &addr.ip6.bytes); } test "parse and render IPv6 addresses" { + // TODO make this test parsing and rendering only, then it doesn't need I/O + const io = testing.io; + var buffer: [100]u8 = undefined; const ips = [_][]const u8{ "FF01:0:0:0:0:0:0:FB", @@ -79,12 +77,12 @@ test "parse and render IPv6 addresses" { for (ips, 0..) |ip, i| { const addr = net.IpAddress.parseIp6(ip, 0) catch unreachable; var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; - try std.testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3])); + try testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3])); if (builtin.os.tag == .linux) { - const addr_via_resolve = net.IpAddress.resolveIp6(ip, 0) catch unreachable; + const addr_via_resolve = net.IpAddress.resolveIp6(io, ip, 0) catch unreachable; var newResolvedIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr_via_resolve}) catch unreachable; - try std.testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3])); + try testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3])); } } @@ -97,21 +95,23 @@ test "parse and render IPv6 addresses" { try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("1", 0)); // TODO Make this test pass on other operating systems. if (builtin.os.tag == .linux or comptime builtin.os.tag.isDarwin() or builtin.os.tag == .windows) { - try testing.expectError(error.Incomplete, net.IpAddress.resolveIp6("ff01::fb%", 0)); + try testing.expectError(error.Incomplete, net.IpAddress.resolveIp6(io, "ff01::fb%", 0)); // Assumes IFNAMESIZE will always be a multiple of 2 - try testing.expectError(error.Overflow, net.IpAddress.resolveIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); - try testing.expectError(error.Overflow, net.IpAddress.resolveIp6("ff01::fb%12345678901234", 0)); + try testing.expectError(error.Overflow, net.IpAddress.resolveIp6(io, "ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); + try testing.expectError(error.Overflow, net.IpAddress.resolveIp6(io, "ff01::fb%12345678901234", 0)); } } test "invalid but parseable IPv6 scope ids" { + const io = testing.io; + if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) { // Currently, resolveIp6 with alphanumerical scope IDs only works on Linux. // TODO Make this test pass on other operating systems. return error.SkipZigTest; } - try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6("ff01::fb%123s45678901234", 0)); + try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6(io, "ff01::fb%123s45678901234", 0)); } test "parse and render IPv4 addresses" { @@ -125,7 +125,7 @@ test "parse and render IPv4 addresses" { }) |ip| { const addr = net.IpAddress.parseIp4(ip, 0) catch unreachable; var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; - try std.testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2])); + try testing.expect(std.mem.eql(u8, ip, newIp[0 .. newIp.len - 2])); } try testing.expectError(error.Overflow, net.IpAddress.parseIp4("256.0.0.1", 0)); @@ -136,50 +136,43 @@ test "parse and render IPv4 addresses" { try testing.expectError(error.NonCanonical, net.IpAddress.parseIp4("127.01.0.1", 0)); } -test "parse and render UNIX addresses" { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - if (!net.has_unix_sockets) return error.SkipZigTest; - - const addr = net.Address.initUnix("/tmp/testpath") catch unreachable; - try std.testing.expectFmt("/tmp/testpath", "{f}", .{addr}); - - const too_long = [_]u8{'a'} ** 200; - try testing.expectError(error.NameTooLong, net.Address.initUnix(too_long[0..])); -} - test "resolve DNS" { if (builtin.os.tag == .wasi) return error.SkipZigTest; - if (builtin.os.tag == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (builtin.os.tag == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } + const io = testing.io; // Resolve localhost, this should not fail. { const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80); const localhost_v6 = try net.IpAddress.parse("::2", 80); - const result = try net.getAddressList(testing.allocator, "localhost", 80); - defer result.deinit(); - for (result.addrs) |addr| { - if (addr.eql(localhost_v4) or addr.eql(localhost_v6)) break; + var addresses_buffer: [8]net.IpAddress = undefined; + var canon_name_buffer: [net.HostName.max_len]u8 = undefined; + const result = try net.HostName.lookup(try .init("localhost"), io, .{ + .port = 80, + .addresses_buffer = &addresses_buffer, + .canonical_name_buffer = &canon_name_buffer, + }); + for (addresses_buffer[0..result.addresses_len]) |addr| { + if (addr.eql(&localhost_v4) or addr.eql(&localhost_v6)) break; } else @panic("unexpected address for localhost"); } { // The tests are required to work even when there is no Internet connection, // so some of these errors we must accept and skip the test. - const result = net.getAddressList(testing.allocator, "example.com", 80) catch |err| switch (err) { + var addresses_buffer: [8]net.IpAddress = undefined; + var canon_name_buffer: [net.HostName.max_len]u8 = undefined; + const result = net.HostName.lookup(try .init("example.com"), io, .{ + .port = 80, + .addresses_buffer = &addresses_buffer, + .canonical_name_buffer = &canon_name_buffer, + }) catch |err| switch (err) { error.UnknownHostName => return error.SkipZigTest, - error.TemporaryNameServerFailure => return error.SkipZigTest, + error.NameServerFailure => return error.SkipZigTest, else => return err, }; - result.deinit(); + _ = result; } } @@ -187,6 +180,8 @@ test "listen on a port, send bytes, receive bytes" { if (builtin.single_threaded) return error.SkipZigTest; if (builtin.os.tag == .wasi) return error.SkipZigTest; + const io = testing.io; + if (builtin.os.tag == .windows) { _ = try std.os.windows.WSAStartup(2, 2); } @@ -198,28 +193,28 @@ test "listen on a port, send bytes, receive bytes" { // Try only the IPv4 variant as some CI builders have no IPv6 localhost // configured. - const localhost = try net.IpAddress.parse("127.0.0.1", 0); + const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; - var server = try localhost.listen(.{}); - defer server.deinit(); + var server = try localhost.listen(io, .{}); + defer server.deinit(io); const S = struct { fn clientFn(server_address: net.IpAddress) !void { - const socket = try net.tcpConnectToAddress(server_address); - defer socket.close(); + var stream = try server_address.connect(io, .{ .mode = .stream }); + defer stream.close(io); - var stream_writer = socket.writer(&.{}); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll("Hello world!"); } }; - const t = try std.Thread.spawn(.{}, S.clientFn, .{server.listen_address}); + const t = try std.Thread.spawn(.{}, S.clientFn, .{server.socket.address}); defer t.join(); - var client = try server.accept(); - defer client.stream.close(); + var client = try server.accept(io); + defer client.stream.close(io); var buf: [16]u8 = undefined; - var stream_reader = client.stream.reader(&.{}); + var stream_reader = client.stream.reader(io, &.{}); const n = try stream_reader.interface().readSliceShort(&buf); try testing.expectEqual(@as(usize, 12), n); @@ -232,13 +227,15 @@ test "listen on an in use port" { return error.SkipZigTest; } - const localhost = try net.IpAddress.parse("127.0.0.1", 0); + const io = testing.io; - var server1 = try localhost.listen(.{ .reuse_address = true }); - defer server1.deinit(); + const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; - var server2 = try server1.listen_address.listen(.{ .reuse_address = true }); - defer server2.deinit(); + var server1 = try localhost.listen(io, .{ .reuse_address = true }); + defer server1.deinit(io); + + var server2 = try server1.socket.address.listen(io, .{ .reuse_address = true }); + defer server2.deinit(io); } fn testClientToHost(allocator: mem.Allocator, name: []const u8, port: u16) anyerror!void { @@ -268,9 +265,11 @@ fn testClient(addr: net.IpAddress) anyerror!void { fn testServer(server: *net.Server) anyerror!void { if (builtin.os.tag == .wasi) return error.SkipZigTest; - var client = try server.accept(); + const io = testing.io; - const stream = client.stream.writer(); + var client = try server.accept(io); + + const stream = client.stream.writer(io); try stream.print("hello from server\n", .{}); } @@ -278,6 +277,8 @@ test "listen on a unix socket, send bytes, receive bytes" { if (builtin.single_threaded) return error.SkipZigTest; if (!net.has_unix_sockets) return error.SkipZigTest; + const io = testing.io; + if (builtin.os.tag == .windows) { _ = try std.os.windows.WSAStartup(2, 2); } @@ -293,15 +294,15 @@ test "listen on a unix socket, send bytes, receive bytes" { const socket_addr = try net.IpAddress.initUnix(socket_path); defer std.fs.cwd().deleteFile(socket_path) catch {}; - var server = try socket_addr.listen(.{}); - defer server.deinit(); + var server = try socket_addr.listen(io, .{}); + defer server.deinit(io); const S = struct { fn clientFn(path: []const u8) !void { - const socket = try net.connectUnixSocket(path); - defer socket.close(); + var stream = try net.connectUnixSocket(path); + defer stream.close(io); - var stream_writer = socket.writer(&.{}); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll("Hello world!"); } }; @@ -309,10 +310,10 @@ test "listen on a unix socket, send bytes, receive bytes" { const t = try std.Thread.spawn(.{}, S.clientFn, .{socket_path}); defer t.join(); - var client = try server.accept(); - defer client.stream.close(); + var client = try server.accept(io); + defer client.stream.close(io); var buf: [16]u8 = undefined; - var stream_reader = client.stream.reader(&.{}); + var stream_reader = client.stream.reader(io, &.{}); const n = try stream_reader.interface().readSliceShort(&buf); try testing.expectEqual(@as(usize, 12), n); @@ -324,14 +325,16 @@ test "listen on a unix socket with reuse_address option" { // Windows doesn't implement reuse port option. if (builtin.os.tag == .windows) return error.SkipZigTest; + const io = testing.io; + const socket_path = try generateFileName("socket.unix"); defer testing.allocator.free(socket_path); const socket_addr = try net.Address.initUnix(socket_path); defer std.fs.cwd().deleteFile(socket_path) catch {}; - var server = try socket_addr.listen(.{ .reuse_address = true }); - server.deinit(); + var server = try socket_addr.listen(io, .{ .reuse_address = true }); + server.deinit(io); } fn generateFileName(base_name: []const u8) ![]const u8 { @@ -351,19 +354,21 @@ test "non-blocking tcp server" { return error.SkipZigTest; } - const localhost = try net.IpAddress.parse("127.0.0.1", 0); - var server = localhost.listen(.{ .force_nonblocking = true }); - defer server.deinit(); + const io = testing.io; - const accept_err = server.accept(); + const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; + var server = localhost.listen(io, .{ .force_nonblocking = true }); + defer server.deinit(io); + + const accept_err = server.accept(io); try testing.expectError(error.WouldBlock, accept_err); - const socket_file = try net.tcpConnectToAddress(server.listen_address); + const socket_file = try net.tcpConnectToAddress(server.socket.address); defer socket_file.close(); - var client = try server.accept(); - defer client.stream.close(); - const stream = client.stream.writer(); + var client = try server.accept(io); + defer client.stream.close(io); + const stream = client.stream.writer(io); try stream.print("hello from server\n", .{}); var buf: [100]u8 = undefined; diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index d3df0491bb..46903ecf33 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -1,45 +1,48 @@ -//! Uniform Resource Identifier (URI) parsing roughly adhering to . -//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild. +//! Uniform Resource Identifier (URI) parsing roughly adhering to +//! . Does not do perfect grammar and +//! character class checking, but should be robust against URIs in the wild. const std = @import("std.zig"); const testing = std.testing; const Uri = @This(); const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; +const HostName = std.Io.net.HostName; scheme: []const u8, user: ?Component = null, password: ?Component = null, +/// If non-null, already validated. host: ?Component = null, port: ?u16 = null, path: Component = Component.empty, query: ?Component = null, fragment: ?Component = null, -pub const host_name_max = 255; +pub const GetHostError = error{UriMissingHost}; /// Returned value may point into `buffer` or be the original string. /// -/// Suggested buffer length: `host_name_max`. -/// /// See also: /// * `getHostAlloc` -pub fn getHost(uri: Uri, buffer: []u8) error{ UriMissingHost, UriHostTooLong }![]const u8 { +pub fn getHost(uri: Uri, buffer: *[HostName.max_len]u8) GetHostError!HostName { const component = uri.host orelse return error.UriMissingHost; - return component.toRaw(buffer) catch |err| switch (err) { - error.NoSpaceLeft => return error.UriHostTooLong, + const bytes = component.toRaw(buffer) catch |err| switch (err) { + error.NoSpaceLeft => unreachable, // `host` already validated. }; + return .{ .bytes = bytes }; } +pub const GetHostAllocError = GetHostError || error{OutOfMemory}; + /// Returned value may point into `buffer` or be the original string. /// /// See also: /// * `getHost` -pub fn getHostAlloc(uri: Uri, arena: Allocator) error{ UriMissingHost, UriHostTooLong, OutOfMemory }![]const u8 { +pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError![]const u8 { const component = uri.host orelse return error.UriMissingHost; - const result = try component.toRawMaybeAlloc(arena); - if (result.len > host_name_max) return error.UriHostTooLong; - return result; + const bytes = try component.toRawMaybeAlloc(arena); + return .{ .bytes = bytes }; } pub const Component = union(enum) { @@ -397,7 +400,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = new_parsed.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = new_parsed.host, + .host = try validateHost(new_parsed.host), .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -408,7 +411,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = host, + .host = try validateHost(host), .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -430,7 +433,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = base.user, .password = base.password, - .host = base.host, + .host = try validateHost(base.host), .port = base.port, .path = path, .query = query, @@ -438,6 +441,11 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE }; } +fn validateHost(bytes: []const u8) []const u8 { + try HostName.validate(bytes); + return bytes; +} + /// In-place implementation of RFC 3986, Section 5.2.4. fn remove_dot_segments(path: []u8) Component { var in_i: usize = 0; diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index dee900f30e..e9c341d39a 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -2281,7 +2281,7 @@ test "seekTo flushes buffered data" { } var read_buffer: [16]u8 = undefined; - var file_reader: std.Io.File.Reader = .init(file, io, &read_buffer); + var file_reader: std.Io.File.Reader = .initAdapted(file, io, &read_buffer); var buf: [4]u8 = undefined; try file_reader.interface.readSliceAll(&buf); diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 81bfbdcc2e..a701c09a90 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -15,6 +15,7 @@ const assert = std.debug.assert; const Io = std.Io; const Writer = std.Io.Writer; const Reader = std.Io.Reader; +const HostName = std.Io.net.HostName; const Client = @This(); @@ -69,7 +70,7 @@ pub const ConnectionPool = struct { /// The criteria for a connection to be considered a match. pub const Criteria = struct { - host: []const u8, + host: HostName, port: u16, protocol: Protocol, }; @@ -89,7 +90,7 @@ pub const ConnectionPool = struct { if (connection.port != criteria.port) continue; // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) - if (!std.ascii.eqlIgnoreCase(connection.host(), criteria.host)) continue; + if (!connection.host().eql(criteria.host)) continue; pool.acquireUnsafe(connection); return connection; @@ -118,19 +119,19 @@ pub const ConnectionPool = struct { /// If the connection is marked as closing, it will be closed instead. /// /// Threadsafe. - pub fn release(pool: *ConnectionPool, connection: *Connection) void { + pub fn release(pool: *ConnectionPool, connection: *Connection, io: Io) void { pool.mutex.lock(); defer pool.mutex.unlock(); pool.used.remove(&connection.pool_node); - if (connection.closing or pool.free_size == 0) return connection.destroy(); + if (connection.closing or pool.free_size == 0) return connection.destroy(io); if (pool.free_len >= pool.free_size) { const popped: *Connection = @alignCast(@fieldParentPtr("pool_node", pool.free.popFirst().?)); pool.free_len -= 1; - popped.destroy(); + popped.destroy(io); } if (connection.proxied) { @@ -178,21 +179,21 @@ pub const ConnectionPool = struct { /// All future operations on the connection pool will deadlock. /// /// Threadsafe. - pub fn deinit(pool: *ConnectionPool) void { + pub fn deinit(pool: *ConnectionPool, io: Io) void { pool.mutex.lock(); var next = pool.free.first; while (next) |node| { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); next = node.next; - connection.destroy(); + connection.destroy(io); } next = pool.used.first; while (next) |node| { const connection: *Connection = @alignCast(@fieldParentPtr("pool_node", node)); next = node.next; - connection.destroy(); + connection.destroy(io); } pool.* = undefined; @@ -242,19 +243,19 @@ pub const Connection = struct { fn create( client: *Client, - remote_host: []const u8, + remote_host: HostName, port: u16, stream: Io.net.Stream, ) error{OutOfMemory}!*Plain { const gpa = client.allocator; - const alloc_len = allocLen(client, remote_host.len); + const alloc_len = allocLen(client, remote_host.bytes.len); const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); errdefer gpa.free(base); - const host_buffer = base[@sizeOf(Plain)..][0..remote_host.len]; + const host_buffer = base[@sizeOf(Plain)..][0..remote_host.bytes.len]; const socket_read_buffer = host_buffer.ptr[host_buffer.len..][0..client.read_buffer_size]; const socket_write_buffer = socket_read_buffer.ptr[socket_read_buffer.len..][0..client.write_buffer_size]; assert(base.ptr + alloc_len == socket_write_buffer.ptr + socket_write_buffer.len); - @memcpy(host_buffer, remote_host); + @memcpy(host_buffer, remote_host.bytes); const plain: *Plain = @ptrCast(base); plain.* = .{ .connection = .{ @@ -263,7 +264,7 @@ pub const Connection = struct { .stream_reader = stream.reader(socket_read_buffer), .pool_node = .{}, .port = port, - .host_len = @intCast(remote_host.len), + .host_len = @intCast(remote_host.bytes.len), .proxied = false, .closing = false, .protocol = .plain, @@ -283,9 +284,9 @@ pub const Connection = struct { return @sizeOf(Plain) + host_len + client.read_buffer_size + client.write_buffer_size; } - fn host(plain: *Plain) []u8 { + fn host(plain: *Plain) HostName { const base: [*]u8 = @ptrCast(plain); - return base[@sizeOf(Plain)..][0..plain.connection.host_len]; + return .{ .bytes = base[@sizeOf(Plain)..][0..plain.connection.host_len] }; } }; @@ -295,15 +296,15 @@ pub const Connection = struct { fn create( client: *Client, - remote_host: []const u8, + remote_host: HostName, port: u16, stream: Io.net.Stream, ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { const gpa = client.allocator; - const alloc_len = allocLen(client, remote_host.len); + const alloc_len = allocLen(client, remote_host.bytes.len); const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); errdefer gpa.free(base); - const host_buffer = base[@sizeOf(Tls)..][0..remote_host.len]; + const host_buffer = base[@sizeOf(Tls)..][0..remote_host.bytes.len]; // The TLS client wants enough buffer for the max encrypted frame // size, and the HTTP body reader wants enough buffer for the // entire HTTP header. This means we need a combined upper bound. @@ -313,7 +314,7 @@ pub const Connection = struct { const socket_write_buffer = tls_write_buffer.ptr[tls_write_buffer.len..][0..client.write_buffer_size]; const socket_read_buffer = socket_write_buffer.ptr[socket_write_buffer.len..][0..client.tls_buffer_size]; assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len); - @memcpy(host_buffer, remote_host); + @memcpy(host_buffer, remote_host.bytes); const tls: *Tls = @ptrCast(base); tls.* = .{ .connection = .{ @@ -322,17 +323,17 @@ pub const Connection = struct { .stream_reader = stream.reader(socket_read_buffer), .pool_node = .{}, .port = port, - .host_len = @intCast(remote_host.len), + .host_len = @intCast(remote_host.bytes.len), .proxied = false, .closing = false, .protocol = .tls, }, // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true .client = std.crypto.tls.Client.init( - tls.connection.stream_reader.interface(), + &tls.connection.stream_reader.interface, &tls.connection.stream_writer.interface, .{ - .host = .{ .explicit = remote_host }, + .host = .{ .explicit = remote_host.bytes }, .ca = .{ .bundle = client.ca_bundle }, .ssl_key_log = client.ssl_key_log, .read_buffer = tls_read_buffer, @@ -359,9 +360,9 @@ pub const Connection = struct { client.write_buffer_size + client.tls_buffer_size; } - fn host(tls: *Tls) []u8 { + fn host(tls: *Tls) HostName { const base: [*]u8 = @ptrCast(tls); - return base[@sizeOf(Tls)..][0..tls.connection.host_len]; + return .{ .bytes = base[@sizeOf(Tls)..][0..tls.connection.host_len] }; } }; @@ -384,7 +385,7 @@ pub const Connection = struct { return c.stream_reader.stream; } - pub fn host(c: *Connection) []u8 { + pub fn host(c: *Connection) HostName { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; @@ -400,8 +401,8 @@ pub const Connection = struct { /// If this is called without calling `flush` or `end`, data will be /// dropped unsent. - pub fn destroy(c: *Connection) void { - c.getStream().close(); + pub fn destroy(c: *Connection, io: Io) void { + c.stream_reader.stream.close(io); switch (c.protocol) { .tls => { if (disable_tls) unreachable; @@ -437,7 +438,7 @@ pub const Connection = struct { const tls: *Tls = @alignCast(@fieldParentPtr("connection", c)); return &tls.client.reader; }, - .plain => c.stream_reader.interface(), + .plain => &c.stream_reader.interface, }; } @@ -866,6 +867,7 @@ pub const Request = struct { /// Returns the request's `Connection` back to the pool of the `Client`. pub fn deinit(r: *Request) void { + const io = r.client.io; if (r.connection) |connection| { connection.closing = connection.closing or switch (r.reader.state) { .ready => false, @@ -880,7 +882,7 @@ pub const Request = struct { }, else => true, }; - r.client.connection_pool.release(connection); + r.client.connection_pool.release(connection, io); } r.* = undefined; } @@ -1182,6 +1184,7 @@ pub const Request = struct { /// /// `aux_buf` must outlive accesses to `Request.uri`. fn redirect(r: *Request, head: *const Response.Head, aux_buf: *[]u8) !void { + const io = r.client.io; const new_location = head.location orelse return error.HttpRedirectLocationMissing; if (new_location.len > aux_buf.*.len) return error.HttpRedirectLocationOversize; const location = aux_buf.*[0..new_location.len]; @@ -1204,13 +1207,13 @@ pub const Request = struct { const protocol = Protocol.fromUri(new_uri) orelse return error.UnsupportedUriScheme; const old_connection = r.connection.?; const old_host = old_connection.host(); - var new_host_name_buffer: [Uri.host_name_max]u8 = undefined; + var new_host_name_buffer: [HostName.max_len]u8 = undefined; const new_host = try new_uri.getHost(&new_host_name_buffer); const keep_privileged_headers = std.ascii.eqlIgnoreCase(r.uri.scheme, new_uri.scheme) and - sameParentDomain(old_host, new_host); + old_host.sameParentDomain(new_host); - r.client.connection_pool.release(old_connection); + r.client.connection_pool.release(old_connection, io); r.connection = null; if (!keep_privileged_headers) { @@ -1266,7 +1269,7 @@ pub const Request = struct { pub const Proxy = struct { protocol: Protocol, - host: []const u8, + host: HostName, authorization: ?[]const u8, port: u16, supports_connect: bool, @@ -1277,9 +1280,10 @@ pub const Proxy = struct { /// All pending requests must be de-initialized and all active connections released /// before calling this function. pub fn deinit(client: *Client) void { + const io = client.io; assert(client.connection_pool.used.first == null); // There are still active requests. - client.connection_pool.deinit(); + client.connection_pool.deinit(io); if (!disable_tls) client.ca_bundle.deinit(client.allocator); client.* = undefined; @@ -1385,7 +1389,7 @@ pub const basic_authorization = struct { } }; -pub const ConnectTcpError = Allocator.Error || error{ +pub const ConnectTcpError = error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, @@ -1393,17 +1397,16 @@ pub const ConnectTcpError = Allocator.Error || error{ TemporaryNameServerFailure, NameServerFailure, UnknownHostName, - HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed, -}; +} || Allocator.Error || Io.Cancelable; /// Reuses a `Connection` if one matching `host` and `port` is already open. /// /// Threadsafe. pub fn connectTcp( client: *Client, - host: []const u8, + host: HostName, port: u16, protocol: Protocol, ) ConnectTcpError!*Connection { @@ -1411,16 +1414,17 @@ pub fn connectTcp( } pub const ConnectTcpOptions = struct { - host: Io.net.HostName, + host: HostName, port: u16, protocol: Protocol, - proxied_host: ?[]const u8 = null, + proxied_host: ?HostName = null, proxied_port: ?u16 = null, + timeout: Io.Timeout = .none, }; pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { - const host = options.host_name; + const host = options.host; const port = options.port; const protocol = options.protocol; @@ -1433,17 +1437,15 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp .protocol = protocol, })) |conn| return conn; - const stream = host.connectTcp(client.io, port) catch |err| switch (err) { + const stream = host.connect(client.io, port, .{ .mode = .stream }) catch |err| switch (err) { error.ConnectionRefused => return error.ConnectionRefused, error.NetworkUnreachable => return error.NetworkUnreachable, error.ConnectionTimedOut => return error.ConnectionTimedOut, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, - error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure, error.NameServerFailure => return error.NameServerFailure, error.UnknownHostName => return error.UnknownHostName, - error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses, error.Canceled => return error.Canceled, - else => return error.UnexpectedConnectFailure, + //else => return error.UnexpectedConnectFailure, }; errdefer stream.close(); @@ -1479,7 +1481,7 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti errdefer client.allocator.destroy(conn); conn.* = .{ .data = undefined }; - const stream = try std.net.connectUnixSocket(path); + const stream = try Io.net.connectUnixSocket(path); errdefer stream.close(); conn.data = .{ @@ -1504,9 +1506,10 @@ pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connecti pub fn connectProxied( client: *Client, proxy: *Proxy, - proxied_host: []const u8, + proxied_host: HostName, proxied_port: u16, ) !*Connection { + const io = client.io; if (!proxy.supports_connect) return error.TunnelNotSupported; if (client.connection_pool.findConnection(.{ @@ -1526,12 +1529,12 @@ pub fn connectProxied( }); errdefer { connection.closing = true; - client.connection_pool.release(connection); + client.connection_pool.release(connection, io); } var req = client.request(.CONNECT, .{ .scheme = "http", - .host = .{ .raw = proxied_host }, + .host = .{ .raw = proxied_host.bytes }, .port = proxied_port, }, .{ .redirect_behavior = .unhandled, @@ -1576,7 +1579,7 @@ pub const ConnectError = ConnectTcpError || RequestError; /// This function is threadsafe. pub fn connect( client: *Client, - host: []const u8, + host: HostName, port: u16, protocol: Protocol, ) ConnectError!*Connection { @@ -1586,9 +1589,7 @@ pub fn connect( } orelse return client.connectTcp(host, port, protocol); // Prevent proxying through itself. - if (std.ascii.eqlIgnoreCase(proxy.host, host) and - proxy.port == port and proxy.protocol == protocol) - { + if (proxy.host.eql(host) and proxy.port == port and proxy.protocol == protocol) { return client.connectTcp(host, port, protocol); } @@ -1608,7 +1609,6 @@ pub fn connect( pub const RequestError = ConnectTcpError || error{ UnsupportedUriScheme, UriMissingHost, - UriHostTooLong, CertificateBundleLoadFailure, }; @@ -1697,7 +1697,7 @@ pub fn request( } const connection = options.connection orelse c: { - var host_name_buffer: [Uri.host_name_max]u8 = undefined; + var host_name_buffer: [HostName.max_len]u8 = undefined; const host_name = try uri.getHost(&host_name_buffer); break :c try client.connect(host_name, uriPort(uri, protocol), protocol); }; @@ -1835,20 +1835,6 @@ pub fn fetch(client: *Client, options: FetchOptions) FetchError!FetchResult { return .{ .status = response.head.status }; } -pub fn sameParentDomain(parent_host: []const u8, child_host: []const u8) bool { - if (!std.ascii.endsWithIgnoreCase(child_host, parent_host)) return false; - if (child_host.len == parent_host.len) return true; - if (parent_host.len > child_host.len) return false; - return child_host[child_host.len - parent_host.len - 1] == '.'; -} - -test sameParentDomain { - try testing.expect(!sameParentDomain("foo.com", "bar.com")); - try testing.expect(sameParentDomain("foo.com", "foo.com")); - try testing.expect(sameParentDomain("foo.com", "bar.foo.com")); - try testing.expect(!sameParentDomain("bar.foo.com", "foo.com")); -} - test { _ = Response; } diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index 765e16003b..cb973993ec 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -53,7 +53,7 @@ test "trailers" { const gpa = std.testing.allocator; - var client: http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ .allocator = gpa, .io = io }; defer client.deinit(); const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/trailer", .{ @@ -141,12 +141,13 @@ test "HTTP server handles a chunked transfer coding request" { "0\r\n" ++ "\r\n"; - const gpa = std.testing.allocator; - var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); + const host_name: net.HostName = try .init("127.0.0.1"); + var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream }); defer stream.close(io); - var stream_writer = stream.writer(&.{}); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll(request_bytes); + const gpa = std.testing.allocator; const expected_response = "HTTP/1.1 200 OK\r\n" ++ "connection: close\r\n" ++ @@ -154,8 +155,8 @@ test "HTTP server handles a chunked transfer coding request" { "content-type: text/plain\r\n" ++ "\r\n" ++ "message from server!\n"; - var stream_reader = stream.reader(&.{}); - const response = try stream_reader.interface().allocRemaining(gpa, .limited(expected_response.len + 1)); + var stream_reader = stream.reader(io, &.{}); + const response = try stream_reader.interface.allocRemaining(gpa, .limited(expected_response.len + 1)); defer gpa.free(response); try expectEqualStrings(expected_response, response); } @@ -241,7 +242,7 @@ test "echo content server" { defer test_server.destroy(); { - var client: http.Client = .{ .allocator = std.testing.allocator }; + var client: http.Client = .{ .allocator = std.testing.allocator, .io = io }; defer client.deinit(); try echoTests(&client, test_server.port()); @@ -294,14 +295,15 @@ test "Server.Request.respondStreaming non-chunked, unknown content-length" { defer test_server.destroy(); const request_bytes = "GET /foo HTTP/1.1\r\n\r\n"; - const gpa = std.testing.allocator; - var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); + const host_name: net.HostName = try .init("127.0.0.1"); + var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream }); defer stream.close(io); - var stream_writer = stream.writer(&.{}); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll(request_bytes); - var stream_reader = stream.reader(&.{}); - const response = try stream_reader.interface().allocRemaining(gpa, .unlimited); + var stream_reader = stream.reader(io, &.{}); + const gpa = std.testing.allocator; + const response = try stream_reader.interface.allocRemaining(gpa, .unlimited); defer gpa.free(response); var expected_response = std.array_list.Managed(u8).init(gpa); @@ -366,14 +368,15 @@ test "receiving arbitrary http headers from the client" { "CoNneCtIoN:close\r\n" ++ "aoeu: asdf \r\n" ++ "\r\n"; - const gpa = std.testing.allocator; - var stream = try net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port()); + const host_name: net.HostName = try .init("127.0.0.1"); + var stream = try host_name.connect(io, test_server.port(), .{ .mode = .stream }); defer stream.close(io); - var stream_writer = stream.writer(&.{}); + var stream_writer = stream.writer(io, &.{}); try stream_writer.interface.writeAll(request_bytes); - var stream_reader = stream.reader(&.{}); - const response = try stream_reader.interface().allocRemaining(gpa, .unlimited); + var stream_reader = stream.reader(io, &.{}); + const gpa = std.testing.allocator; + const response = try stream_reader.interface.allocRemaining(gpa, .unlimited); defer gpa.free(response); var expected_response = std.array_list.Managed(u8).init(gpa); @@ -413,7 +416,7 @@ test "general client/server API coverage" { else => |e| return e, }; - try handleRequest(&request, net_server.listen_address.getPort()); + try handleRequest(&request, net_server.socket.address.getPort()); } } } @@ -543,9 +546,9 @@ test "general client/server API coverage" { fn getUnusedTcpPort() !u16 { const addr = try net.IpAddress.parse("127.0.0.1", 0); - var s = try addr.listen(.{}); - defer s.deinit(); - return s.listen_address.in.getPort(); + var s = try addr.listen(io, .{}); + defer s.deinit(io); + return s.socket.address.getPort(); } }); defer test_server.destroy(); @@ -553,7 +556,7 @@ test "general client/server API coverage" { const log = std.log.scoped(.client); const gpa = std.testing.allocator; - var client: http.Client = .{ .allocator = gpa }; + var client: http.Client = .{ .allocator = gpa, .io = io }; defer client.deinit(); const port = test_server.port(); @@ -918,7 +921,10 @@ test "Server streams both reading and writing" { }); defer test_server.destroy(); - var client: http.Client = .{ .allocator = std.testing.allocator }; + var client: http.Client = .{ + .allocator = std.testing.allocator, + .io = io, + }; defer client.deinit(); var redirect_buffer: [555]u8 = undefined; @@ -1089,17 +1095,20 @@ fn echoTests(client: *http.Client, port: u16) !void { } const TestServer = struct { + io: Io, shutting_down: bool, server_thread: std.Thread, net_server: net.Server, fn destroy(self: *@This()) void { + const io = self.io; self.shutting_down = true; - const conn = net.tcpConnectToAddress(self.net_server.listen_address) catch @panic("shutdown failure"); - conn.close(); + var stream = self.net_server.socket.address.connect(io, .{ .mode = .stream }) catch + @panic("shutdown failure"); + stream.close(io); self.server_thread.join(); - self.net_server.deinit(); + self.net_server.deinit(io); std.testing.allocator.destroy(self); } @@ -1118,6 +1127,7 @@ fn createTestServer(io: Io, S: type) !*TestServer { const address = try net.IpAddress.parse("127.0.0.1", 0); const test_server = try std.testing.allocator.create(TestServer); test_server.* = .{ + .io = io, .net_server = try address.listen(io, .{ .reuse_address = true }), .shutting_down = false, .server_thread = try std.Thread.spawn(.{}, S.run, .{test_server}), From 066864a0bf59bc1a926412b3c6e4d2d0c65e5642 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 6 Oct 2025 18:34:51 -0700 Subject: [PATCH 077/244] std.zig.system: upgrade to std.Io.Reader --- lib/compiler/build_runner.zig | 5 + lib/std/Build.zig | 8 +- lib/std/Build/Step/Options.zig | 4 +- lib/std/Build/WebServer.zig | 3 +- lib/std/Io.zig | 6 +- lib/std/Io/Threaded.zig | 13 +- lib/std/Io/net.zig | 2 +- lib/std/Target/Query.zig | 12 +- lib/std/elf.zig | 166 ++++++--- lib/std/zig.zig | 11 +- lib/std/zig/system.zig | 622 ++++++++++++--------------------- src/main.zig | 89 +++-- 12 files changed, 428 insertions(+), 513 deletions(-) diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 374bfa6ed3..523ef98824 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -38,6 +38,10 @@ pub fn main() !void { const args = try process.argsAlloc(arena); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + // skip my own exe name var arg_idx: usize = 1; @@ -68,6 +72,7 @@ pub fn main() !void { }; var graph: std.Build.Graph = .{ + .io = io, .arena = arena, .cache = .{ .gpa = arena, diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 9fd906e333..d3df0c0d39 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1,5 +1,7 @@ -const std = @import("std.zig"); const builtin = @import("builtin"); + +const std = @import("std.zig"); +const Io = std.Io; const fs = std.fs; const mem = std.mem; const debug = std.debug; @@ -110,6 +112,7 @@ pub const ReleaseMode = enum { /// Shared state among all Build instances. /// Settings that are here rather than in Build are not configurable per-package. pub const Graph = struct { + io: Io, arena: Allocator, system_library_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty, system_package_mode: bool = false, @@ -2666,9 +2669,10 @@ pub fn resolveTargetQuery(b: *Build, query: Target.Query) ResolvedTarget { // Hot path. This is faster than querying the native CPU and OS again. return b.graph.host; } + const io = b.graph.io; return .{ .query = query, - .result = std.zig.system.resolveTargetQuery(query) catch + .result = std.zig.system.resolveTargetQuery(io, query) catch @panic("unable to resolve target query"), }; } diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig index fd6194f7ff..d738b8edaa 100644 --- a/lib/std/Build/Step/Options.zig +++ b/lib/std/Build/Step/Options.zig @@ -532,6 +532,8 @@ const Arg = struct { test Options { if (builtin.os.tag == .wasi) return error.SkipZigTest; + const io = std.testing.io; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -546,7 +548,7 @@ test Options { .global_cache_root = .{ .path = "test", .handle = std.fs.cwd() }, .host = .{ .query = .{}, - .result = try std.zig.system.resolveTargetQuery(.{}), + .result = try std.zig.system.resolveTargetQuery(io, .{}), }, .zig_lib_directory = std.Build.Cache.Directory.cwd(), .time_report = false, diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index a67b96da03..135fbe7f0a 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -516,6 +516,7 @@ pub fn serveTarFile( } fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path { + const io = ws.graph.io; const root_name = "build-web"; const arch_os_abi = "wasm32-freestanding"; const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; @@ -659,7 +660,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim }; const bin_name = try std.zig.binNameAlloc(arena, .{ .root_name = root_name, - .target = &(std.zig.system.resolveTargetQuery(std.Build.parseTargetQuery(.{ + .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ .arch_os_abi = arch_os_abi, .cpu_features = cpu_features, }) catch unreachable) catch unreachable), diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 5f3c9811b8..5cdb9b0f01 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -738,9 +738,9 @@ pub const Timestamp = struct { /// * On Linux, corresponds `CLOCK_MONOTONIC`. /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`. awake, - /// Identical to `awake` except it expresses intent to include time - /// that the system is suspended, however, it may be implemented - /// identically to `awake`. + /// Identical to `awake` except it expresses intent to **include time + /// that the system is suspended**, however, due to limitations it may + /// behave identically to `awake`. /// /// * On Linux, corresponds `CLOCK_BOOTTIME`. /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 67f3f553ec..0727730b93 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1054,7 +1054,7 @@ fn nowWasi(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error! fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); const clock_id: posix.clockid_t = clockToPosix(switch (timeout) { - .none => .monotonic, + .none => .awake, .duration => |d| d.clock, .deadline => |d| d.clock, }); @@ -1087,7 +1087,6 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const ms = ms: { const duration_and_clock = (try timeout.toDurationFromNow(pool.io())) orelse break :ms std.math.maxInt(windows.DWORD); - if (duration_and_clock.clock != .monotonic) return error.UnsupportedClock; break :ms std.math.lossyCast(windows.DWORD, duration_and_clock.duration.toMilliseconds()); }; windows.kernel32.Sleep(ms); @@ -1132,8 +1131,6 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { .sec = std.math.maxInt(sec_type), .nsec = std.math.maxInt(nsec_type), }; - // TODO check which clock nanosleep uses on this host - // and return error.UnsupportedClock if it does not match const ns = d.duration.nanoseconds; break :t .{ .sec = @intCast(@divFloor(ns, std.time.ns_per_s)), @@ -2046,9 +2043,9 @@ fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t { fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t { return switch (clock) { .realtime => .REALTIME, - .monotonic => .MONOTONIC, - .uptime => .MONOTONIC, - .process_cputime_id => .PROCESS_CPUTIME_ID, - .thread_cputime_id => .THREAD_CPUTIME_ID, + .awake => .MONOTONIC, + .boot => .MONOTONIC, + .cpu_process => .PROCESS_CPUTIME_ID, + .cpu_thread => .THREAD_CPUTIME_ID, }; } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 8fdc64987c..e305145575 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -228,7 +228,7 @@ pub const IpAddress = union(enum) { /// /// One bound `Socket` can be used to receive messages from multiple /// different addresses. - pub fn bind(address: IpAddress, io: Io, options: BindOptions) BindError!Socket { + pub fn bind(address: *const IpAddress, io: Io, options: BindOptions) BindError!Socket { return io.vtable.ipBind(io.userdata, address, options); } diff --git a/lib/std/Target/Query.zig b/lib/std/Target/Query.zig index 90c127d800..f3f5155b06 100644 --- a/lib/std/Target/Query.zig +++ b/lib/std/Target/Query.zig @@ -612,6 +612,8 @@ fn versionEqualOpt(a: ?SemanticVersion, b: ?SemanticVersion) bool { } test parse { + const io = std.testing.io; + if (builtin.target.isGnuLibC()) { var query = try Query.parse(.{}); query.setGnuLibCVersion(2, 1, 1); @@ -654,7 +656,7 @@ test parse { .arch_os_abi = "x86_64-linux-gnu", .cpu_features = "x86_64-sse-sse2-avx-cx8", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.os.tag == .linux); try std.testing.expect(target.abi == .gnu); @@ -679,7 +681,7 @@ test parse { .arch_os_abi = "arm-linux-musleabihf", .cpu_features = "generic+v8a", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.os.tag == .linux); try std.testing.expect(target.abi == .musleabihf); @@ -696,7 +698,7 @@ test parse { .arch_os_abi = "aarch64-linux.3.10...4.4.1-gnu.2.27", .cpu_features = "generic+v8a", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.cpu.arch == .aarch64); try std.testing.expect(target.os.tag == .linux); @@ -719,7 +721,7 @@ test parse { const query = try Query.parse(.{ .arch_os_abi = "aarch64-linux.3.10...4.4.1-android.30", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.cpu.arch == .aarch64); try std.testing.expect(target.os.tag == .linux); @@ -740,7 +742,7 @@ test parse { const query = try Query.parse(.{ .arch_os_abi = "x86-windows.xp...win8-msvc", }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); try std.testing.expect(target.cpu.arch == .x86); try std.testing.expect(target.os.tag == .windows); diff --git a/lib/std/elf.zig b/lib/std/elf.zig index 3b0c085003..746d24f61a 100644 --- a/lib/std/elf.zig +++ b/lib/std/elf.zig @@ -1,9 +1,11 @@ //! Executable and Linkable Format. const std = @import("std.zig"); +const Io = std.Io; const math = std.math; const mem = std.mem; const assert = std.debug.assert; +const Endian = std.builtin.Endian; const native_endian = @import("builtin").target.cpu.arch.endian(); pub const AT_NULL = 0; @@ -568,7 +570,7 @@ pub const ET = enum(u16) { /// All integers are native endian. pub const Header = struct { is_64: bool, - endian: std.builtin.Endian, + endian: Endian, os_abi: OSABI, /// The meaning of this value depends on `os_abi`. abi_version: u8, @@ -583,48 +585,76 @@ pub const Header = struct { shnum: u16, shstrndx: u16, - pub fn iterateProgramHeaders(h: Header, file_reader: *std.fs.File.Reader) ProgramHeaderIterator { + pub fn iterateProgramHeaders(h: *const Header, file_reader: *Io.File.Reader) ProgramHeaderIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .phnum = h.phnum, + .phoff = h.phoff, .file_reader = file_reader, }; } - pub fn iterateProgramHeadersBuffer(h: Header, buf: []const u8) ProgramHeaderBufferIterator { + pub fn iterateProgramHeadersBuffer(h: *const Header, buf: []const u8) ProgramHeaderBufferIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .phnum = h.phnum, + .phoff = h.phoff, .buf = buf, }; } - pub fn iterateSectionHeaders(h: Header, file_reader: *std.fs.File.Reader) SectionHeaderIterator { + pub fn iterateSectionHeaders(h: *const Header, file_reader: *Io.File.Reader) SectionHeaderIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .shnum = h.shnum, + .shoff = h.shoff, .file_reader = file_reader, }; } - pub fn iterateSectionHeadersBuffer(h: Header, buf: []const u8) SectionHeaderBufferIterator { + pub fn iterateSectionHeadersBuffer(h: *const Header, buf: []const u8) SectionHeaderBufferIterator { return .{ - .elf_header = h, + .is_64 = h.is_64, + .endian = h.endian, + .shnum = h.shnum, + .shoff = h.shoff, .buf = buf, }; } - pub const ReadError = std.Io.Reader.Error || error{ + pub fn iterateDynamicSection( + h: *const Header, + file_reader: *Io.File.Reader, + offset: u64, + size: u64, + ) DynamicSectionIterator { + return .{ + .is_64 = h.is_64, + .endian = h.endian, + .offset = offset, + .end_offset = offset + size, + .file_reader = file_reader, + }; + } + + pub const ReadError = Io.Reader.Error || error{ InvalidElfMagic, InvalidElfVersion, InvalidElfClass, InvalidElfEndian, }; - pub fn read(r: *std.Io.Reader) ReadError!Header { + /// If this function fails, seek position of `r` is unchanged. + pub fn read(r: *Io.Reader) ReadError!Header { const buf = try r.peek(@sizeOf(Elf64_Ehdr)); if (!mem.eql(u8, buf[0..4], MAGIC)) return error.InvalidElfMagic; if (buf[EI.VERSION] != 1) return error.InvalidElfVersion; - const endian: std.builtin.Endian = switch (buf[EI.DATA]) { + const endian: Endian = switch (buf[EI.DATA]) { ELFDATA2LSB => .little, ELFDATA2MSB => .big, else => return error.InvalidElfEndian, @@ -637,7 +667,7 @@ pub const Header = struct { }; } - pub fn init(hdr: anytype, endian: std.builtin.Endian) Header { + pub fn init(hdr: anytype, endian: Endian) Header { // Converting integers to exhaustive enums using `@enumFromInt` could cause a panic. comptime assert(!@typeInfo(OSABI).@"enum".is_exhaustive); return .{ @@ -664,46 +694,54 @@ pub const Header = struct { }; pub const ProgramHeaderIterator = struct { - elf_header: Header, - file_reader: *std.fs.File.Reader, + is_64: bool, + endian: Endian, + phnum: u16, + phoff: u64, + + file_reader: *Io.File.Reader, index: usize = 0, pub fn next(it: *ProgramHeaderIterator) !?Elf64_Phdr { - if (it.index >= it.elf_header.phnum) return null; + if (it.index >= it.phnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); - const offset = it.elf_header.phoff + size * it.index; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); + const offset = it.phoff + size * it.index; try it.file_reader.seekTo(offset); - return takePhdr(&it.file_reader.interface, it.elf_header); + return takeProgramHeader(&it.file_reader.interface, it.is_64, it.endian); } }; pub const ProgramHeaderBufferIterator = struct { - elf_header: Header, + is_64: bool, + endian: Endian, + phnum: u16, + phoff: u64, + buf: []const u8, index: usize = 0, pub fn next(it: *ProgramHeaderBufferIterator) !?Elf64_Phdr { - if (it.index >= it.elf_header.phnum) return null; + if (it.index >= it.phnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); - const offset = it.elf_header.phoff + size * it.index; - var reader = std.Io.Reader.fixed(it.buf[offset..]); + const size: u64 = if (it.is_64) @sizeOf(Elf64_Phdr) else @sizeOf(Elf32_Phdr); + const offset = it.phoff + size * it.index; + var reader = Io.Reader.fixed(it.buf[offset..]); - return takePhdr(&reader, it.elf_header); + return takeProgramHeader(&reader, it.is_64, it.endian); } }; -fn takePhdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Phdr { - if (elf_header.is_64) { - const phdr = try reader.takeStruct(Elf64_Phdr, elf_header.endian); +pub fn takeProgramHeader(reader: *Io.Reader, is_64: bool, endian: Endian) !Elf64_Phdr { + if (is_64) { + const phdr = try reader.takeStruct(Elf64_Phdr, endian); return phdr; } - const phdr = try reader.takeStruct(Elf32_Phdr, elf_header.endian); + const phdr = try reader.takeStruct(Elf32_Phdr, endian); return .{ .p_type = phdr.p_type, .p_offset = phdr.p_offset, @@ -717,47 +755,55 @@ fn takePhdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Phdr { } pub const SectionHeaderIterator = struct { - elf_header: Header, - file_reader: *std.fs.File.Reader, + is_64: bool, + endian: Endian, + shnum: u16, + shoff: u64, + + file_reader: *Io.File.Reader, index: usize = 0, pub fn next(it: *SectionHeaderIterator) !?Elf64_Shdr { - if (it.index >= it.elf_header.shnum) return null; + if (it.index >= it.shnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); - const offset = it.elf_header.shoff + size * it.index; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); + const offset = it.shoff + size * it.index; try it.file_reader.seekTo(offset); - return takeShdr(&it.file_reader.interface, it.elf_header); + return takeSectionHeader(&it.file_reader.interface, it.is_64, it.endian); } }; pub const SectionHeaderBufferIterator = struct { - elf_header: Header, + is_64: bool, + endian: Endian, + shnum: u16, + shoff: u64, + buf: []const u8, index: usize = 0, pub fn next(it: *SectionHeaderBufferIterator) !?Elf64_Shdr { - if (it.index >= it.elf_header.shnum) return null; + if (it.index >= it.shnum) return null; defer it.index += 1; - const size: u64 = if (it.elf_header.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); - const offset = it.elf_header.shoff + size * it.index; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Shdr) else @sizeOf(Elf32_Shdr); + const offset = it.shoff + size * it.index; if (offset > it.buf.len) return error.EndOfStream; - var reader = std.Io.Reader.fixed(it.buf[@intCast(offset)..]); + var reader = Io.Reader.fixed(it.buf[@intCast(offset)..]); - return takeShdr(&reader, it.elf_header); + return takeSectionHeader(&reader, it.is_64, it.endian); } }; -fn takeShdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Shdr { - if (elf_header.is_64) { - const shdr = try reader.takeStruct(Elf64_Shdr, elf_header.endian); +pub fn takeSectionHeader(reader: *Io.Reader, is_64: bool, endian: Endian) !Elf64_Shdr { + if (is_64) { + const shdr = try reader.takeStruct(Elf64_Shdr, endian); return shdr; } - const shdr = try reader.takeStruct(Elf32_Shdr, elf_header.endian); + const shdr = try reader.takeStruct(Elf32_Shdr, endian); return .{ .sh_name = shdr.sh_name, .sh_type = shdr.sh_type, @@ -772,6 +818,36 @@ fn takeShdr(reader: *std.Io.Reader, elf_header: Header) !?Elf64_Shdr { }; } +pub const DynamicSectionIterator = struct { + is_64: bool, + endian: Endian, + offset: u64, + end_offset: u64, + + file_reader: *Io.File.Reader, + + pub fn next(it: *SectionHeaderIterator) !?Elf64_Dyn { + if (it.offset >= it.end_offset) return null; + const size: u64 = if (it.is_64) @sizeOf(Elf64_Dyn) else @sizeOf(Elf32_Dyn); + defer it.offset += size; + try it.file_reader.seekTo(it.offset); + return takeDynamicSection(&it.file_reader.interface, it.is_64, it.endian); + } +}; + +pub fn takeDynamicSection(reader: *Io.Reader, is_64: bool, endian: Endian) !Elf64_Dyn { + if (is_64) { + const dyn = try reader.takeStruct(Elf64_Dyn, endian); + return dyn; + } + + const dyn = try reader.takeStruct(Elf32_Dyn, endian); + return .{ + .d_tag = dyn.d_tag, + .d_val = dyn.d_val, + }; +} + pub const EI = struct { pub const CLASS = 4; pub const DATA = 5; diff --git a/lib/std/zig.zig b/lib/std/zig.zig index dfae03dea7..04e5c2b221 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -6,6 +6,7 @@ const std = @import("std.zig"); const tokenizer = @import("zig/tokenizer.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const Io = std.Io; const Writer = std.Io.Writer; pub const ErrorBundle = @import("zig/ErrorBundle.zig"); @@ -52,9 +53,9 @@ pub const Color = enum { /// Assume stderr is a terminal. on, - pub fn get_tty_conf(color: Color) std.Io.tty.Config { + pub fn get_tty_conf(color: Color) Io.tty.Config { return switch (color) { - .auto => std.Io.tty.detectConfig(std.fs.File.stderr()), + .auto => Io.tty.detectConfig(std.fs.File.stderr()), .on => .escape_codes, .off => .no_color, }; @@ -323,7 +324,7 @@ pub const BuildId = union(enum) { try std.testing.expectError(error.InvalidBuildIdStyle, parse("yaddaxxx")); } - pub fn format(id: BuildId, writer: *std.Io.Writer) std.Io.Writer.Error!void { + pub fn format(id: BuildId, writer: *Writer) Writer.Error!void { switch (id) { .none, .fast, .uuid, .sha1, .md5 => { try writer.writeAll(@tagName(id)); @@ -620,8 +621,8 @@ pub fn putAstErrorsIntoBundle( try wip_errors.addZirErrorMessages(zir, tree, tree.source, path); } -pub fn resolveTargetQueryOrFatal(target_query: std.Target.Query) std.Target { - return std.zig.system.resolveTargetQuery(target_query) catch |err| +pub fn resolveTargetQueryOrFatal(io: Io, target_query: std.Target.Query) std.Target { + return std.zig.system.resolveTargetQuery(io, target_query) catch |err| std.process.fatal("unable to resolve target: {s}", .{@errorName(err)}); } diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 47ff3d05fa..43d64205a7 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -1,3 +1,14 @@ +const builtin = @import("builtin"); +const std = @import("../std.zig"); +const mem = std.mem; +const elf = std.elf; +const fs = std.fs; +const assert = std.debug.assert; +const Target = std.Target; +const native_endian = builtin.cpu.arch.endian(); +const posix = std.posix; +const Io = std.Io; + pub const NativePaths = @import("system/NativePaths.zig"); pub const windows = @import("system/windows.zig"); @@ -199,14 +210,14 @@ pub const DetectError = error{ OSVersionDetectionFail, Unexpected, ProcessNotFound, -}; +} || Io.Cancelable; /// Given a `Target.Query`, which specifies in detail which parts of the /// target should be detected natively, which should be standard or default, /// and which are provided explicitly, this function resolves the native /// components by detecting the native system, and then resolves /// standard/default parts relative to that. -pub fn resolveTargetQuery(query: Target.Query) DetectError!Target { +pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { // Until https://github.com/ziglang/zig/issues/4592 is implemented (support detecting the // native CPU architecture as being different than the current target), we use this: const query_cpu_arch = query.cpu_arch orelse builtin.cpu.arch; @@ -411,7 +422,33 @@ pub fn resolveTargetQuery(query: Target.Query) DetectError!Target { query.cpu_features_sub, ); - var result = try detectAbiAndDynamicLinker(cpu, os, query); + var result = detectAbiAndDynamicLinker(io, cpu, os, query) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Unexpected => |e| return e, + error.WouldBlock => return error.Unexpected, + error.BrokenPipe => return error.Unexpected, + error.ConnectionResetByPeer => return error.Unexpected, + error.ConnectionTimedOut => return error.Unexpected, + error.NotOpenForReading => return error.Unexpected, + error.SocketUnconnected => return error.Unexpected, + + error.AccessDenied, + error.ProcessNotFound, + error.SymLinkLoop, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SystemResources, + error.IsDir, + error.DeviceBusy, + error.InputOutput, + error.LockViolation, + + error.UnableToOpenElfFile, + error.UnhelpfulFile, + error.InvalidElfFile, + error.RelativeShebang, + => return defaultAbiAndDynamicLinker(cpu, os, query), + }; // These CPU feature hacks have to come after ABI detection. { @@ -505,54 +542,16 @@ fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: T return null; } -pub const AbiAndDynamicLinkerFromFileError = error{ - FileSystem, - SystemResources, - SymLinkLoop, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - UnableToReadElfFile, - InvalidElfClass, - InvalidElfVersion, - InvalidElfEndian, - InvalidElfFile, - InvalidElfMagic, - Unexpected, - UnexpectedEndOfFile, - NameTooLong, - ProcessNotFound, - StaticElfFile, -}; +pub const AbiAndDynamicLinkerFromFileError = error{}; pub fn abiAndDynamicLinkerFromFile( - file: fs.File, + file_reader: *Io.File.Reader, + header: *const elf.Header, cpu: Target.Cpu, os: Target.Os, ld_info_list: []const LdInfo, query: Target.Query, ) AbiAndDynamicLinkerFromFileError!Target { - var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; - _ = try preadAtLeast(file, &hdr_buf, 0, hdr_buf.len); - const hdr32: *elf.Elf32_Ehdr = @ptrCast(&hdr_buf); - const hdr64: *elf.Elf64_Ehdr = @ptrCast(&hdr_buf); - if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; - const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI.DATA]) { - elf.ELFDATA2LSB => .little, - elf.ELFDATA2MSB => .big, - else => return error.InvalidElfEndian, - }; - const need_bswap = elf_endian != native_endian; - if (hdr32.e_ident[elf.EI.VERSION] != 1) return error.InvalidElfVersion; - - const is_64 = switch (hdr32.e_ident[elf.EI.CLASS]) { - elf.ELFCLASS32 => false, - elf.ELFCLASS64 => true, - else => return error.InvalidElfClass, - }; - var phoff = elfInt(is_64, need_bswap, hdr32.e_phoff, hdr64.e_phoff); - const phentsize = elfInt(is_64, need_bswap, hdr32.e_phentsize, hdr64.e_phentsize); - const phnum = elfInt(is_64, need_bswap, hdr32.e_phnum, hdr64.e_phnum); - var result: Target = .{ .cpu = cpu, .os = os, @@ -563,167 +562,87 @@ pub fn abiAndDynamicLinkerFromFile( var rpath_offset: ?u64 = null; // Found inside PT_DYNAMIC const look_for_ld = query.dynamic_linker.get() == null; - var ph_buf: [16 * @sizeOf(elf.Elf64_Phdr)]u8 align(@alignOf(elf.Elf64_Phdr)) = undefined; - if (phentsize > @sizeOf(elf.Elf64_Phdr)) return error.InvalidElfFile; - - var ph_i: u16 = 0; var got_dyn_section: bool = false; + { + var it = header.iterateProgramHeaders(file_reader); + while (try it.next()) |phdr| switch (phdr.p_type) { + elf.PT_INTERP => { + got_dyn_section = true; - while (ph_i < phnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const ph_reserve: usize = @sizeOf(elf.Elf64_Phdr) - @sizeOf(elf.Elf32_Phdr); - const ph_read_byte_len = try preadAtLeast(file, ph_buf[0 .. ph_buf.len - ph_reserve], phoff, phentsize); - var ph_buf_i: usize = 0; - while (ph_buf_i < ph_read_byte_len and ph_i < phnum) : ({ - ph_i += 1; - phoff += phentsize; - ph_buf_i += phentsize; - }) { - const ph32: *elf.Elf32_Phdr = @ptrCast(@alignCast(&ph_buf[ph_buf_i])); - const ph64: *elf.Elf64_Phdr = @ptrCast(@alignCast(&ph_buf[ph_buf_i])); - const p_type = elfInt(is_64, need_bswap, ph32.p_type, ph64.p_type); - switch (p_type) { - elf.PT_INTERP => { - got_dyn_section = true; + if (look_for_ld) { + const p_filesz = phdr.p_filesz; + if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; + const filesz: usize = @intCast(p_filesz); + try file_reader.seekTo(phdr.p_offset); + try file_reader.interface.readSliceAll(result.dynamic_linker.buffer[0..filesz]); + // PT_INTERP includes a null byte in filesz. + const len = filesz - 1; + // dynamic_linker.max_byte is "max", not "len". + // We know it will fit in u8 because we check against dynamic_linker.buffer.len above. + result.dynamic_linker.len = @intCast(len); - if (look_for_ld) { - const p_offset = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); - const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); - if (p_filesz > result.dynamic_linker.buffer.len) return error.NameTooLong; - const filesz: usize = @intCast(p_filesz); - _ = try preadAtLeast(file, result.dynamic_linker.buffer[0..filesz], p_offset, filesz); - // PT_INTERP includes a null byte in filesz. - const len = filesz - 1; - // dynamic_linker.max_byte is "max", not "len". - // We know it will fit in u8 because we check against dynamic_linker.buffer.len above. - result.dynamic_linker.len = @intCast(len); - - // Use it to determine ABI. - const full_ld_path = result.dynamic_linker.buffer[0..len]; - for (ld_info_list) |ld_info| { - const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); - if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) { - result.abi = ld_info.abi; - break; - } + // Use it to determine ABI. + const full_ld_path = result.dynamic_linker.buffer[0..len]; + for (ld_info_list) |ld_info| { + const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); + if (std.mem.endsWith(u8, full_ld_path, standard_ld_basename)) { + result.abi = ld_info.abi; + break; } } - }, - // We only need this for detecting glibc version. - elf.PT_DYNAMIC => { - got_dyn_section = true; + } + }, + // We only need this for detecting glibc version. + elf.PT_DYNAMIC => { + got_dyn_section = true; - if (builtin.target.os.tag == .linux and result.isGnuLibC() and - query.glibc_version == null) - { - var dyn_off = elfInt(is_64, need_bswap, ph32.p_offset, ph64.p_offset); - const p_filesz = elfInt(is_64, need_bswap, ph32.p_filesz, ph64.p_filesz); - const dyn_size: usize = if (is_64) @sizeOf(elf.Elf64_Dyn) else @sizeOf(elf.Elf32_Dyn); - const dyn_num = p_filesz / dyn_size; - var dyn_buf: [16 * @sizeOf(elf.Elf64_Dyn)]u8 align(@alignOf(elf.Elf64_Dyn)) = undefined; - var dyn_i: usize = 0; - dyn: while (dyn_i < dyn_num) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const dyn_reserve: usize = @sizeOf(elf.Elf64_Dyn) - @sizeOf(elf.Elf32_Dyn); - const dyn_read_byte_len = try preadAtLeast( - file, - dyn_buf[0 .. dyn_buf.len - dyn_reserve], - dyn_off, - dyn_size, - ); - var dyn_buf_i: usize = 0; - while (dyn_buf_i < dyn_read_byte_len and dyn_i < dyn_num) : ({ - dyn_i += 1; - dyn_off += dyn_size; - dyn_buf_i += dyn_size; - }) { - const dyn32: *elf.Elf32_Dyn = @ptrCast(@alignCast(&dyn_buf[dyn_buf_i])); - const dyn64: *elf.Elf64_Dyn = @ptrCast(@alignCast(&dyn_buf[dyn_buf_i])); - const tag = elfInt(is_64, need_bswap, dyn32.d_tag, dyn64.d_tag); - const val = elfInt(is_64, need_bswap, dyn32.d_val, dyn64.d_val); - if (tag == elf.DT_RUNPATH) { - rpath_offset = val; - break :dyn; - } - } + if (builtin.target.os.tag == .linux and result.isGnuLibC() and query.glibc_version == null) { + var dyn_it = header.iterateDynamicSection(file_reader, phdr.p_offset, phdr.p_filesz); + while (try dyn_it.next()) |dyn| { + if (dyn.d_tag == elf.DT_RUNPATH) { + rpath_offset = dyn.d_val; + break; } } - }, - else => continue, - } - } + } + }, + else => continue, + }; } if (!got_dyn_section) { return error.StaticElfFile; } - if (builtin.target.os.tag == .linux and result.isGnuLibC() and - query.glibc_version == null) - { - const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); - - var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); - const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); - const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); - - var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; - if (sh_buf.len < shentsize) return error.InvalidElfFile; - - _ = try preadAtLeast(file, &sh_buf, str_section_off, shentsize); - const shstr32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); - const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); - var strtab_buf: [4096:0]u8 = undefined; - const shstrtab_len = @min(shstrtab_size, strtab_buf.len); - const shstrtab_read_len = try preadAtLeast(file, &strtab_buf, shstrtab_off, shstrtab_len); - const shstrtab = strtab_buf[0..shstrtab_read_len]; - - const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); - var sh_i: u16 = 0; - const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); - const sh_read_byte_len = try preadAtLeast( - file, - sh_buf[0 .. sh_buf.len - sh_reserve], - shoff, - shentsize, - ); - var sh_buf_i: usize = 0; - while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ - sh_i += 1; - shoff += shentsize; - sh_buf_i += shentsize; - }) { - const sh32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); - const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 0); - if (mem.eql(u8, sh_name, ".dynstr")) { - break :find_dyn_str .{ - .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), - .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), - }; - } - } - } else null; - + if (builtin.target.os.tag == .linux and result.isGnuLibC() and query.glibc_version == null) { + const str_section_off = header.shoff + @as(u64, header.shentsize) * @as(u64, header.shstrndx); + try file_reader.seekTo(str_section_off); + const shstr = try elf.takeSectionHeader(&file_reader.interface, header.is_64, header.endian); + var strtab_buf: [4096]u8 = undefined; + const shstrtab = strtab_buf[0..@min(shstr.sh_size, strtab_buf.len)]; + try file_reader.seekTo(shstr.sh_offset); + try file_reader.interface.readSliceAll(shstrtab); + const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: { + var it = header.iterateSectionHeaders(&file_reader.interface); + while (it.next()) |shdr| { + const end = mem.findScalarPos(u8, shstrtab, shdr.sh_name, 0) orelse continue; + const sh_name = shstrtab[shdr.sh_name..end :0]; + if (mem.eql(u8, sh_name, ".dynstr")) break :find_dyn_str .{ + .offset = shdr.sh_offset, + .size = shdr.sh_size, + }; + } else break :find_dyn_str null; + }; if (dynstr) |ds| { if (rpath_offset) |rpoff| { if (rpoff > ds.size) return error.InvalidElfFile; const rpoff_file = ds.offset + rpoff; const rp_max_size = ds.size - rpoff; - const strtab_len = @min(rp_max_size, strtab_buf.len); - const strtab_read_len = try preadAtLeast(file, &strtab_buf, rpoff_file, strtab_len); - const strtab = strtab_buf[0..strtab_read_len]; + try file_reader.seekTo(rpoff_file); + const rpath_list = try file_reader.interface.takeSentinel(0); + if (rpath_list.len > rp_max_size) return error.StreamTooLong; - const rpath_list = mem.sliceTo(strtab, 0); var it = mem.tokenizeScalar(u8, rpath_list, ':'); while (it.next()) |rpath| { if (glibcVerFromRPath(rpath)) |ver| { @@ -845,7 +764,7 @@ test glibcVerFromLinkName { try std.testing.expectError(error.InvalidGnuLibCVersion, glibcVerFromLinkName("ld-2.37.4.5.so", "ld-")); } -fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion { +fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { error.NameTooLong => unreachable, error.InvalidUtf8 => unreachable, // WASI only @@ -879,7 +798,7 @@ fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion { // .dynstr section, and finding the max version number of symbols // that start with "GLIBC_2.". const glibc_so_basename = "libc.so.6"; - var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { + var file = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { error.NameTooLong => unreachable, error.InvalidUtf8 => unreachable, // WASI only error.InvalidWtf8 => unreachable, // Windows only @@ -913,16 +832,20 @@ fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion { error.Unexpected, => |e| return e, }; - defer f.close(); + defer file.close(); - return glibcVerFromSoFile(f) catch |err| switch (err) { + // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system. + var buffer: [8000]u8 = undefined; + var file_reader: Io.File.Reader = .initAdapted(file, io, &buffer); + + return glibcVerFromSoFile(&file_reader) catch |err| switch (err) { error.InvalidElfMagic, error.InvalidElfEndian, error.InvalidElfClass, error.InvalidElfFile, error.InvalidElfVersion, error.InvalidGnuLibCVersion, - error.UnexpectedEndOfFile, + error.EndOfStream, => return error.GLibCNotFound, error.SystemResources, @@ -934,88 +857,34 @@ fn glibcVerFromRPath(rpath: []const u8) !std.SemanticVersion { }; } -fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { - var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; - _ = try preadAtLeast(file, &hdr_buf, 0, hdr_buf.len); - const hdr32: *elf.Elf32_Ehdr = @ptrCast(&hdr_buf); - const hdr64: *elf.Elf64_Ehdr = @ptrCast(&hdr_buf); - if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; - const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI.DATA]) { - elf.ELFDATA2LSB => .little, - elf.ELFDATA2MSB => .big, - else => return error.InvalidElfEndian, +fn glibcVerFromSoFile(file_reader: *Io.File.Reader) !std.SemanticVersion { + const header = try elf.Header.read(&file_reader.interface); + const str_section_off = header.shoff + @as(u64, header.shentsize) * @as(u64, header.shstrndx); + try file_reader.seekTo(str_section_off); + const shstr = try elf.takeSectionHeader(&file_reader.interface, header.is_64, header.endian); + var strtab_buf: [4096]u8 = undefined; + const shstrtab = strtab_buf[0..@min(shstr.sh_size, strtab_buf.len)]; + try file_reader.seekTo(shstr.sh_offset); + try file_reader.interface.readSliceAll(shstrtab); + const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: { + var it = header.iterateSectionHeaders(&file_reader.interface); + while (it.next()) |shdr| { + const end = mem.findScalarPos(u8, shstrtab, shdr.sh_name, 0) orelse continue; + const sh_name = shstrtab[shdr.sh_name..end :0]; + if (mem.eql(u8, sh_name, ".dynstr")) break :find_dyn_str .{ + .offset = shdr.sh_offset, + .size = shdr.sh_size, + }; + } else return error.InvalidGnuLibCVersion; }; - const need_bswap = elf_endian != native_endian; - if (hdr32.e_ident[elf.EI.VERSION] != 1) return error.InvalidElfVersion; - - const is_64 = switch (hdr32.e_ident[elf.EI.CLASS]) { - elf.ELFCLASS32 => false, - elf.ELFCLASS64 => true, - else => return error.InvalidElfClass, - }; - const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); - var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); - const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); - const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); - var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; - if (sh_buf.len < shentsize) return error.InvalidElfFile; - - _ = try preadAtLeast(file, &sh_buf, str_section_off, shentsize); - const shstr32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstr64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf)); - const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); - const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); - var strtab_buf: [4096:0]u8 = undefined; - const shstrtab_len = @min(shstrtab_size, strtab_buf.len); - const shstrtab_read_len = try preadAtLeast(file, &strtab_buf, shstrtab_off, shstrtab_len); - const shstrtab = strtab_buf[0..shstrtab_read_len]; - const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); - var sh_i: u16 = 0; - const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); - const sh_read_byte_len = try preadAtLeast( - file, - sh_buf[0 .. sh_buf.len - sh_reserve], - shoff, - shentsize, - ); - var sh_buf_i: usize = 0; - while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ - sh_i += 1; - shoff += shentsize; - sh_buf_i += shentsize; - }) { - const sh32: *elf.Elf32_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh64: *elf.Elf64_Shdr = @ptrCast(@alignCast(&sh_buf[sh_buf_i])); - const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); - const sh_name = mem.sliceTo(shstrtab[sh_name_off..], 0); - if (mem.eql(u8, sh_name, ".dynstr")) { - break :find_dyn_str .{ - .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), - .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), - }; - } - } - } else return error.InvalidGnuLibCVersion; // Here we loop over all the strings in the dynstr string table, assuming that any // strings that start with "GLIBC_2." indicate the existence of such a glibc version, // and furthermore, that the system-installed glibc is at minimum that version. - - // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system. - // Here I use double this value plus some headroom. This makes it only need - // a single read syscall here. - var buf: [80000]u8 = undefined; - if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion; - - const dynstr_size: usize = @intCast(dynstr.size); - const dynstr_bytes = buf[0..dynstr_size]; - _ = try preadAtLeast(file, dynstr_bytes, dynstr.offset, dynstr_bytes.len); - var it = mem.splitScalar(u8, dynstr_bytes, 0); var max_ver: std.SemanticVersion = .{ .major = 2, .minor = 2, .patch = 5 }; - while (it.next()) |s| { + + try file_reader.seekTo(dynstr.offset); + while (file_reader.interface.takeSentinel(0)) |s| { if (mem.startsWith(u8, s, "GLIBC_2.")) { const chopped = s["GLIBC_".len..]; const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) { @@ -1028,6 +897,7 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { } } } + return max_ver; } @@ -1044,11 +914,7 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { /// answer to these questions, or if there is a shebang line, then it chases the referenced /// file recursively. If that does not provide the answer, then the function falls back to /// defaults. -fn detectAbiAndDynamicLinker( - cpu: Target.Cpu, - os: Target.Os, - query: Target.Query, -) DetectError!Target { +fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Target.Query) !Target { const native_target_has_ld = comptime Target.DynamicLinker.kind(builtin.os.tag) != .none; const is_linux = builtin.target.os.tag == .linux; const is_illumos = builtin.target.os.tag == .illumos; @@ -1111,49 +977,52 @@ fn detectAbiAndDynamicLinker( const ld_info_list = ld_info_list_buffer[0..ld_info_list_len]; + var file_reader: Io.File.Reader = undefined; + // According to `man 2 execve`: + // + // The kernel imposes a maximum length on the text + // that follows the "#!" characters at the start of a script; + // characters beyond the limit are ignored. + // Before Linux 5.1, the limit is 127 characters. + // Since Linux 5.1, the limit is 255 characters. + // + // Tests show that bash and zsh consider 255 as total limit, + // *including* "#!" characters and ignoring newline. + // For safety, we set max length as 255 + \n (1). + const max_shebang_line_size = 256; + var file_reader_buffer: [4096]u8 = undefined; + comptime assert(file_reader_buffer.len >= max_shebang_line_size); + // Best case scenario: the executable is dynamically linked, and we can iterate // over our own shared objects and find a dynamic linker. - const elf_file = elf_file: { - // This block looks for a shebang line in /usr/bin/env, - // if it finds one, then instead of using /usr/bin/env as the ELF file to examine, it uses the file it references instead, - // doing the same logic recursively in case it finds another shebang line. + const header = elf_file: { + // This block looks for a shebang line in "/usr/bin/env". If it finds + // one, then instead of using "/usr/bin/env" as the ELF file to examine, + // it uses the file it references instead, doing the same logic + // recursively in case it finds another shebang line. var file_name: []const u8 = switch (os.tag) { - // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a - // reasonably reliable path to start with. + // Since /usr/bin/env is hard-coded into the shebang line of many + // portable scripts, it's a reasonably reliable path to start with. else => "/usr/bin/env", // Haiku does not have a /usr root directory. .haiku => "/bin/env", }; - // According to `man 2 execve`: - // - // The kernel imposes a maximum length on the text - // that follows the "#!" characters at the start of a script; - // characters beyond the limit are ignored. - // Before Linux 5.1, the limit is 127 characters. - // Since Linux 5.1, the limit is 255 characters. - // - // Tests show that bash and zsh consider 255 as total limit, - // *including* "#!" characters and ignoring newline. - // For safety, we set max length as 255 + \n (1). - var buffer: [255 + 1]u8 = undefined; while (true) { - // Interpreter path can be relative on Linux, but - // for simplicity we are asserting it is an absolute path. const file = fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) { - error.NoSpaceLeft => unreachable, - error.NameTooLong => unreachable, - error.PathAlreadyExists => unreachable, - error.SharingViolation => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only - error.BadPathName => unreachable, - error.PipeBusy => unreachable, - error.FileLocksNotSupported => unreachable, - error.WouldBlock => unreachable, - error.FileBusy => unreachable, // opened without write permissions - error.AntivirusInterference => unreachable, // Windows-only error + error.NoSpaceLeft => return error.Unexpected, + error.NameTooLong => return error.Unexpected, + error.PathAlreadyExists => return error.Unexpected, + error.SharingViolation => return error.Unexpected, + error.InvalidUtf8 => return error.Unexpected, // WASI only + error.InvalidWtf8 => return error.Unexpected, // Windows only + error.BadPathName => return error.Unexpected, + error.PipeBusy => return error.Unexpected, + error.FileLocksNotSupported => return error.Unexpected, + error.WouldBlock => return error.Unexpected, + error.FileBusy => return error.Unexpected, // opened without write permissions + error.AntivirusInterference => return error.Unexpected, // Windows-only error error.IsDir, error.NotDir, @@ -1164,66 +1033,58 @@ fn detectAbiAndDynamicLinker( error.NetworkNotFound, error.FileTooBig, error.Unexpected, - => |e| { - std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)}); - return defaultAbiAndDynamicLinker(cpu, os, query); - }, + => return error.UnableToOpenElfFile, else => |e| return e, }; var is_elf_file = false; - defer if (is_elf_file == false) file.close(); + defer if (!is_elf_file) file.close(); - // Shortest working interpreter path is "#!/i" (4) - // (interpreter is "/i", assuming all paths are absolute, like in above comment). - // ELF magic number length is also 4. - // - // If file is shorter than that, it is definitely not ELF file - // nor file with "shebang" line. - const min_len: usize = 4; + file_reader = .initAdapted(file, io, &file_reader_buffer); + file_name = undefined; // it aliases file_reader_buffer - const len = preadAtLeast(file, &buffer, 0, min_len) catch |err| switch (err) { - error.UnexpectedEndOfFile, - error.UnableToReadElfFile, - error.ProcessNotFound, - => return defaultAbiAndDynamicLinker(cpu, os, query), + const header = elf.Header.read(&file_reader.interface) catch |hdr_err| switch (hdr_err) { + error.EndOfStream, + error.InvalidElfMagic, + => { + const shebang_line = file_reader.interface.takeSentinel('\n') catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + // It's neither an ELF file nor file with shebang line. + error.EndOfStream, error.StreamTooLong => return error.UnhelpfulFile, + }; + if (!mem.startsWith(u8, shebang_line, "#!")) return error.UnhelpfulFile; + // We detected shebang, now parse entire line. - else => |e| return e, + // Trim leading "#!", spaces and tabs. + const trimmed_line = mem.trimStart(u8, shebang_line[2..], &.{ ' ', '\t' }); + + // This line can have: + // * Interpreter path only, + // * Interpreter path and arguments, all separated by space, tab or NUL character. + // And optionally newline at the end. + const path_maybe_args = mem.trimEnd(u8, trimmed_line, "\n"); + + // Separate path and args. + const path_end = mem.indexOfAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len; + const unvalidated_path = path_maybe_args[0..path_end]; + file_name = if (fs.path.isAbsolute(unvalidated_path)) unvalidated_path else return error.RelativeShebang; + continue; + }, + + error.InvalidElfVersion, + error.InvalidElfClass, + error.InvalidElfEndian, + => return error.InvalidElfFile, + + error.ReadFailed => return file_reader.err.?, }; - const content = buffer[0..len]; - - if (mem.eql(u8, content[0..4], std.elf.MAGIC)) { - // It is very likely ELF file! - is_elf_file = true; - break :elf_file file; - } else if (mem.eql(u8, content[0..2], "#!")) { - // We detected shebang, now parse entire line. - - // Trim leading "#!", spaces and tabs. - const trimmed_line = mem.trimStart(u8, content[2..], &.{ ' ', '\t' }); - - // This line can have: - // * Interpreter path only, - // * Interpreter path and arguments, all separated by space, tab or NUL character. - // And optionally newline at the end. - const path_maybe_args = mem.trimEnd(u8, trimmed_line, "\n"); - - // Separate path and args. - const path_end = mem.indexOfAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len; - - file_name = path_maybe_args[0..path_end]; - continue; - } else { - // Not a ELF file, not a shell script with "shebang line", invalid duck. - return defaultAbiAndDynamicLinker(cpu, os, query); - } + is_elf_file = true; + break :elf_file header; } }; - defer elf_file.close(); + defer file_reader.file.close(io); - // TODO: inline this function and combine the buffer we already read above to find - // the possible shebang line with the buffer we use for the ELF header. - return abiAndDynamicLinkerFromFile(elf_file, cpu, os, ld_info_list, query) catch |err| switch (err) { + return abiAndDynamicLinkerFromFile(&file_reader, &header, cpu, os, ld_info_list, query) catch |err| switch (err) { error.FileSystem, error.SystemResources, error.SymLinkLoop, @@ -1232,6 +1093,8 @@ fn detectAbiAndDynamicLinker( error.ProcessNotFound, => |e| return e, + error.ReadFailed => return file_reader.err.?, + error.UnableToReadElfFile, error.InvalidElfClass, error.InvalidElfVersion, @@ -1239,12 +1102,12 @@ fn detectAbiAndDynamicLinker( error.InvalidElfFile, error.InvalidElfMagic, error.Unexpected, - error.UnexpectedEndOfFile, + error.EndOfStream, error.NameTooLong, error.StaticElfFile, // Finally, we fall back on the standard path. => |e| { - std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)}); + std.log.warn("encountered {t}; falling back to default ABI and dynamic linker", .{e}); return defaultAbiAndDynamicLinker(cpu, os, query); }, }; @@ -1269,59 +1132,6 @@ const LdInfo = struct { abi: Target.Abi, }; -fn preadAtLeast(file: fs.File, buf: []u8, offset: u64, min_read_len: usize) !usize { - var i: usize = 0; - while (i < min_read_len) { - const len = file.pread(buf[i..], offset + i) catch |err| switch (err) { - error.OperationAborted => unreachable, // Windows-only - error.WouldBlock => unreachable, // Did not request blocking mode - error.Canceled => unreachable, // timerfd is unseekable - error.NotOpenForReading => unreachable, - error.SystemResources => return error.SystemResources, - error.IsDir => return error.UnableToReadElfFile, - error.BrokenPipe => return error.UnableToReadElfFile, - error.Unseekable => return error.UnableToReadElfFile, - error.ConnectionResetByPeer => return error.UnableToReadElfFile, - error.ConnectionTimedOut => return error.UnableToReadElfFile, - error.SocketUnconnected => return error.UnableToReadElfFile, - error.Unexpected => return error.Unexpected, - error.InputOutput => return error.FileSystem, - error.AccessDenied => return error.Unexpected, - error.ProcessNotFound => return error.ProcessNotFound, - error.LockViolation => return error.UnableToReadElfFile, - }; - if (len == 0) return error.UnexpectedEndOfFile; - i += len; - } - return i; -} - -fn elfInt(is_64: bool, need_bswap: bool, int_32: anytype, int_64: anytype) @TypeOf(int_64) { - if (is_64) { - if (need_bswap) { - return @byteSwap(int_64); - } else { - return int_64; - } - } else { - if (need_bswap) { - return @byteSwap(int_32); - } else { - return int_32; - } - } -} - -const builtin = @import("builtin"); -const std = @import("../std.zig"); -const mem = std.mem; -const elf = std.elf; -const fs = std.fs; -const assert = std.debug.assert; -const Target = std.Target; -const native_endian = builtin.cpu.arch.endian(); -const posix = std.posix; - test { _ = NativePaths; diff --git a/src/main.zig b/src/main.zig index f84fc36d80..76c77a7b83 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,8 @@ -const std = @import("std"); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const mem = std.mem; @@ -10,7 +13,6 @@ const Color = std.zig.Color; const warn = std.log.warn; const ThreadPool = std.Thread.Pool; const cleanExit = std.process.cleanExit; -const native_os = builtin.os.tag; const Cache = std.Build.Cache; const Path = std.Build.Cache.Path; const Directory = std.Build.Cache.Directory; @@ -245,26 +247,30 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } } + var threaded: Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const cmd = args[1]; const cmd_args = args[2..]; if (mem.eql(u8, cmd, "build-exe")) { dev.check(.build_exe_command); - return buildOutputType(gpa, arena, args, .{ .build = .Exe }); + return buildOutputType(gpa, arena, io, args, .{ .build = .Exe }); } else if (mem.eql(u8, cmd, "build-lib")) { dev.check(.build_lib_command); - return buildOutputType(gpa, arena, args, .{ .build = .Lib }); + return buildOutputType(gpa, arena, io, args, .{ .build = .Lib }); } else if (mem.eql(u8, cmd, "build-obj")) { dev.check(.build_obj_command); - return buildOutputType(gpa, arena, args, .{ .build = .Obj }); + return buildOutputType(gpa, arena, io, args, .{ .build = .Obj }); } else if (mem.eql(u8, cmd, "test")) { dev.check(.test_command); - return buildOutputType(gpa, arena, args, .zig_test); + return buildOutputType(gpa, arena, io, args, .zig_test); } else if (mem.eql(u8, cmd, "test-obj")) { dev.check(.test_command); - return buildOutputType(gpa, arena, args, .zig_test_obj); + return buildOutputType(gpa, arena, io, args, .zig_test_obj); } else if (mem.eql(u8, cmd, "run")) { dev.check(.run_command); - return buildOutputType(gpa, arena, args, .run); + return buildOutputType(gpa, arena, io, args, .run); } else if (mem.eql(u8, cmd, "dlltool") or mem.eql(u8, cmd, "ranlib") or mem.eql(u8, cmd, "lib") or @@ -274,7 +280,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return process.exit(try llvmArMain(arena, args)); } else if (mem.eql(u8, cmd, "build")) { dev.check(.build_command); - return cmdBuild(gpa, arena, cmd_args); + return cmdBuild(gpa, arena, io, cmd_args); } else if (mem.eql(u8, cmd, "clang") or mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as")) { @@ -288,16 +294,16 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return process.exit(try lldMain(arena, args, true)); } else if (mem.eql(u8, cmd, "cc")) { dev.check(.cc_command); - return buildOutputType(gpa, arena, args, .cc); + return buildOutputType(gpa, arena, io, args, .cc); } else if (mem.eql(u8, cmd, "c++")) { dev.check(.cc_command); - return buildOutputType(gpa, arena, args, .cpp); + return buildOutputType(gpa, arena, io, args, .cpp); } else if (mem.eql(u8, cmd, "translate-c")) { dev.check(.translate_c_command); - return buildOutputType(gpa, arena, args, .translate_c); + return buildOutputType(gpa, arena, io, args, .translate_c); } else if (mem.eql(u8, cmd, "rc")) { const use_server = cmd_args.len > 0 and std.mem.eql(u8, cmd_args[0], "--zig-integration"); - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "resinator", .root_src_path = "resinator/main.zig", .depend_on_aro = true, @@ -308,20 +314,20 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { dev.check(.fmt_command); return @import("fmt.zig").run(gpa, arena, cmd_args); } else if (mem.eql(u8, cmd, "objcopy")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "objcopy", .root_src_path = "objcopy.zig", }); } else if (mem.eql(u8, cmd, "fetch")) { return cmdFetch(gpa, arena, cmd_args); } else if (mem.eql(u8, cmd, "libc")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "libc", .root_src_path = "libc.zig", .prepend_zig_lib_dir_path = true, }); } else if (mem.eql(u8, cmd, "std")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "std", .root_src_path = "std-docs.zig", .prepend_zig_lib_dir_path = true, @@ -332,7 +338,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return cmdInit(gpa, arena, cmd_args); } else if (mem.eql(u8, cmd, "targets")) { dev.check(.targets_command); - const host = std.zig.resolveTargetQueryOrFatal(.{}); + const host = std.zig.resolveTargetQueryOrFatal(io, .{}); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try @import("print_targets.zig").cmdTargets(arena, cmd_args, &stdout_writer.interface, &host); return stdout_writer.interface.flush(); @@ -351,7 +357,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { ); return stdout_writer.interface.flush(); } else if (mem.eql(u8, cmd, "reduce")) { - return jitCmd(gpa, arena, cmd_args, .{ + return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "reduce", .root_src_path = "reduce.zig", }); @@ -364,7 +370,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } else if (mem.eql(u8, cmd, "ast-check")) { return cmdAstCheck(arena, cmd_args); } else if (mem.eql(u8, cmd, "detect-cpu")) { - return cmdDetectCpu(cmd_args); + return cmdDetectCpu(io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "changelist")) { return cmdChangelist(arena, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "dump-zir")) { @@ -792,6 +798,7 @@ const CliModule = struct { fn buildOutputType( gpa: Allocator, arena: Allocator, + io: Io, all_args: []const []const u8, arg_mode: ArgMode, ) !void { @@ -3017,7 +3024,7 @@ fn buildOutputType( create_module.opts.emit_bin = emit_bin != .no; create_module.opts.any_c_source_files = create_module.c_source_files.items.len != 0; - const main_mod = try createModule(gpa, arena, &create_module, 0, null, color); + const main_mod = try createModule(gpa, arena, io, &create_module, 0, null, color); for (create_module.modules.keys(), create_module.modules.values()) |key, cli_mod| { if (cli_mod.resolved == null) fatal("module '{s}' declared but not used", .{key}); @@ -3545,6 +3552,7 @@ fn buildOutputType( var stdin_reader = fs.File.stdin().reader(&stdin_buffer); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try serve( + io, comp, &stdin_reader.interface, &stdout_writer.interface, @@ -3571,6 +3579,7 @@ fn buildOutputType( var output = conn.stream.writer(&stdout_buffer); try serve( + io, comp, input.interface(), &output.interface, @@ -3646,6 +3655,7 @@ fn buildOutputType( comp, gpa, arena, + io, test_exec_args.items, self_exe_path, arg_mode, @@ -3704,6 +3714,7 @@ const CreateModule = struct { fn createModule( gpa: Allocator, arena: Allocator, + io: Io, create_module: *CreateModule, index: usize, parent: ?*Package.Module, @@ -3777,7 +3788,7 @@ fn createModule( } const target_query = std.zig.parseTargetQueryOrReportFatalError(arena, target_parse_options); - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); break :t .{ .result = target, .is_native_os = target_query.isNativeOs(), @@ -4022,7 +4033,7 @@ fn createModule( for (cli_mod.deps) |dep| { const dep_index = create_module.modules.getIndex(dep.value) orelse fatal("module '{s}' depends on non-existent module '{s}'", .{ name, dep.key }); - const dep_mod = try createModule(gpa, arena, create_module, dep_index, mod, color); + const dep_mod = try createModule(gpa, arena, io, create_module, dep_index, mod, color); try mod.deps.put(arena, dep.key, dep_mod); } @@ -4038,9 +4049,10 @@ fn saveState(comp: *Compilation, incremental: bool) void { } fn serve( + io: Io, comp: *Compilation, - in: *std.Io.Reader, - out: *std.Io.Writer, + in: *Io.Reader, + out: *Io.Writer, test_exec_args: []const ?[]const u8, self_exe_path: ?[]const u8, arg_mode: ArgMode, @@ -4090,7 +4102,7 @@ fn serve( defer arena_instance.deinit(); const arena = arena_instance.allocator(); var output: Compilation.CImportResult = undefined; - try cmdTranslateC(comp, arena, &output, file_system_inputs, main_progress_node); + try cmdTranslateC(io, comp, arena, &output, file_system_inputs, main_progress_node); defer output.deinit(gpa); if (file_system_inputs.items.len != 0) { @@ -4126,6 +4138,7 @@ fn serve( // comp, // gpa, // arena, + // io, // test_exec_args, // self_exe_path.?, // arg_mode, @@ -4280,6 +4293,7 @@ fn runOrTest( comp: *Compilation, gpa: Allocator, arena: Allocator, + io: Io, test_exec_args: []const ?[]const u8, self_exe_path: []const u8, arg_mode: ArgMode, @@ -4334,7 +4348,7 @@ fn runOrTest( std.debug.lockStdErr(); const err = process.execve(gpa, argv.items, &env_map); std.debug.unlockStdErr(); - try warnAboutForeignBinaries(arena, arg_mode, target, link_libc); + try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); } else if (process.can_spawn) { @@ -4355,7 +4369,7 @@ fn runOrTest( break :t child.spawnAndWait(); }; const term = term_result catch |err| { - try warnAboutForeignBinaries(arena, arg_mode, target, link_libc); + try warnAboutForeignBinaries(io, arena, arg_mode, target, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd }); }; @@ -4594,11 +4608,12 @@ fn cmdTranslateC( pub fn translateC( gpa: Allocator, arena: Allocator, + io: Io, argv: []const []const u8, prog_node: std.Progress.Node, capture: ?*[]u8, ) !void { - try jitCmd(gpa, arena, argv, .{ + try jitCmd(gpa, arena, io, argv, .{ .cmd_name = "translate-c", .root_src_path = "translate-c/main.zig", .depend_on_aro = true, @@ -4755,7 +4770,7 @@ test sanitizeExampleName { try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project")); } -fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { +fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.build_command); var build_file: ?[]const u8 = null; @@ -4983,7 +4998,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { .arch_os_abi = triple, }); break :t .{ - .result = std.zig.resolveTargetQueryOrFatal(target_query), + .result = std.zig.resolveTargetQueryOrFatal(io, target_query), .is_native_os = false, .is_native_abi = false, .is_explicit_dynamic_linker = false, @@ -4991,7 +5006,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } } break :t .{ - .result = std.zig.resolveTargetQueryOrFatal(.{}), + .result = std.zig.resolveTargetQueryOrFatal(io, .{}), .is_native_os = true, .is_native_abi = true, .is_explicit_dynamic_linker = false, @@ -5400,6 +5415,7 @@ const JitCmdOptions = struct { fn jitCmd( gpa: Allocator, arena: Allocator, + io: Io, args: []const []const u8, options: JitCmdOptions, ) !void { @@ -5412,7 +5428,7 @@ fn jitCmd( const target_query: std.Target.Query = .{}; const resolved_target: Package.Module.ResolvedTarget = .{ - .result = std.zig.resolveTargetQueryOrFatal(target_query), + .result = std.zig.resolveTargetQueryOrFatal(io, target_query), .is_native_os = true, .is_native_abi = true, .is_explicit_dynamic_linker = false, @@ -6209,7 +6225,7 @@ fn cmdAstCheck( } } -fn cmdDetectCpu(args: []const []const u8) !void { +fn cmdDetectCpu(io: Io, args: []const []const u8) !void { dev.check(.detect_cpu_command); const detect_cpu_usage = @@ -6254,7 +6270,7 @@ fn cmdDetectCpu(args: []const []const u8) !void { const cpu = try detectNativeCpuWithLLVM(builtin.cpu.arch, name, features); try printCpu(cpu); } else { - const host_target = std.zig.resolveTargetQueryOrFatal(.{}); + const host_target = std.zig.resolveTargetQueryOrFatal(io, .{}); try printCpu(host_target.cpu); } } @@ -6521,13 +6537,14 @@ fn prefixedIntArg(arg: []const u8, prefix: []const u8) ?u64 { } fn warnAboutForeignBinaries( + io: Io, arena: Allocator, arg_mode: ArgMode, target: *const std.Target, link_libc: bool, ) !void { const host_query: std.Target.Query = .{}; - const host_target = std.zig.resolveTargetQueryOrFatal(host_query); + const host_target = std.zig.resolveTargetQueryOrFatal(io, host_query); switch (std.zig.system.getExternalExecutor(&host_target, target, .{ .link_libc = link_libc })) { .native => return, @@ -7080,7 +7097,7 @@ fn cmdFetch( try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text); } - var aw: std.Io.Writer.Allocating = .init(gpa); + var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); try ast.render(gpa, &aw.writer, fixups); const rendered = aw.written(); From 47aa5a70a54ef7838e7c8e5ebdc570f07048ec04 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 7 Oct 2025 22:31:06 -0700 Subject: [PATCH 078/244] std: updating to std.Io interface got the build runner compiling --- lib/compiler/build_runner.zig | 32 +++-- lib/compiler/test_runner.zig | 15 +- lib/std/Build.zig | 4 +- lib/std/Build/Cache.zig | 22 ++- lib/std/Build/Fuzz.zig | 7 +- lib/std/Build/Step.zig | 19 +-- lib/std/Build/Step/Options.zig | 2 + lib/std/Build/Step/Run.zig | 13 +- lib/std/Build/Step/UpdateSourceFiles.zig | 24 ++-- lib/std/Build/Step/WriteFile.zig | 36 ++--- lib/std/Build/WebServer.zig | 67 +++++---- lib/std/Io.zig | 12 ++ lib/std/Io/Dir.zig | 165 +++++++++++++++++++++- lib/std/Io/File.zig | 6 +- lib/std/Io/Threaded.zig | 73 +++++++++- lib/std/Io/Writer.zig | 8 +- lib/std/Io/net.zig | 33 +++++ lib/std/Io/net/HostName.zig | 19 ++- lib/std/Io/net/test.zig | 31 ++-- lib/std/crypto/tls/Client.zig | 20 +-- lib/std/debug/SelfInfo/Windows.zig | 3 +- lib/std/elf.zig | 12 +- lib/std/fs/Dir.zig | 150 ++++++++------------ lib/std/fs/File.zig | 22 ++- lib/std/fs/test.zig | 171 +++++++++-------------- lib/std/http/Client.zig | 45 +++--- lib/std/os/linux/IoUring.zig | 162 +++++++++++---------- lib/std/posix.zig | 26 +--- lib/std/posix/test.zig | 7 +- lib/std/process/Child.zig | 37 +++-- lib/std/tar/Writer.zig | 38 ++--- lib/std/zig.zig | 2 +- lib/std/zig/system.zig | 73 +++++----- test/src/Cases.zig | 13 +- 34 files changed, 805 insertions(+), 564 deletions(-) diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 523ef98824..b6d771302c 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -1,5 +1,8 @@ -const std = @import("std"); +const runner = @This(); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fmt = std.fmt; const mem = std.mem; @@ -11,7 +14,6 @@ const WebServer = std.Build.WebServer; const Allocator = std.mem.Allocator; const fatal = std.process.fatal; const Writer = std.Io.Writer; -const runner = @This(); const tty = std.Io.tty; pub const root = @import("@build"); @@ -75,6 +77,7 @@ pub fn main() !void { .io = io, .arena = arena, .cache = .{ + .io = io, .gpa = arena, .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}), }, @@ -84,7 +87,7 @@ pub fn main() !void { .zig_lib_directory = zig_lib_directory, .host = .{ .query = .{}, - .result = try std.zig.system.resolveTargetQuery(.{}), + .result = try std.zig.system.resolveTargetQuery(io, .{}), }, .time_report = false, }; @@ -121,7 +124,7 @@ pub fn main() !void { var watch = false; var fuzz: ?std.Build.Fuzz.Mode = null; var debounce_interval_ms: u16 = 50; - var webui_listen: ?std.net.Address = null; + var webui_listen: ?Io.net.IpAddress = null; if (try std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(arena)) |str| { if (std.meta.stringToEnum(ErrorStyle, str)) |style| { @@ -288,11 +291,11 @@ pub fn main() !void { }); }; } else if (mem.eql(u8, arg, "--webui")) { - webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; + if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; } else if (mem.startsWith(u8, arg, "--webui=")) { const addr_str = arg["--webui=".len..]; if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{}); - webui_listen = std.net.Address.parseIpAndPort(addr_str) catch |err| { + webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| { fatal("invalid web UI address '{s}': {s}", .{ addr_str, @errorName(err) }); }; } else if (mem.eql(u8, arg, "--debug-log")) { @@ -334,14 +337,10 @@ pub fn main() !void { watch = true; } else if (mem.eql(u8, arg, "--time-report")) { graph.time_report = true; - if (webui_listen == null) { - webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; - } + if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; } else if (mem.eql(u8, arg, "--fuzz")) { fuzz = .{ .forever = undefined }; - if (webui_listen == null) { - webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; - } + if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; } else if (mem.startsWith(u8, arg, "--fuzz=")) { const value = arg["--fuzz=".len..]; if (value.len == 0) fatal("missing argument to --fuzz", .{}); @@ -550,13 +549,15 @@ pub fn main() !void { var w: Watch = w: { if (!watch) break :w undefined; - if (!Watch.have_impl) fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)}); + if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag}); break :w try .init(); }; try run.thread_pool.init(thread_pool_options); defer run.thread_pool.deinit(); + const now = Io.Timestamp.now(io, .awake) catch |err| fatal("failed to collect timestamp: {t}", .{err}); + run.web_server = if (webui_listen) |listen_address| ws: { if (builtin.single_threaded) unreachable; // `fatal` above break :ws .init(.{ @@ -568,11 +569,12 @@ pub fn main() !void { .root_prog_node = main_progress_node, .watch = watch, .listen_address = listen_address, + .base_timestamp = now, }); } else null; if (run.web_server) |*ws| { - ws.start() catch |err| fatal("failed to start web server: {s}", .{@errorName(err)}); + ws.start() catch |err| fatal("failed to start web server: {t}", .{err}); } rebuild: while (true) : (if (run.error_style.clearOnUpdate()) { @@ -755,6 +757,7 @@ fn runStepNames( fuzz: ?std.Build.Fuzz.Mode, ) !void { const gpa = run.gpa; + const io = b.graph.io; const step_stack = &run.step_stack; const thread_pool = &run.thread_pool; @@ -858,6 +861,7 @@ fn runStepNames( assert(mode == .limit); var f = std.Build.Fuzz.init( gpa, + io, thread_pool, step_stack.keys(), parent_prog_node, diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 40f52fbd39..0d6f451947 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -2,6 +2,7 @@ const builtin = @import("builtin"); const std = @import("std"); +const Io = std.Io; const fatal = std.process.fatal; const testing = std.testing; const assert = std.debug.assert; @@ -16,6 +17,7 @@ var fba: std.heap.FixedBufferAllocator = .init(&fba_buffer); var fba_buffer: [8192]u8 = undefined; var stdin_buffer: [4096]u8 = undefined; var stdout_buffer: [4096]u8 = undefined; +var runner_threaded_io: Io.Threaded = .init_single_threaded; /// Keep in sync with logic in `std.Build.addRunArtifact` which decides whether /// the test runner will communicate with the build runner via `std.zig.Server`. @@ -63,8 +65,6 @@ pub fn main() void { fuzz_abi.fuzzer_init(.fromSlice(cache_dir)); } - fba.reset(); - if (listen) { return mainServer() catch @panic("internal test runner failure"); } else { @@ -74,7 +74,7 @@ pub fn main() void { fn mainServer() !void { @disableInstrumentation(); - var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer); + var stdin_reader = std.fs.File.stdin().readerStreaming(runner_threaded_io.io(), &stdin_buffer); var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); var server = try std.zig.Server.init(.{ .in = &stdin_reader.interface, @@ -131,7 +131,7 @@ fn mainServer() !void { .run_test => { testing.allocator_instance = .{}; - testing.io_instance = .init(fba.allocator()); + testing.io_instance = .init(testing.allocator); log_err_count = 0; const index = try server.receiveBody_u32(); const test_fn = builtin.test_functions[index]; @@ -154,7 +154,6 @@ fn mainServer() !void { }, }; testing.io_instance.deinit(); - fba.reset(); const leak_count = testing.allocator_instance.detectLeaks(); testing.allocator_instance.deinitWithoutLeakChecks(); try server.serveTestResults(.{ @@ -234,10 +233,10 @@ fn mainTerminal() void { var leaks: usize = 0; for (test_fn_list, 0..) |test_fn, i| { testing.allocator_instance = .{}; - testing.io_instance = .init(fba.allocator()); + testing.io_instance = .init(testing.allocator); defer { - if (testing.allocator_instance.deinit() == .leak) leaks += 1; testing.io_instance.deinit(); + if (testing.allocator_instance.deinit() == .leak) leaks += 1; } testing.log_level = .warn; @@ -324,7 +323,7 @@ pub fn mainSimple() anyerror!void { .stage2_aarch64, .stage2_riscv64 => true, else => false, }; - // is the backend capable of calling `std.Io.Writer.print`? + // is the backend capable of calling `Io.Writer.print`? const enable_print = switch (builtin.zig_backend) { .stage2_aarch64, .stage2_riscv64 => true, else => false, diff --git a/lib/std/Build.zig b/lib/std/Build.zig index d3df0c0d39..e9d2e81fba 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1837,6 +1837,8 @@ pub fn runAllowFail( if (!process.can_spawn) return error.ExecNotSupported; + const io = b.graph.io; + const max_output_size = 400 * 1024; var child = std.process.Child.init(argv, b.allocator); child.stdin_behavior = .Ignore; @@ -1847,7 +1849,7 @@ pub fn runAllowFail( try Step.handleVerbose2(b, null, child.env_map, argv); try child.spawn(); - var stdout_reader = child.stdout.?.readerStreaming(&.{}); + var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch { return error.ReadFailure; }; diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index fe9714296d..8202c4dd15 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -3,8 +3,10 @@ //! not to withstand attacks using specially-crafted input. const Cache = @This(); -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const crypto = std.crypto; const fs = std.fs; const assert = std.debug.assert; @@ -15,6 +17,7 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.cache); gpa: Allocator, +io: Io, manifest_dir: fs.Dir, hash: HashHelper = .{}, /// This value is accessed from multiple threads, protected by mutex. @@ -661,9 +664,10 @@ pub const Manifest = struct { }, } { const gpa = self.cache.gpa; + const io = self.cache.io; const input_file_count = self.files.entries.len; var tiny_buffer: [1]u8 = undefined; // allows allocRemaining to detect limit exceeded - var manifest_reader = self.manifest_file.?.reader(&tiny_buffer); // Reads positionally from zero. + var manifest_reader = self.manifest_file.?.reader(io, &tiny_buffer); // Reads positionally from zero. const limit: std.Io.Limit = .limited(manifest_file_size_max); const file_contents = manifest_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, @@ -1337,7 +1341,8 @@ test "cache file and then recall it" { var digest2: HexDigest = undefined; { - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1402,7 +1407,8 @@ test "check that changing a file makes cache fail" { var digest2: HexDigest = undefined; { - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1451,6 +1457,8 @@ test "check that changing a file makes cache fail" { } test "no file inputs" { + const io = testing.io; + var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -1459,7 +1467,8 @@ test "no file inputs" { var digest1: HexDigest = undefined; var digest2: HexDigest = undefined; - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; @@ -1517,7 +1526,8 @@ test "Manifest with files added after initial hash work" { var digest3: HexDigest = undefined; { - var cache = Cache{ + var cache: Cache = .{ + .io = io, .gpa = testing.allocator, .manifest_dir = try tmp.dir.makeOpenPath(temp_manifest_dir, .{}), }; diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index 8281fc4726..d342628871 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -1,4 +1,5 @@ const std = @import("../std.zig"); +const Io = std.Io; const Build = std.Build; const Cache = Build.Cache; const Step = std.Build.Step; @@ -14,6 +15,7 @@ const Fuzz = @This(); const build_runner = @import("root"); gpa: Allocator, +io: Io, mode: Mode, /// Allocated into `gpa`. @@ -75,6 +77,7 @@ const CoverageMap = struct { pub fn init( gpa: Allocator, + io: Io, thread_pool: *std.Thread.Pool, all_steps: []const *Build.Step, root_prog_node: std.Progress.Node, @@ -111,6 +114,7 @@ pub fn init( return .{ .gpa = gpa, + .io = io, .mode = mode, .run_steps = run_steps, .wait_group = .{}, @@ -484,6 +488,7 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte pub fn waitAndPrintReport(fuzz: *Fuzz) void { assert(fuzz.mode == .limit); + const io = fuzz.io; fuzz.wait_group.wait(); fuzz.wait_group.reset(); @@ -506,7 +511,7 @@ pub fn waitAndPrintReport(fuzz: *Fuzz) void { const fuzz_abi = std.Build.abi.fuzz; var rbuf: [0x1000]u8 = undefined; - var r = coverage_file.reader(&rbuf); + var r = coverage_file.reader(io, &rbuf); var header: fuzz_abi.SeenPcsHeader = undefined; r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 72eb66e530..aa922ff37b 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -1,9 +1,11 @@ const Step = @This(); +const builtin = @import("builtin"); + const std = @import("../std.zig"); +const Io = std.Io; const Build = std.Build; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const builtin = @import("builtin"); const Cache = Build.Cache; const Path = Cache.Path; const ArrayList = std.ArrayList; @@ -327,7 +329,7 @@ pub fn cast(step: *Step, comptime T: type) ?*T { } /// For debugging purposes, prints identifying information about this Step. -pub fn dump(step: *Step, w: *std.Io.Writer, tty_config: std.Io.tty.Config) void { +pub fn dump(step: *Step, w: *Io.Writer, tty_config: Io.tty.Config) void { if (step.debug_stack_trace.instruction_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; std.debug.writeStackTrace(&step.debug_stack_trace, w, tty_config) catch {}; @@ -382,7 +384,7 @@ pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutO pub const ZigProcess = struct { child: std.process.Child, - poller: std.Io.Poller(StreamEnum), + poller: Io.Poller(StreamEnum), progress_ipc_fd: if (std.Progress.have_ipc) ?std.posix.fd_t else void, pub const StreamEnum = enum { stdout, stderr }; @@ -458,7 +460,7 @@ pub fn evalZigProcess( const zp = try gpa.create(ZigProcess); zp.* = .{ .child = child, - .poller = std.Io.poll(gpa, ZigProcess.StreamEnum, .{ + .poller = Io.poll(gpa, ZigProcess.StreamEnum, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }), @@ -505,11 +507,12 @@ pub fn evalZigProcess( } /// Wrapper around `std.fs.Dir.updateFile` that handles verbose and error output. -pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !std.fs.Dir.PrevStatus { +pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus { const b = s.owner; + const io = b.graph.io; const src_path = src_lazy_path.getPath3(b, s); try handleVerbose(b, null, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path }); - return src_path.root_dir.handle.updateFile(src_path.sub_path, std.fs.cwd(), dest_path, .{}) catch |err| { + return Io.Dir.updateFile(src_path.root_dir.handle.adaptToNewApi(), io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| { return s.fail("unable to update file from '{f}' to '{s}': {s}", .{ src_path, dest_path, @errorName(err), }); @@ -738,7 +741,7 @@ pub fn allocPrintCmd2( argv: []const []const u8, ) Allocator.Error![]u8 { const shell = struct { - fn escape(writer: *std.Io.Writer, string: []const u8, is_argv0: bool) !void { + fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void { for (string) |c| { if (switch (c) { else => true, @@ -772,7 +775,7 @@ pub fn allocPrintCmd2( } }; - var aw: std.Io.Writer.Allocating = .init(gpa); + var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); const writer = &aw.writer; if (opt_cwd) |cwd| writer.print("cd {s} && ", .{cwd}) catch return error.OutOfMemory; diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig index d738b8edaa..8590fa9608 100644 --- a/lib/std/Build/Step/Options.zig +++ b/lib/std/Build/Step/Options.zig @@ -538,8 +538,10 @@ test Options { defer arena.deinit(); var graph: std.Build.Graph = .{ + .io = io, .arena = arena.allocator(), .cache = .{ + .io = io, .gpa = arena.allocator(), .manifest_dir = std.fs.cwd(), }, diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index cd29262612..314862e201 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -761,6 +761,7 @@ const IndexedOutput = struct { }; fn make(step: *Step, options: Step.MakeOptions) !void { const b = step.owner; + const io = b.graph.io; const arena = b.allocator; const run: *Run = @fieldParentPtr("step", step); const has_side_effects = run.hasSideEffects(); @@ -834,7 +835,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void { defer file.close(); var buf: [1024]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(io, &buf); _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { error.ReadFailed => return step.fail( "failed to read from '{f}': {t}", @@ -1067,6 +1068,7 @@ pub fn rerunInFuzzMode( ) !void { const step = &run.step; const b = step.owner; + const io = b.graph.io; const arena = b.allocator; var argv_list: std.ArrayList([]const u8) = .empty; for (run.argv.items) |arg| { @@ -1093,7 +1095,7 @@ pub fn rerunInFuzzMode( defer file.close(); var buf: [1024]u8 = undefined; - var file_reader = file.reader(&buf); + var file_reader = file.reader(io, &buf); _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.WriteFailed => return error.OutOfMemory, @@ -2090,6 +2092,7 @@ fn sendRunFuzzTestMessage( fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { const b = run.step.owner; + const io = b.graph.io; const arena = b.allocator; try child.spawn(); @@ -2113,7 +2116,7 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { defer file.close(); // TODO https://github.com/ziglang/zig/issues/23955 var read_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&read_buffer); + var file_reader = file.reader(io, &read_buffer); var write_buffer: [1024]u8 = undefined; var stdin_writer = child.stdin.?.writer(&write_buffer); _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { @@ -2159,7 +2162,7 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { stdout_bytes = try poller.toOwnedSlice(.stdout); stderr_bytes = try poller.toOwnedSlice(.stderr); } else { - var stdout_reader = stdout.readerStreaming(&.{}); + var stdout_reader = stdout.readerStreaming(io, &.{}); stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return stdout_reader.err.?, @@ -2167,7 +2170,7 @@ fn evalGeneric(run: *Run, child: *std.process.Child) !EvalGenericResult { }; } } else if (child.stderr) |stderr| { - var stderr_reader = stderr.readerStreaming(&.{}); + var stderr_reader = stderr.readerStreaming(io, &.{}); stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return stderr_reader.err.?, diff --git a/lib/std/Build/Step/UpdateSourceFiles.zig b/lib/std/Build/Step/UpdateSourceFiles.zig index 674e2a01c6..6f1559bd68 100644 --- a/lib/std/Build/Step/UpdateSourceFiles.zig +++ b/lib/std/Build/Step/UpdateSourceFiles.zig @@ -3,11 +3,13 @@ //! not be used during the normal build process, but as a utility run by a //! developer with intention to update source files, which will then be //! committed to version control. +const UpdateSourceFiles = @This(); + const std = @import("std"); +const Io = std.Io; const Step = std.Build.Step; const fs = std.fs; const ArrayList = std.ArrayList; -const UpdateSourceFiles = @This(); step: Step, output_source_files: std.ArrayListUnmanaged(OutputSourceFile), @@ -70,22 +72,21 @@ pub fn addBytesToSource(usf: *UpdateSourceFiles, bytes: []const u8, sub_path: [] fn make(step: *Step, options: Step.MakeOptions) !void { _ = options; const b = step.owner; + const io = b.graph.io; const usf: *UpdateSourceFiles = @fieldParentPtr("step", step); var any_miss = false; for (usf.output_source_files.items) |output_source_file| { if (fs.path.dirname(output_source_file.sub_path)) |dirname| { b.build_root.handle.makePath(dirname) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.build_root, dirname, @errorName(err), - }); + return step.fail("unable to make path '{f}{s}': {t}", .{ b.build_root, dirname, err }); }; } switch (output_source_file.contents) { .bytes => |bytes| { b.build_root.handle.writeFile(.{ .sub_path = output_source_file.sub_path, .data = bytes }) catch |err| { - return step.fail("unable to write file '{f}{s}': {s}", .{ - b.build_root, output_source_file.sub_path, @errorName(err), + return step.fail("unable to write file '{f}{s}': {t}", .{ + b.build_root, output_source_file.sub_path, err, }); }; any_miss = true; @@ -94,15 +95,16 @@ fn make(step: *Step, options: Step.MakeOptions) !void { if (!step.inputs.populated()) try step.addWatchInput(file_source); const source_path = file_source.getPath2(b, step); - const prev_status = fs.Dir.updateFile( - fs.cwd(), + const prev_status = Io.Dir.updateFile( + .cwd(), + io, source_path, - b.build_root.handle, + b.build_root.handle.adaptToNewApi(), output_source_file.sub_path, .{}, ) catch |err| { - return step.fail("unable to update file from '{s}' to '{f}{s}': {s}", .{ - source_path, b.build_root, output_source_file.sub_path, @errorName(err), + return step.fail("unable to update file from '{s}' to '{f}{s}': {t}", .{ + source_path, b.build_root, output_source_file.sub_path, err, }); }; any_miss = any_miss or prev_status == .stale; diff --git a/lib/std/Build/Step/WriteFile.zig b/lib/std/Build/Step/WriteFile.zig index b1cfb3b42a..1a531604e3 100644 --- a/lib/std/Build/Step/WriteFile.zig +++ b/lib/std/Build/Step/WriteFile.zig @@ -2,6 +2,7 @@ //! the local cache which has a set of files that have either been generated //! during the build, or are copied from the source package. const std = @import("std"); +const Io = std.Io; const Step = std.Build.Step; const fs = std.fs; const ArrayList = std.ArrayList; @@ -174,6 +175,7 @@ fn maybeUpdateName(write_file: *WriteFile) void { fn make(step: *Step, options: Step.MakeOptions) !void { _ = options; const b = step.owner; + const io = b.graph.io; const arena = b.allocator; const gpa = arena; const write_file: *WriteFile = @fieldParentPtr("step", step); @@ -264,40 +266,27 @@ fn make(step: *Step, options: Step.MakeOptions) !void { }; defer cache_dir.close(); - const cwd = fs.cwd(); - for (write_file.files.items) |file| { if (fs.path.dirname(file.sub_path)) |dirname| { cache_dir.makePath(dirname) catch |err| { - return step.fail("unable to make path '{f}{s}{c}{s}': {s}", .{ - b.cache_root, cache_path, fs.path.sep, dirname, @errorName(err), + return step.fail("unable to make path '{f}{s}{c}{s}': {t}", .{ + b.cache_root, cache_path, fs.path.sep, dirname, err, }); }; } switch (file.contents) { .bytes => |bytes| { cache_dir.writeFile(.{ .sub_path = file.sub_path, .data = bytes }) catch |err| { - return step.fail("unable to write file '{f}{s}{c}{s}': {s}", .{ - b.cache_root, cache_path, fs.path.sep, file.sub_path, @errorName(err), + return step.fail("unable to write file '{f}{s}{c}{s}': {t}", .{ + b.cache_root, cache_path, fs.path.sep, file.sub_path, err, }); }; }, .copy => |file_source| { const source_path = file_source.getPath2(b, step); - const prev_status = fs.Dir.updateFile( - cwd, - source_path, - cache_dir, - file.sub_path, - .{}, - ) catch |err| { - return step.fail("unable to update file from '{s}' to '{f}{s}{c}{s}': {s}", .{ - source_path, - b.cache_root, - cache_path, - fs.path.sep, - file.sub_path, - @errorName(err), + const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir.adaptToNewApi(), file.sub_path, .{}) catch |err| { + return step.fail("unable to update file from '{s}' to '{f}{s}{c}{s}': {t}", .{ + source_path, b.cache_root, cache_path, fs.path.sep, file.sub_path, err, }); }; // At this point we already will mark the step as a cache miss. @@ -331,10 +320,11 @@ fn make(step: *Step, options: Step.MakeOptions) !void { switch (entry.kind) { .directory => try cache_dir.makePath(dest_path), .file => { - const prev_status = fs.Dir.updateFile( - src_entry_path.root_dir.handle, + const prev_status = Io.Dir.updateFile( + src_entry_path.root_dir.handle.adaptToNewApi(), + io, src_entry_path.sub_path, - cache_dir, + cache_dir.adaptToNewApi(), dest_path, .{}, ) catch |err| { diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 135fbe7f0a..95338c9f08 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -3,14 +3,15 @@ thread_pool: *std.Thread.Pool, graph: *const Build.Graph, all_steps: []const *Build.Step, listen_address: net.IpAddress, -ttyconf: std.Io.tty.Config, +ttyconf: Io.tty.Config, root_prog_node: std.Progress.Node, watch: bool, tcp_server: ?net.Server, serve_thread: ?std.Thread, -base_timestamp: i128, +/// Uses `Io.Clock.awake`. +base_timestamp: i96, /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. step_names_trailing: []u8, @@ -53,15 +54,17 @@ pub const Options = struct { thread_pool: *std.Thread.Pool, graph: *const std.Build.Graph, all_steps: []const *Build.Step, - ttyconf: std.Io.tty.Config, + ttyconf: Io.tty.Config, root_prog_node: std.Progress.Node, watch: bool, listen_address: net.IpAddress, + base_timestamp: Io.Timestamp, }; pub fn init(opts: Options) WebServer { - // The upcoming `std.Io` interface should allow us to use `Io.async` and `Io.concurrent` + // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` // instead of threads, so that the web server can function in single-threaded builds. comptime assert(!builtin.single_threaded); + assert(opts.base_timestamp.clock == .awake); const all_steps = opts.all_steps; @@ -106,7 +109,7 @@ pub fn init(opts: Options) WebServer { .tcp_server = null, .serve_thread = null, - .base_timestamp = std.time.nanoTimestamp(), + .base_timestamp = opts.base_timestamp.nanoseconds, .step_names_trailing = step_names_trailing, .step_status_bits = step_status_bits, @@ -147,32 +150,34 @@ pub fn deinit(ws: *WebServer) void { pub fn start(ws: *WebServer) error{AlreadyReported}!void { assert(ws.tcp_server == null); assert(ws.serve_thread == null); + const io = ws.graph.io; - ws.tcp_server = ws.listen_address.listen(.{ .reuse_address = true }) catch |err| { + ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| { log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.getPort(), @errorName(err) }); return error.AlreadyReported; }; ws.serve_thread = std.Thread.spawn(.{}, serve, .{ws}) catch |err| { log.err("unable to spawn web server thread: {s}", .{@errorName(err)}); - ws.tcp_server.?.deinit(); + ws.tcp_server.?.deinit(io); ws.tcp_server = null; return error.AlreadyReported; }; - log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.listen_address}); + log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address}); if (ws.listen_address.getPort() == 0) { - log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.listen_address}); + log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address}); } } fn serve(ws: *WebServer) void { + const io = ws.graph.io; while (true) { - const connection = ws.tcp_server.?.accept() catch |err| { + var stream = ws.tcp_server.?.accept(io) catch |err| { log.err("failed to accept connection: {s}", .{@errorName(err)}); return; }; - _ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| { + _ = std.Thread.spawn(.{}, accept, .{ ws, stream }) catch |err| { log.err("unable to spawn connection thread: {s}", .{@errorName(err)}); - connection.stream.close(); + stream.close(io); continue; }; } @@ -227,6 +232,7 @@ pub fn finishBuild(ws: *WebServer, opts: struct { ws.fuzz = Fuzz.init( ws.gpa, + ws.graph.io, ws.thread_pool, ws.all_steps, ws.root_prog_node, @@ -241,17 +247,25 @@ pub fn finishBuild(ws: *WebServer, opts: struct { } pub fn now(s: *const WebServer) i64 { - return @intCast(std.time.nanoTimestamp() - s.base_timestamp); + const io = s.graph.io; + const base: Io.Timestamp = .{ .nanoseconds = s.base_timestamp, .clock = .awake }; + const ts = Io.Timestamp.now(io, base.clock) catch base; + return @intCast(base.durationTo(ts).toNanoseconds()); } -fn accept(ws: *WebServer, connection: net.Server.Connection) void { - defer connection.stream.close(); - +fn accept(ws: *WebServer, stream: net.Stream) void { + const io = ws.graph.io; + defer { + // `net.Stream.close` wants to helpfully overwrite `stream` with + // `undefined`, but it cannot do so since it is an immutable parameter. + var copy = stream; + copy.close(io); + } var send_buffer: [4096]u8 = undefined; var recv_buffer: [4096]u8 = undefined; - var connection_reader = connection.stream.reader(&recv_buffer); - var connection_writer = connection.stream.writer(&send_buffer); - var server: http.Server = .init(connection_reader.interface(), &connection_writer.interface); + var connection_reader = stream.reader(io, &recv_buffer); + var connection_writer = stream.writer(io, &send_buffer); + var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); while (true) { var request = server.receiveHead() catch |err| switch (err) { @@ -466,12 +480,9 @@ pub fn serveFile( }, }); } -pub fn serveTarFile( - ws: *WebServer, - request: *http.Server.Request, - paths: []const Cache.Path, -) !void { +pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { const gpa = ws.gpa; + const io = ws.graph.io; var send_buffer: [0x4000]u8 = undefined; var response = try request.respondStreaming(&send_buffer, .{ @@ -496,7 +507,7 @@ pub fn serveTarFile( defer file.close(); const stat = try file.stat(); var read_buffer: [1024]u8 = undefined; - var file_reader: std.fs.File.Reader = .initSize(file, &read_buffer, stat.size); + var file_reader: Io.File.Reader = .initSize(file.adaptToNewApi(), io, &read_buffer, stat.size); // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI: @@ -566,7 +577,7 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim child.stderr_behavior = .Pipe; try child.spawn(); - var poller = std.Io.poll(gpa, enum { stdout, stderr }, .{ + var poller = Io.poll(gpa, enum { stdout, stderr }, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }); @@ -842,7 +853,10 @@ const cache_control_header: http.Header = .{ }; const builtin = @import("builtin"); + const std = @import("std"); +const Io = std.Io; +const net = std.Io.net; const assert = std.debug.assert; const mem = std.mem; const log = std.log.scoped(.web_server); @@ -852,6 +866,5 @@ const Cache = Build.Cache; const Fuzz = Build.Fuzz; const abi = Build.abi; const http = std.http; -const net = std.Io.net; const WebServer = @This(); diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 5cdb9b0f01..e569c4ec94 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -654,6 +654,10 @@ pub const VTable = struct { conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, + dirMake: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, + dirStat: *const fn (?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat, + dirStatPath: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8) Dir.StatError!File.Stat, + fileStat: *const fn (?*anyopaque, file: File) File.StatError!File.Stat, createFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File, fileOpen: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File, fileClose: *const fn (?*anyopaque, File) void, @@ -804,6 +808,10 @@ pub const Timestamp = struct { assert(lhs.clock == rhs.clock); return std.math.compare(lhs.nanoseconds, op, rhs.nanoseconds); } + + pub fn toSeconds(t: Timestamp) i64 { + return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s)); + } }; pub const Duration = struct { @@ -831,6 +839,10 @@ pub const Duration = struct { return @intCast(@divTrunc(d.nanoseconds, std.time.ns_per_s)); } + pub fn toNanoseconds(d: Duration) i96 { + return d.nanoseconds; + } + pub fn sleep(duration: Duration, io: Io) SleepError!void { return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .awake } }); } diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index a2ae96210e..3c9772076b 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -6,6 +6,9 @@ const File = Io.File; handle: Handle, +pub const Mode = Io.File.Mode; +pub const default_mode: Mode = 0o755; + pub fn cwd() Dir { return .{ .handle = std.fs.cwd().fd }; } @@ -47,8 +50,9 @@ pub const UpdateFileError = File.OpenError; /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If /// they are equal, does nothing. Otherwise, atomically copies `source_path` to -/// `dest_path`. The destination file gains the mtime, atime, and mode of the -/// source file so that the next call to `updateFile` will not need a copy. +/// `dest_path`, creating the parent directory hierarchy as needed. The +/// destination file gains the mtime, atime, and mode of the source file so +/// that the next call to `updateFile` will not need a copy. /// /// Returns the previous status of the file before updating. /// @@ -65,7 +69,7 @@ pub fn updateFile( options: std.fs.Dir.CopyFileOptions, ) !PrevStatus { var src_file = try source_dir.openFile(io, source_path, .{}); - defer src_file.close(); + defer src_file.close(io); const src_stat = try src_file.stat(io); const actual_mode = options.override_mode orelse src_stat.mode; @@ -93,13 +97,13 @@ pub fn updateFile( } var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available. - var atomic_file = try dest_dir.atomicFile(io, dest_path, .{ + var atomic_file = try std.fs.Dir.atomicFile(.adaptFromNewApi(dest_dir), dest_path, .{ .mode = actual_mode, .write_buffer = &buffer, }); defer atomic_file.deinit(); - var src_reader: File.Reader = .initSize(io, src_file, &.{}, src_stat.size); + var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size); const dest_writer = &atomic_file.file_writer.interface; _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) { @@ -111,3 +115,154 @@ pub fn updateFile( try atomic_file.renameIntoPlace(); return .stale; } + +pub const ReadFileError = File.OpenError || File.Reader.Error; + +/// Read all of file contents using a preallocated buffer. +/// +/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` +/// the situation is ambiguous. It could either mean that the entire file was read, and +/// it exactly fits the buffer, or it could mean the buffer was not big enough for the +/// entire file. +/// +/// * On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// * On WASI, `file_path` should be encoded as valid UTF-8. +/// * On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding. +pub fn readFile(dir: Dir, io: Io, file_path: []const u8, buffer: []u8) ReadFileError![]u8 { + var file = try dir.openFile(io, file_path, .{}); + defer file.close(io); + + var reader = file.reader(io, &.{}); + const n = reader.interface.readSliceShort(buffer) catch |err| switch (err) { + error.ReadFailed => return reader.err.?, + }; + + return buffer[0..n]; +} + +pub const MakeError = error{ + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to create a new directory relative to it. + AccessDenied, + PermissionDenied, + DiskQuota, + PathAlreadyExists, + SymLinkLoop, + LinkQuotaExceeded, + NameTooLong, + FileNotFound, + SystemResources, + NoSpaceLeft, + NotDir, + ReadOnlyFileSystem, + /// WASI-only; file paths must be valid UTF-8. + InvalidUtf8, + /// Windows-only; file paths provided by the user must be valid WTF-8. + /// https://simonsapin.github.io/wtf-8/ + InvalidWtf8, + BadPathName, + NoDevice, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || Io.Cancelable || Io.UnexpectedError; + +/// Creates a single directory with a relative or absolute path. +/// +/// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// * On WASI, `sub_path` should be encoded as valid UTF-8. +/// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// +/// Related: +/// * `makePath` +/// * `makeDirAbsolute` +pub fn makeDir(dir: Dir, io: Io, sub_path: []const u8) MakeError!void { + return io.vtable.dirMake(io.userdata, dir, sub_path, default_mode); +} + +pub const MakePathError = MakeError || StatPathError; + +/// Calls makeDir iteratively to make an entire path, creating any parent +/// directories that do not exist. +/// +/// Returns success if the path already exists and is a directory. +/// +/// This function is not atomic, and if it returns an error, the file system +/// may have been modified regardless. +/// +/// Fails on an empty path with `error.BadPathName` as that is not a path that +/// can be created. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// +/// Paths containing `..` components are handled differently depending on the platform: +/// - On Windows, `..` are resolved before the path is passed to NtCreateFile, meaning +/// a `sub_path` like "first/../second" will resolve to "second" and only a +/// `./second` directory will be created. +/// - On other platforms, `..` are not resolved before the path is passed to `mkdirat`, +/// meaning a `sub_path` like "first/../second" will create both a `./first` +/// and a `./second` directory. +pub fn makePath(dir: Dir, io: Io, sub_path: []const u8) MakePathError!void { + _ = try makePathStatus(dir, io, sub_path); +} + +pub const MakePathStatus = enum { existed, created }; + +/// Same as `makePath` except returns whether the path already existed or was +/// successfully created. +pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!MakePathStatus { + var it = try std.fs.path.componentIterator(sub_path); + var status: MakePathStatus = .existed; + var component = it.last() orelse return error.BadPathName; + while (true) { + if (makeDir(dir, io, component.path)) |_| { + status = .created; + } else |err| switch (err) { + error.PathAlreadyExists => { + // stat the file and return an error if it's not a directory + // this is important because otherwise a dangling symlink + // could cause an infinite loop + check_dir: { + // workaround for windows, see https://github.com/ziglang/zig/issues/16738 + const fstat = statPath(dir, io, component.path) catch |stat_err| switch (stat_err) { + error.IsDir => break :check_dir, + else => |e| return e, + }; + if (fstat.kind != .directory) return error.NotDir; + } + }, + error.FileNotFound => |e| { + component = it.previous() orelse return e; + continue; + }, + else => |e| return e, + } + component = it.next() orelse return status; + } +} + +pub const Stat = File.Stat; +pub const StatError = File.StatError; + +pub fn stat(dir: Dir, io: Io) StatError!Stat { + return io.vtable.dirStat(io.userdata, dir); +} + +pub const StatPathError = File.OpenError || File.StatError; + +/// Returns metadata for a file inside the directory. +/// +/// On Windows, this requires three syscalls. On other operating systems, it +/// only takes one. +/// +/// Symlinks are followed. +/// +/// `sub_path` may be absolute, in which case `self` is ignored. +/// +/// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). +/// * On WASI, `sub_path` should be encoded as valid UTF-8. +/// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn statPath(dir: Dir, io: Io, sub_path: []const u8) StatPathError!File.Stat { + return io.vtable.dirStatPath(io.userdata, dir, sub_path); +} diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 242d96d52f..7d87fab973 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -446,7 +446,11 @@ pub const Reader = struct { fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); - switch (r.mode) { + return streamMode(r, w, limit, r.mode); + } + + pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Reader.Mode) Io.Reader.StreamError!usize { + switch (mode) { .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { error.Unimplemented => { r.mode = r.mode.toReading(); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 0727730b93..0f217c7f80 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -63,7 +63,17 @@ const Closure = struct { pub const InitError = std.Thread.CpuCountError || Allocator.Error; -pub fn init(gpa: Allocator) Pool { +/// Related: +/// * `init_single_threaded` +pub fn init( + /// Must be threadsafe. Only used for the following functions: + /// * `Io.VTable.async` + /// * `Io.VTable.concurrent` + /// * `Io.VTable.groupAsync` + /// If these functions are avoided, then `Allocator.failing` may be passed + /// here. + gpa: Allocator, +) Pool { var pool: Pool = .{ .allocator = gpa, .threads = .empty, @@ -77,6 +87,20 @@ pub fn init(gpa: Allocator) Pool { return pool; } +/// Statically initialize such that any call to the following functions will +/// fail with `error.OutOfMemory`: +/// * `Io.VTable.async` +/// * `Io.VTable.concurrent` +/// * `Io.VTable.groupAsync` +/// When initialized this way, `deinit` is safe, but unnecessary to call. +pub const init_single_threaded: Pool = .{ + .allocator = .failing, + .threads = .empty, + .stack_size = std.Thread.SpawnConfig.default_stack_size, + .cpu_count = 1, + .concurrent_count = 0, +}; + pub fn deinit(pool: *Pool) void { const gpa = pool.allocator; pool.join(); @@ -136,6 +160,10 @@ pub fn io(pool: *Pool) Io { .conditionWait = conditionWait, .conditionWake = conditionWake, + .dirMake = dirMake, + .dirStat = dirStat, + .dirStatPath = dirStatPath, + .fileStat = fileStat, .createFile = createFile, .fileOpen = fileOpen, .fileClose = fileClose, @@ -520,10 +548,11 @@ fn groupAsync( fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const gpa = pool.allocator; if (builtin.single_threaded) return; + // TODO these primitives are too high level, need to check cancel on EINTR const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); std.Thread.WaitGroup.waitStateless(group_state, reset_event); @@ -531,8 +560,9 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); while (true) { const gc: *GroupClosure = @fieldParentPtr("node", node); - gc.closure.requestCancel(); - node = node.next orelse break; + const node_next = node.next; + gc.free(gpa); + node = node_next orelse break; } } @@ -724,6 +754,41 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. } } +fn dirMake(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + +fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = dir; + @panic("TODO"); +} + +fn dirStatPath(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.StatError!Io.File.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = dir; + _ = sub_path; + @panic("TODO"); +} + +fn fileStat(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + try pool.checkCancel(); + + _ = file; + @panic("TODO"); +} + fn createFile( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 2bb3c12b15..32aae1673e 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -2827,6 +2827,8 @@ pub const Allocating = struct { }; test "discarding sendFile" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2837,7 +2839,7 @@ test "discarding sendFile" { try file_writer.interface.writeByte('h'); try file_writer.interface.flush(); - var file_reader = file_writer.moveToReader(); + var file_reader = file_writer.moveToReader(io); try file_reader.seekTo(0); var w_buffer: [256]u8 = undefined; @@ -2847,6 +2849,8 @@ test "discarding sendFile" { } test "allocating sendFile" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2857,7 +2861,7 @@ test "allocating sendFile" { try file_writer.interface.writeAll("abcd"); try file_writer.interface.flush(); - var file_reader = file_writer.moveToReader(); + var file_reader = file_writer.moveToReader(io); try file_reader.seekTo(0); try file_reader.interface.fill(2); diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index e305145575..dc1080ab38 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -57,6 +57,39 @@ pub const IpAddress = union(enum) { pub const Family = @typeInfo(IpAddress).@"union".tag_type.?; + pub const ParseLiteralError = error{ InvalidAddress, InvalidPort }; + + /// Parse an IP address which may include a port. + /// + /// For IPv4, this is written `address:port`. + /// + /// For IPv6, RFC 3986 defines this as an "IP literal", and the port is + /// differentiated from the address by surrounding the address part in + /// brackets "[addr]:port". Even if the port is not given, the brackets are + /// mandatory. + pub fn parseLiteral(text: []const u8) ParseLiteralError!IpAddress { + if (text.len == 0) return error.InvalidAddress; + if (text[0] == '[') { + const addr_end = std.mem.indexOfScalar(u8, text, ']') orelse + return error.InvalidAddress; + const addr_text = text[1..addr_end]; + const port: u16 = p: { + if (addr_end == text.len - 1) break :p 0; + if (text[addr_end + 1] != ':') return error.InvalidAddress; + break :p std.fmt.parseInt(u16, text[addr_end + 2 ..], 10) catch return error.InvalidPort; + }; + return parseIp6(addr_text, port) catch error.InvalidAddress; + } + if (std.mem.indexOfScalar(u8, text, ':')) |i| { + const addr = Ip4Address.parse(text[0..i], 0) catch return error.InvalidAddress; + return .{ .ip4 = .{ + .bytes = addr.bytes, + .port = std.fmt.parseInt(u16, text[i + 1 ..], 10) catch return error.InvalidPort, + } }; + } + return parseIp4(text, 0) catch error.InvalidAddress; + } + /// Parse the given IP address string into an `IpAddress` value. /// /// This is a pure function but it cannot handle IPv6 addresses that have diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index d8c3a3a903..72b5d39038 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -77,7 +77,9 @@ pub const LookupError = error{ InvalidDnsAAAARecord, InvalidDnsCnameRecord, NameServerFailure, -} || Io.Timestamp.Error || IpAddress.BindError || Io.File.OpenError || Io.File.Reader.Error || Io.Cancelable; + /// Failed to open or read "/etc/hosts" or "/etc/resolv.conf". + DetectingNetworkConfigurationFailed, +} || Io.Timestamp.Error || IpAddress.BindError || Io.Cancelable; pub const LookupResult = struct { /// How many `LookupOptions.addresses_buffer` elements are populated. @@ -428,14 +430,25 @@ fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResul error.AccessDenied, => return .empty, - else => |e| return e, + error.Canceled => |e| return e, + + else => { + // TODO populate optional diagnostic struct + return error.DetectingNetworkConfigurationFailed; + }, }; defer file.close(io); var line_buf: [512]u8 = undefined; var file_reader = file.reader(io, &line_buf); return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) { - error.ReadFailed => return file_reader.err.?, + error.ReadFailed => switch (file_reader.err.?) { + error.Canceled => |e| return e, + else => { + // TODO populate optional diagnostic struct + return error.DetectingNetworkConfigurationFailed; + }, + }, }; } diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 01c4b213a8..f1fb6cd57c 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -211,11 +211,11 @@ test "listen on a port, send bytes, receive bytes" { const t = try std.Thread.spawn(.{}, S.clientFn, .{server.socket.address}); defer t.join(); - var client = try server.accept(io); - defer client.stream.close(io); + var stream = try server.accept(io); + defer stream.close(io); var buf: [16]u8 = undefined; - var stream_reader = client.stream.reader(io, &.{}); - const n = try stream_reader.interface().readSliceShort(&buf); + var stream_reader = stream.reader(io, &.{}); + const n = try stream_reader.interface.readSliceShort(&buf); try testing.expectEqual(@as(usize, 12), n); try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); @@ -267,10 +267,9 @@ fn testServer(server: *net.Server) anyerror!void { const io = testing.io; - var client = try server.accept(io); - - const stream = client.stream.writer(io); - try stream.print("hello from server\n", .{}); + var stream = try server.accept(io); + var writer = stream.writer(io, &.{}); + try writer.interface.print("hello from server\n", .{}); } test "listen on a unix socket, send bytes, receive bytes" { @@ -310,11 +309,11 @@ test "listen on a unix socket, send bytes, receive bytes" { const t = try std.Thread.spawn(.{}, S.clientFn, .{socket_path}); defer t.join(); - var client = try server.accept(io); - defer client.stream.close(io); + var stream = try server.accept(io); + defer stream.close(io); var buf: [16]u8 = undefined; - var stream_reader = client.stream.reader(io, &.{}); - const n = try stream_reader.interface().readSliceShort(&buf); + var stream_reader = stream.reader(io, &.{}); + const n = try stream_reader.interface.readSliceShort(&buf); try testing.expectEqual(@as(usize, 12), n); try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); @@ -366,10 +365,10 @@ test "non-blocking tcp server" { const socket_file = try net.tcpConnectToAddress(server.socket.address); defer socket_file.close(); - var client = try server.accept(io); - defer client.stream.close(io); - const stream = client.stream.writer(io); - try stream.print("hello from server\n", .{}); + var stream = try server.accept(io); + defer stream.close(io); + var writer = stream.writer(io, .{}); + try writer.interface.print("hello from server\n", .{}); var buf: [100]u8 = undefined; const len = try socket_file.read(&buf); diff --git a/lib/std/crypto/tls/Client.zig b/lib/std/crypto/tls/Client.zig index b697d624fa..f6e334af8e 100644 --- a/lib/std/crypto/tls/Client.zig +++ b/lib/std/crypto/tls/Client.zig @@ -105,6 +105,14 @@ pub const Options = struct { /// Verify that the server certificate is authorized by a given ca bundle. bundle: Certificate.Bundle, }, + write_buffer: []u8, + read_buffer: []u8, + /// Cryptographically secure random bytes. The pointer is not captured; data is only + /// read during `init`. + entropy: *const [176]u8, + /// Current time according to the wall clock / calendar, in seconds. + realtime_now_seconds: i64, + /// If non-null, ssl secrets are logged to this stream. Creating such a log file allows /// other programs with access to that file to decrypt all traffic over this connection. /// @@ -120,8 +128,6 @@ pub const Options = struct { /// application layer itself verifies that the amount of data received equals /// the amount of data expected, such as HTTP with the Content-Length header. allow_truncation_attacks: bool = false, - write_buffer: []u8, - read_buffer: []u8, /// Populated when `error.TlsAlert` is returned from `init`. alert: ?*tls.Alert = null, }; @@ -189,14 +195,12 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client }; const host_len: u16 = @intCast(host.len); - var random_buffer: [176]u8 = undefined; - crypto.random.bytes(&random_buffer); - const client_hello_rand = random_buffer[0..32].*; + const client_hello_rand = options.entropy[0..32].*; var key_seq: u64 = 0; var server_hello_rand: [32]u8 = undefined; - const legacy_session_id = random_buffer[32..64].*; + const legacy_session_id = options.entropy[32..64].*; - var key_share = KeyShare.init(random_buffer[64..176].*) catch |err| switch (err) { + var key_share = KeyShare.init(options.entropy[64..176].*) catch |err| switch (err) { // Only possible to happen if the seed is all zeroes. error.IdentityElement => return error.InsufficientEntropy, }; @@ -321,7 +325,7 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client var handshake_cipher: tls.HandshakeCipher = undefined; var main_cert_pub_key: CertificatePublicKey = undefined; var tls12_negotiated_group: ?tls.NamedGroup = null; - const now_sec = std.time.timestamp(); + const now_sec = options.realtime_now_seconds; var cleartext_fragment_start: usize = 0; var cleartext_fragment_end: usize = 0; diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index f84836a6d4..ea2fa96199 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -434,7 +434,7 @@ const Module = struct { }; errdefer pdb_file.close(); - const pdb_reader = try arena.create(std.fs.File.Reader); + const pdb_reader = try arena.create(Io.File.Reader); pdb_reader.* = pdb_file.reader(try arena.alloc(u8, 4096)); var pdb = Pdb.init(gpa, pdb_reader) catch |err| switch (err) { @@ -544,6 +544,7 @@ fn findModule(si: *SelfInfo, gpa: Allocator, address: usize) error{ MissingDebug } const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Dwarf = std.debug.Dwarf; const Pdb = std.debug.Pdb; diff --git a/lib/std/elf.zig b/lib/std/elf.zig index 746d24f61a..c5f8a7ea80 100644 --- a/lib/std/elf.zig +++ b/lib/std/elf.zig @@ -710,7 +710,7 @@ pub const ProgramHeaderIterator = struct { const offset = it.phoff + size * it.index; try it.file_reader.seekTo(offset); - return takeProgramHeader(&it.file_reader.interface, it.is_64, it.endian); + return try takeProgramHeader(&it.file_reader.interface, it.is_64, it.endian); } }; @@ -731,7 +731,7 @@ pub const ProgramHeaderBufferIterator = struct { const offset = it.phoff + size * it.index; var reader = Io.Reader.fixed(it.buf[offset..]); - return takeProgramHeader(&reader, it.is_64, it.endian); + return try takeProgramHeader(&reader, it.is_64, it.endian); } }; @@ -771,7 +771,7 @@ pub const SectionHeaderIterator = struct { const offset = it.shoff + size * it.index; try it.file_reader.seekTo(offset); - return takeSectionHeader(&it.file_reader.interface, it.is_64, it.endian); + return try takeSectionHeader(&it.file_reader.interface, it.is_64, it.endian); } }; @@ -793,7 +793,7 @@ pub const SectionHeaderBufferIterator = struct { if (offset > it.buf.len) return error.EndOfStream; var reader = Io.Reader.fixed(it.buf[@intCast(offset)..]); - return takeSectionHeader(&reader, it.is_64, it.endian); + return try takeSectionHeader(&reader, it.is_64, it.endian); } }; @@ -826,12 +826,12 @@ pub const DynamicSectionIterator = struct { file_reader: *Io.File.Reader, - pub fn next(it: *SectionHeaderIterator) !?Elf64_Dyn { + pub fn next(it: *DynamicSectionIterator) !?Elf64_Dyn { if (it.offset >= it.end_offset) return null; const size: u64 = if (it.is_64) @sizeOf(Elf64_Dyn) else @sizeOf(Elf32_Dyn); defer it.offset += size; try it.file_reader.seekTo(it.offset); - return takeDynamicSection(&it.file_reader.interface, it.is_64, it.endian); + return try takeDynamicSection(&it.file_reader.interface, it.is_64, it.endian); } }; diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 3e3577bbf0..c2ddf98627 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1,6 +1,11 @@ +//! Deprecated in favor of `Io.Dir`. const Dir = @This(); + const builtin = @import("builtin"); +const native_os = builtin.os.tag; + const std = @import("../std.zig"); +const Io = std.Io; const File = std.fs.File; const AtomicFile = std.fs.AtomicFile; const base64_encoder = fs.base64_encoder; @@ -12,7 +17,6 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const linux = std.os.linux; const windows = std.os.windows; -const native_os = builtin.os.tag; const have_flock = @TypeOf(posix.system.flock) != void; fd: Handle, @@ -1189,84 +1193,41 @@ pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) return file; } -pub const MakeError = posix.MakeDirError; +/// Deprecated in favor of `Io.Dir.MakeError`. +pub const MakeError = Io.Dir.MakeError; -/// Creates a single directory with a relative or absolute path. -/// To create multiple directories to make an entire path, see `makePath`. -/// To operate on only absolute paths, see `makeDirAbsolute`. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDir(self: Dir, sub_path: []const u8) MakeError!void { - try posix.mkdirat(self.fd, sub_path, default_mode); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.makeDir(.{ .handle = self.fd }, io, sub_path); } -/// Same as `makeDir`, but `sub_path` is null-terminated. -/// To create multiple directories to make an entire path, see `makePath`. -/// To operate on only absolute paths, see `makeDirAbsoluteZ`. +/// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) MakeError!void { try posix.mkdiratZ(self.fd, sub_path, default_mode); } -/// Creates a single directory with a relative or absolute null-terminated WTF-16 LE-encoded path. -/// To create multiple directories to make an entire path, see `makePath`. -/// To operate on only absolute paths, see `makeDirAbsoluteW`. +/// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) MakeError!void { try posix.mkdiratW(self.fd, mem.span(sub_path), default_mode); } -/// Calls makeDir iteratively to make an entire path -/// (i.e. creating any parent directories that do not exist). -/// Returns success if the path already exists and is a directory. -/// This function is not atomic, and if it returns an error, the file system may -/// have been modified regardless. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -/// Fails on an empty path with `error.BadPathName` as that is not a path that can be created. -/// -/// Paths containing `..` components are handled differently depending on the platform: -/// - On Windows, `..` are resolved before the path is passed to NtCreateFile, meaning -/// a `sub_path` like "first/../second" will resolve to "second" and only a -/// `./second` directory will be created. -/// - On other platforms, `..` are not resolved before the path is passed to `mkdirat`, -/// meaning a `sub_path` like "first/../second" will create both a `./first` -/// and a `./second` directory. -pub fn makePath(self: Dir, sub_path: []const u8) (MakeError || StatFileError)!void { +/// Deprecated in favor of `Io.Dir.makePath`. +pub fn makePath(self: Dir, sub_path: []const u8) MakePathError!void { _ = try self.makePathStatus(sub_path); } -pub const MakePathStatus = enum { existed, created }; -/// Same as `makePath` except returns whether the path already existed or was successfully created. -pub fn makePathStatus(self: Dir, sub_path: []const u8) (MakeError || StatFileError)!MakePathStatus { - var it = try fs.path.componentIterator(sub_path); - var status: MakePathStatus = .existed; - var component = it.last() orelse return error.BadPathName; - while (true) { - if (self.makeDir(component.path)) |_| { - status = .created; - } else |err| switch (err) { - error.PathAlreadyExists => { - // stat the file and return an error if it's not a directory - // this is important because otherwise a dangling symlink - // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = self.statFile(component.path) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } - }, - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - else => |e| return e, - } - component = it.next() orelse return status; - } +/// Deprecated in favor of `Io.Dir.MakePathStatus`. +pub const MakePathStatus = Io.Dir.MakePathStatus; +/// Deprecated in favor of `Io.Dir.MakePathError`. +pub const MakePathError = Io.Dir.MakePathError; + +/// Deprecated in favor of `Io.Dir.makePathStatus`. +pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathStatus { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path); } /// Windows only. Calls makeOpenDirAccessMaskW iteratively to make an entire path @@ -2052,20 +2013,11 @@ pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { return windows.ReadLink(self.fd, sub_path_w, buffer); } -/// Read all of file contents using a preallocated buffer. -/// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` -/// the situation is ambiguous. It could either mean that the entire file was read, and -/// it exactly fits the buffer, or it could mean the buffer was not big enough for the -/// entire file. -/// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `file_path` should be encoded as valid UTF-8. -/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.readFile`. pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { - var file = try self.openFile(file_path, .{}); - defer file.close(); - - const end_index = try file.readAll(buffer); - return buffer[0..end_index]; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.readFile(.{ .handle = self.fd }, io, file_path, buffer); } pub const ReadFileAllocError = File.OpenError || File.ReadError || Allocator.Error || error{ @@ -2091,7 +2043,7 @@ pub fn readFileAlloc( /// Used to allocate the result. gpa: Allocator, /// If reached or exceeded, `error.StreamTooLong` is returned instead. - limit: std.Io.Limit, + limit: Io.Limit, ) ReadFileAllocError![]u8 { return readFileAllocOptions(dir, sub_path, gpa, limit, .of(u8), null); } @@ -2101,6 +2053,8 @@ pub fn readFileAlloc( /// /// If the file size is already known, a better alternative is to initialize a /// `File.Reader`. +/// +/// TODO move this function to Io.Dir pub fn readFileAllocOptions( dir: Dir, /// On Windows, should be encoded as [WTF-8](https://wtf-8.codeberg.page/). @@ -2110,13 +2064,16 @@ pub fn readFileAllocOptions( /// Used to allocate the result. gpa: Allocator, /// If reached or exceeded, `error.StreamTooLong` is returned instead. - limit: std.Io.Limit, + limit: Io.Limit, comptime alignment: std.mem.Alignment, comptime sentinel: ?u8, ) ReadFileAllocError!(if (sentinel) |s| [:s]align(alignment.toByteUnits()) u8 else []align(alignment.toByteUnits()) u8) { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + var file = try dir.openFile(sub_path, .{}); defer file.close(); - var file_reader = file.reader(&.{}); + var file_reader = file.reader(io, &.{}); return file_reader.interface.allocRemainingAlignedSentinel(gpa, limit, alignment, sentinel) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, error.OutOfMemory, error.StreamTooLong => |e| return e, @@ -2647,6 +2604,8 @@ pub const CopyFileError = File.OpenError || File.StatError || /// [WTF-8](https://wtf-8.codeberg.page/). On WASI, both paths should be /// encoded as valid UTF-8. On other platforms, both paths are an opaque /// sequence of bytes with no particular encoding. +/// +/// TODO move this function to Io.Dir pub fn copyFile( source_dir: Dir, source_path: []const u8, @@ -2654,11 +2613,15 @@ pub fn copyFile( dest_path: []const u8, options: CopyFileOptions, ) CopyFileError!void { - var file_reader: File.Reader = .init(try source_dir.openFile(source_path, .{}), &.{}); - defer file_reader.file.close(); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + + const file = try source_dir.openFile(source_path, .{}); + var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{}); + defer file_reader.file.close(io); const mode = options.override_mode orelse blk: { - const st = try file_reader.file.stat(); + const st = try file_reader.file.stat(io); file_reader.size = st.size; break :blk st.mode; }; @@ -2708,6 +2671,7 @@ pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) pub const Stat = File.Stat; pub const StatError = File.StatError; +/// Deprecated in favor of `Io.Dir.stat`. pub fn stat(self: Dir) StatError!Stat { const file: File = .{ .handle = self.fd }; return file.stat(); @@ -2715,17 +2679,7 @@ pub fn stat(self: Dir) StatError!Stat { pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError; -/// Returns metadata for a file inside the directory. -/// -/// On Windows, this requires three syscalls. On other operating systems, it -/// only takes one. -/// -/// Symlinks are followed. -/// -/// `sub_path` may be absolute, in which case `self` is ignored. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.statPath`. pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { if (native_os == .windows) { var file = try self.openFile(sub_path, .{}); @@ -2799,3 +2753,11 @@ pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!v const file: File = .{ .handle = self.fd }; try file.setPermissions(permissions); } + +pub fn adaptToNewApi(dir: Dir) Io.Dir { + return .{ .handle = dir.fd }; +} + +pub fn adaptFromNewApi(dir: Io.Dir) Dir { + return .{ .fd = dir.handle }; +} diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 7c94fc1df6..3de4d9bcb4 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -858,10 +858,12 @@ pub const Writer = struct { }; } - pub fn moveToReader(w: *Writer) Reader { + /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted + pub fn moveToReader(w: *Writer, io: std.Io) Reader { defer w.* = undefined; return .{ - .file = w.file, + .io = io, + .file = .{ .handle = w.file.handle }, .mode = w.mode, .pos = w.pos, .interface = Reader.initInterface(w.interface.buffer), @@ -1350,15 +1352,15 @@ pub const Writer = struct { /// /// Positional is more threadsafe, since the global seek position is not /// affected. -pub fn reader(file: File, buffer: []u8) Reader { - return .init(file, buffer); +pub fn reader(file: File, io: std.Io, buffer: []u8) Reader { + return .init(.{ .handle = file.handle }, io, buffer); } /// Positional is more threadsafe, since the global seek position is not /// affected, but when such syscalls are not available, preemptively /// initializing in streaming mode skips a failed syscall. -pub fn readerStreaming(file: File, buffer: []u8) Reader { - return .initStreaming(file, buffer); +pub fn readerStreaming(file: File, io: std.Io, buffer: []u8) Reader { + return .initStreaming(.{ .handle = file.handle }, io, buffer); } /// Defaults to positional reading; falls back to streaming. @@ -1538,3 +1540,11 @@ pub fn downgradeLock(file: File) LockError!void { }; } } + +pub fn adaptToNewApi(file: File) std.Io.File { + return .{ .handle = file.handle }; +} + +pub fn adaptFromNewApi(file: std.Io.File) File { + return .{ .handle = file.handle }; +} diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index e9c341d39a..e60e3c7911 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1,10 +1,12 @@ -const std = @import("../std.zig"); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../std.zig"); +const Io = std.Io; const testing = std.testing; const fs = std.fs; const mem = std.mem; const wasi = std.os.wasi; -const native_os = builtin.os.tag; const windows = std.os.windows; const posix = std.posix; @@ -73,6 +75,7 @@ const PathType = enum { }; const TestContext = struct { + io: Io, path_type: PathType, path_sep: u8, arena: ArenaAllocator, @@ -83,6 +86,7 @@ const TestContext = struct { pub fn init(path_type: PathType, path_sep: u8, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext { const tmp = tmpDir(.{ .iterate = true }); return .{ + .io = testing.io, .path_type = path_type, .path_sep = path_sep, .arena = ArenaAllocator.init(allocator), @@ -1319,6 +1323,8 @@ test "max file name component lengths" { } test "writev, readv" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1327,78 +1333,55 @@ test "writev, readv" { var buf1: [line1.len]u8 = undefined; var buf2: [line2.len]u8 = undefined; - var write_vecs = [_]posix.iovec_const{ - .{ - .base = line1, - .len = line1.len, - }, - .{ - .base = line2, - .len = line2.len, - }, - }; - var read_vecs = [_]posix.iovec{ - .{ - .base = &buf2, - .len = buf2.len, - }, - .{ - .base = &buf1, - .len = buf1.len, - }, - }; + var write_vecs: [2][]const u8 = .{ line1, line2 }; + var read_vecs: [2][]u8 = .{ &buf2, &buf1 }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); - try src_file.writevAll(&write_vecs); + var writer = src_file.writerStreaming(&.{}); + + try writer.interface.writeVecAll(&write_vecs); + try writer.interface.flush(); try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos()); - try src_file.seekTo(0); - const read = try src_file.readvAll(&read_vecs); - try testing.expectEqual(@as(usize, line1.len + line2.len), read); + + var reader = writer.moveToReader(io); + try reader.seekTo(0); + try reader.interface.readVecAll(&read_vecs); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); + try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1)); } test "pwritev, preadv" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); const line1 = "line1\n"; const line2 = "line2\n"; - + var lines: [2][]const u8 = .{ line1, line2 }; var buf1: [line1.len]u8 = undefined; var buf2: [line2.len]u8 = undefined; - var write_vecs = [_]posix.iovec_const{ - .{ - .base = line1, - .len = line1.len, - }, - .{ - .base = line2, - .len = line2.len, - }, - }; - var read_vecs = [_]posix.iovec{ - .{ - .base = &buf2, - .len = buf2.len, - }, - .{ - .base = &buf1, - .len = buf1.len, - }, - }; + var read_vecs: [2][]u8 = .{ &buf2, &buf1 }; var src_file = try tmp.dir.createFile("test.txt", .{ .read = true }); defer src_file.close(); - try src_file.pwritevAll(&write_vecs, 16); + var writer = src_file.writer(&.{}); + + try writer.seekTo(16); + try writer.interface.writeVecAll(&lines); + try writer.interface.flush(); try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos()); - const read = try src_file.preadvAll(&read_vecs, 16); - try testing.expectEqual(@as(usize, line1.len + line2.len), read); + + var reader = writer.moveToReader(io); + try reader.seekTo(16); + try reader.interface.readVecAll(&read_vecs); try testing.expectEqualStrings(&buf1, "line2\n"); try testing.expectEqualStrings(&buf2, "line1\n"); + try testing.expectError(error.EndOfStream, reader.interface.readSliceAll(&buf1)); } test "setEndPos" { @@ -1406,6 +1389,8 @@ test "setEndPos" { if (native_os == .wasi and builtin.link_libc) return error.SkipZigTest; if (builtin.cpu.arch.isMIPS64() and (builtin.abi == .gnuabin32 or builtin.abi == .muslabin32)) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/23806 + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1416,11 +1401,13 @@ test "setEndPos" { const initial_size = try f.getEndPos(); var buffer: [32]u8 = undefined; + var reader = f.reader(io, &.{}); { try f.setEndPos(initial_size); try testing.expectEqual(initial_size, try f.getEndPos()); - try testing.expectEqual(initial_size, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(initial_size, reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]); } @@ -1428,7 +1415,8 @@ test "setEndPos" { const larger = initial_size + 4; try f.setEndPos(larger); try testing.expectEqual(larger, try f.getEndPos()); - try testing.expectEqual(larger, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(larger, reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]); } @@ -1436,25 +1424,21 @@ test "setEndPos" { const smaller = initial_size - 5; try f.setEndPos(smaller); try testing.expectEqual(smaller, try f.getEndPos()); - try testing.expectEqual(smaller, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(smaller, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("nine", buffer[0..@intCast(smaller)]); } try f.setEndPos(0); try testing.expectEqual(0, try f.getEndPos()); - try testing.expectEqual(0, try f.preadAll(&buffer, 0)); + try reader.seekTo(0); + try testing.expectEqual(0, try reader.interface.readSliceShort(&buffer)); // Invalid file length should error gracefully. Actual limit is host // and file-system dependent, but 1PB should fail on filesystems like // EXT4 and NTFS. But XFS or Btrfs support up to 8EiB files. - f.setEndPos(0x4_0000_0000_0000) catch |err| if (err != error.FileTooBig) { - return err; - }; - - f.setEndPos(std.math.maxInt(u63)) catch |err| if (err != error.FileTooBig) { - return err; - }; - + try testing.expectError(error.FileTooBig, f.setEndPos(0x4_0000_0000_0000)); + try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u63))); try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u63) + 1)); try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u64))); } @@ -1560,31 +1544,6 @@ test "sendfile with buffered data" { try std.testing.expectEqualSlices(u8, "AAAA", written_buf[0..amt]); } -test "copyRangeAll" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - try tmp.dir.makePath("os_test_tmp"); - - var dir = try tmp.dir.openDir("os_test_tmp", .{}); - defer dir.close(); - - var src_file = try dir.createFile("file1.txt", .{ .read = true }); - defer src_file.close(); - - const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP"; - try src_file.writeAll(data); - - var dest_file = try dir.createFile("file2.txt", .{ .read = true }); - defer dest_file.close(); - - var written_buf: [100]u8 = undefined; - _ = try src_file.copyRangeAll(0, dest_file, 0, data.len); - - const amt = try dest_file.preadAll(&written_buf, 0); - try testing.expectEqualStrings(data, written_buf[0..amt]); -} - test "copyFile" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { @@ -1708,8 +1667,8 @@ test "open file with exclusive lock twice, make sure second lock waits" { } }; - var started = std.Thread.ResetEvent{}; - var locked = std.Thread.ResetEvent{}; + var started: std.Thread.ResetEvent = .unset; + var locked: std.Thread.ResetEvent = .unset; const t = try std.Thread.spawn(.{}, S.checkFn, .{ &ctx.dir, @@ -1773,7 +1732,7 @@ test "read from locked file" { const f = try ctx.dir.createFile(filename, .{ .read = true }); defer f.close(); var buffer: [1]u8 = undefined; - _ = try f.readAll(&buffer); + _ = try f.read(&buffer); } { const f = try ctx.dir.createFile(filename, .{ @@ -1785,9 +1744,9 @@ test "read from locked file" { defer f2.close(); var buffer: [1]u8 = undefined; if (builtin.os.tag == .windows) { - try std.testing.expectError(error.LockViolation, f2.readAll(&buffer)); + try std.testing.expectError(error.LockViolation, f2.read(&buffer)); } else { - try std.testing.expectEqual(0, f2.readAll(&buffer)); + try std.testing.expectEqual(0, f2.read(&buffer)); } } } @@ -1944,6 +1903,7 @@ test "'.' and '..' in fs.Dir functions" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { + const io = ctx.io; const subdir_path = try ctx.transformPath("./subdir"); const file_path = try ctx.transformPath("./subdir/../file"); const copy_path = try ctx.transformPath("./subdir/../copy"); @@ -1966,7 +1926,8 @@ test "'.' and '..' in fs.Dir functions" { try ctx.dir.deleteFile(rename_path); try ctx.dir.writeFile(.{ .sub_path = update_path, .data = "something" }); - const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{}); + var dir = ctx.dir.adaptToNewApi(); + const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{}); try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); try ctx.dir.deleteDir(subdir_path); @@ -2005,13 +1966,6 @@ test "'.' and '..' in absolute functions" { renamed_file.close(); try fs.deleteFileAbsolute(renamed_file_path); - const update_file_path = try fs.path.join(allocator, &.{ subdir_path, "../update" }); - const update_file = try fs.createFileAbsolute(update_file_path, .{}); - try update_file.writeAll("something"); - update_file.close(); - const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{}); - try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); - try fs.deleteDirAbsolute(subdir_path); } @@ -2079,6 +2033,7 @@ test "invalid UTF-8/WTF-8 paths" { try testWithAllSupportedPathTypes(struct { fn impl(ctx: *TestContext) !void { + const io = ctx.io; // This is both invalid UTF-8 and WTF-8, since \xFF is an invalid start byte const invalid_path = try ctx.transformPath("\xFF"); @@ -2129,7 +2084,8 @@ test "invalid UTF-8/WTF-8 paths" { try testing.expectError(expected_err, ctx.dir.access(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.accessZ(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.updateFile(invalid_path, ctx.dir, invalid_path, .{})); + var dir = ctx.dir.adaptToNewApi(); + try testing.expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.copyFile(invalid_path, ctx.dir, invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.statFile(invalid_path)); @@ -2144,7 +2100,6 @@ test "invalid UTF-8/WTF-8 paths" { try testing.expectError(expected_err, fs.renameZ(ctx.dir, invalid_path, ctx.dir, invalid_path)); if (native_os != .wasi and ctx.path_type != .relative) { - try testing.expectError(expected_err, fs.updateFileAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path)); try testing.expectError(expected_err, fs.makeDirAbsoluteZ(invalid_path)); @@ -2175,6 +2130,8 @@ test "invalid UTF-8/WTF-8 paths" { } test "read file non vectored" { + const io = std.testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2188,7 +2145,7 @@ test "read file non vectored" { try file_writer.interface.flush(); } - var file_reader: std.fs.File.Reader = .init(file, &.{}); + var file_reader: std.Io.File.Reader = .initAdapted(file, io, &.{}); var write_buffer: [100]u8 = undefined; var w: std.Io.Writer = .fixed(&write_buffer); @@ -2205,6 +2162,8 @@ test "read file non vectored" { } test "seek keeping partial buffer" { + const io = std.testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2219,7 +2178,7 @@ test "seek keeping partial buffer" { } var read_buffer: [3]u8 = undefined; - var file_reader: std.fs.File.Reader = .init(file, &read_buffer); + var file_reader: Io.File.Reader = .initAdapted(file, io, &read_buffer); try testing.expectEqual(0, file_reader.logicalPos()); @@ -2246,13 +2205,15 @@ test "seek keeping partial buffer" { } test "seekBy" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); try tmp_dir.dir.writeFile(.{ .sub_path = "blah.txt", .data = "let's test seekBy" }); const f = try tmp_dir.dir.openFile("blah.txt", .{ .mode = .read_only }); defer f.close(); - var reader = f.readerStreaming(&.{}); + var reader = f.readerStreaming(io, &.{}); try reader.seekBy(2); var buffer: [20]u8 = undefined; diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index a701c09a90..dbd547611f 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -247,6 +247,7 @@ pub const Connection = struct { port: u16, stream: Io.net.Stream, ) error{OutOfMemory}!*Plain { + const io = client.io; const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.bytes.len); const base = try gpa.alignedAlloc(u8, .of(Plain), alloc_len); @@ -260,8 +261,8 @@ pub const Connection = struct { plain.* = .{ .connection = .{ .client = client, - .stream_writer = stream.writer(socket_write_buffer), - .stream_reader = stream.reader(socket_read_buffer), + .stream_writer = stream.writer(io, socket_write_buffer), + .stream_reader = stream.reader(io, socket_read_buffer), .pool_node = .{}, .port = port, .host_len = @intCast(remote_host.bytes.len), @@ -300,6 +301,7 @@ pub const Connection = struct { port: u16, stream: Io.net.Stream, ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { + const io = client.io; const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.bytes.len); const base = try gpa.alignedAlloc(u8, .of(Tls), alloc_len); @@ -316,11 +318,14 @@ pub const Connection = struct { assert(base.ptr + alloc_len == socket_read_buffer.ptr + socket_read_buffer.len); @memcpy(host_buffer, remote_host.bytes); const tls: *Tls = @ptrCast(base); + var random_buffer: [176]u8 = undefined; + std.crypto.random.bytes(&random_buffer); + const now_ts = if (Io.Timestamp.now(io, .real)) |ts| ts.toSeconds() else |_| return error.TlsInitializationFailed; tls.* = .{ .connection = .{ .client = client, - .stream_writer = stream.writer(tls_write_buffer), - .stream_reader = stream.reader(socket_read_buffer), + .stream_writer = stream.writer(io, tls_write_buffer), + .stream_reader = stream.reader(io, socket_read_buffer), .pool_node = .{}, .port = port, .host_len = @intCast(remote_host.bytes.len), @@ -338,6 +343,8 @@ pub const Connection = struct { .ssl_key_log = client.ssl_key_log, .read_buffer = tls_read_buffer, .write_buffer = socket_write_buffer, + .entropy = &random_buffer, + .realtime_now_seconds = now_ts, // This is appropriate for HTTPS because the HTTP headers contain // the content length which is used to detect truncation attacks. .allow_truncation_attacks = true, @@ -1390,16 +1397,8 @@ pub const basic_authorization = struct { }; pub const ConnectTcpError = error{ - ConnectionRefused, - NetworkUnreachable, - ConnectionTimedOut, - ConnectionResetByPeer, - TemporaryNameServerFailure, - NameServerFailure, - UnknownHostName, - UnexpectedConnectFailure, TlsInitializationFailed, -} || Allocator.Error || Io.Cancelable; +} || Allocator.Error || HostName.ConnectError; /// Reuses a `Connection` if one matching `host` and `port` is already open. /// @@ -1424,6 +1423,7 @@ pub const ConnectTcpOptions = struct { }; pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcpError!*Connection { + const io = client.io; const host = options.host; const port = options.port; const protocol = options.protocol; @@ -1437,22 +1437,17 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp .protocol = protocol, })) |conn| return conn; - const stream = host.connect(client.io, port, .{ .mode = .stream }) catch |err| switch (err) { - error.ConnectionRefused => return error.ConnectionRefused, - error.NetworkUnreachable => return error.NetworkUnreachable, - error.ConnectionTimedOut => return error.ConnectionTimedOut, - error.ConnectionResetByPeer => return error.ConnectionResetByPeer, - error.NameServerFailure => return error.NameServerFailure, - error.UnknownHostName => return error.UnknownHostName, - error.Canceled => return error.Canceled, - //else => return error.UnexpectedConnectFailure, - }; - errdefer stream.close(); + var stream = try host.connect(io, port, .{ .mode = .stream }); + errdefer stream.close(io); switch (protocol) { .tls => { if (disable_tls) return error.TlsInitializationFailed; - const tc = try Connection.Tls.create(client, proxied_host, proxied_port, stream); + const tc = Connection.Tls.create(client, proxied_host, proxied_port, stream) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.Unexpected => |e| return e, + error.UnsupportedClock => return error.TlsInitializationFailed, + }; client.connection_pool.addUsed(&tc.connection); return &tc.connection; }, diff --git a/lib/std/os/linux/IoUring.zig b/lib/std/os/linux/IoUring.zig index 25d4d88fd0..eaaa7643a9 100644 --- a/lib/std/os/linux/IoUring.zig +++ b/lib/std/os/linux/IoUring.zig @@ -3,7 +3,7 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const mem = std.mem; -const net = std.net; +const net = std.Io.net; const posix = std.posix; const linux = std.os.linux; const testing = std.testing; @@ -2361,19 +2361,22 @@ test "sendmsg/recvmsg" { }; defer ring.deinit(); - var address_server = try net.Address.parseIp4("127.0.0.1", 0); + var address_server: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; - const server = try posix.socket(address_server.any.family, posix.SOCK.DGRAM, 0); + const server = try posix.socket(address_server.family, posix.SOCK.DGRAM, 0); defer posix.close(server); try posix.setsockopt(server, posix.SOL.SOCKET, posix.SO.REUSEPORT, &mem.toBytes(@as(c_int, 1))); try posix.setsockopt(server, posix.SOL.SOCKET, posix.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); - try posix.bind(server, &address_server.any, address_server.getOsSockLen()); + try posix.bind(server, addrAny(&address_server), @sizeOf(linux.sockaddr.in)); // set address_server to the OS-chosen IP/port. - var slen: posix.socklen_t = address_server.getOsSockLen(); - try posix.getsockname(server, &address_server.any, &slen); + var slen: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(server, addrAny(&address_server), &slen); - const client = try posix.socket(address_server.any.family, posix.SOCK.DGRAM, 0); + const client = try posix.socket(address_server.family, posix.SOCK.DGRAM, 0); defer posix.close(client); const buffer_send = [_]u8{42} ** 128; @@ -2381,8 +2384,8 @@ test "sendmsg/recvmsg" { posix.iovec_const{ .base = &buffer_send, .len = buffer_send.len }, }; const msg_send: posix.msghdr_const = .{ - .name = &address_server.any, - .namelen = address_server.getOsSockLen(), + .name = addrAny(&address_server), + .namelen = @sizeOf(linux.sockaddr.in), .iov = &iovecs_send, .iovlen = 1, .control = null, @@ -2398,11 +2401,13 @@ test "sendmsg/recvmsg" { var iovecs_recv = [_]posix.iovec{ posix.iovec{ .base = &buffer_recv, .len = buffer_recv.len }, }; - const addr = [_]u8{0} ** 4; - var address_recv = net.Address.initIp4(addr, 0); + var address_recv: linux.sockaddr.in = .{ + .port = 0, + .addr = 0, + }; var msg_recv: posix.msghdr = .{ - .name = &address_recv.any, - .namelen = address_recv.getOsSockLen(), + .name = addrAny(&address_recv), + .namelen = @sizeOf(linux.sockaddr.in), .iov = &iovecs_recv, .iovlen = 1, .control = null, @@ -2441,6 +2446,8 @@ test "sendmsg/recvmsg" { test "timeout (after a relative time)" { if (!is_linux) return error.SkipZigTest; + const io = testing.io; + var ring = IoUring.init(1, 0) catch |err| switch (err) { error.SystemOutdated => return error.SkipZigTest, error.PermissionDenied => return error.SkipZigTest, @@ -2452,12 +2459,12 @@ test "timeout (after a relative time)" { const margin = 5; const ts: linux.kernel_timespec = .{ .sec = 0, .nsec = ms * 1000000 }; - const started = std.time.milliTimestamp(); + const started = try std.Io.Timestamp.now(io, .awake); const sqe = try ring.timeout(0x55555555, &ts, 0, 0); try testing.expectEqual(linux.IORING_OP.TIMEOUT, sqe.opcode); try testing.expectEqual(@as(u32, 1), try ring.submit()); const cqe = try ring.copy_cqe(); - const stopped = std.time.milliTimestamp(); + const stopped = try std.Io.Timestamp.now(io, .awake); try testing.expectEqual(linux.io_uring_cqe{ .user_data = 0x55555555, @@ -2466,7 +2473,8 @@ test "timeout (after a relative time)" { }, cqe); // Tests should not depend on timings: skip test if outside margin. - if (!std.math.approxEqAbs(f64, ms, @as(f64, @floatFromInt(stopped - started)), margin)) return error.SkipZigTest; + const ms_elapsed = started.durationTo(stopped).toMilliseconds(); + if (ms_elapsed > margin) return error.SkipZigTest; } test "timeout (after a number of completions)" { @@ -2861,19 +2869,22 @@ test "shutdown" { }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; // Socket bound, expect shutdown to work { - const server = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const server = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); defer posix.close(server); try posix.setsockopt(server, posix.SOL.SOCKET, posix.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); - try posix.bind(server, &address.any, address.getOsSockLen()); + try posix.bind(server, addrAny(&address), @sizeOf(linux.sockaddr.in)); try posix.listen(server, 1); // set address to the OS-chosen IP/port. - var slen: posix.socklen_t = address.getOsSockLen(); - try posix.getsockname(server, &address.any, &slen); + var slen: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(server, addrAny(&address), &slen); const shutdown_sqe = try ring.shutdown(0x445445445, server, linux.SHUT.RD); try testing.expectEqual(linux.IORING_OP.SHUTDOWN, shutdown_sqe.opcode); @@ -2898,7 +2909,7 @@ test "shutdown" { // Socket not bound, expect to fail with ENOTCONN { - const server = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const server = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); defer posix.close(server); const shutdown_sqe = ring.shutdown(0x445445445, server, linux.SHUT.RD) catch |err| switch (err) { @@ -2966,22 +2977,11 @@ test "renameat" { }, cqe); // Validate that the old file doesn't exist anymore - { - _ = tmp.dir.openFile(old_path, .{}) catch |err| switch (err) { - error.FileNotFound => {}, - else => std.debug.panic("unexpected error: {}", .{err}), - }; - } + try testing.expectError(error.FileNotFound, tmp.dir.openFile(old_path, .{})); // Validate that the new file exists with the proper content - { - const new_file = try tmp.dir.openFile(new_path, .{}); - defer new_file.close(); - - var new_file_data: [16]u8 = undefined; - const bytes_read = try new_file.readAll(&new_file_data); - try testing.expectEqualStrings("hello", new_file_data[0..bytes_read]); - } + var new_file_data: [16]u8 = undefined; + try testing.expectEqualStrings("hello", try tmp.dir.readFile(new_path, &new_file_data)); } test "unlinkat" { @@ -3179,12 +3179,8 @@ test "linkat" { }, cqe); // Validate the second file - const second_file = try tmp.dir.openFile(second_path, .{}); - defer second_file.close(); - var second_file_data: [16]u8 = undefined; - const bytes_read = try second_file.readAll(&second_file_data); - try testing.expectEqualStrings("hello", second_file_data[0..bytes_read]); + try testing.expectEqualStrings("hello", try tmp.dir.readFile(second_path, &second_file_data)); } test "provide_buffers: read" { @@ -3588,7 +3584,10 @@ const SocketTestHarness = struct { fn createSocketTestHarness(ring: *IoUring) !SocketTestHarness { // Create a TCP server socket - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; const listener_socket = try createListenerSocket(&address); errdefer posix.close(listener_socket); @@ -3598,9 +3597,9 @@ fn createSocketTestHarness(ring: *IoUring) !SocketTestHarness { _ = try ring.accept(0xaaaaaaaa, listener_socket, &accept_addr, &accept_addr_len, 0); // Create a TCP client socket - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); errdefer posix.close(client); - _ = try ring.connect(0xcccccccc, client, &address.any, address.getOsSockLen()); + _ = try ring.connect(0xcccccccc, client, addrAny(&address), @sizeOf(linux.sockaddr.in)); try testing.expectEqual(@as(u32, 2), try ring.submit()); @@ -3636,18 +3635,18 @@ fn createSocketTestHarness(ring: *IoUring) !SocketTestHarness { }; } -fn createListenerSocket(address: *net.Address) !posix.socket_t { +fn createListenerSocket(address: *linux.sockaddr.in) !posix.socket_t { const kernel_backlog = 1; - const listener_socket = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const listener_socket = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); errdefer posix.close(listener_socket); try posix.setsockopt(listener_socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &mem.toBytes(@as(c_int, 1))); - try posix.bind(listener_socket, &address.any, address.getOsSockLen()); + try posix.bind(listener_socket, addrAny(address), @sizeOf(linux.sockaddr.in)); try posix.listen(listener_socket, kernel_backlog); // set address to the OS-chosen IP/port. - var slen: posix.socklen_t = address.getOsSockLen(); - try posix.getsockname(listener_socket, &address.any, &slen); + var slen: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(listener_socket, addrAny(address), &slen); return listener_socket; } @@ -3662,7 +3661,10 @@ test "accept multishot" { }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; const listener_socket = try createListenerSocket(&address); defer posix.close(listener_socket); @@ -3676,9 +3678,9 @@ test "accept multishot" { var nr: usize = 4; // number of clients to connect while (nr > 0) : (nr -= 1) { // connect client - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); errdefer posix.close(client); - try posix.connect(client, &address.any, address.getOsSockLen()); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); // test accept completion var cqe = try ring.copy_cqe(); @@ -3756,7 +3758,10 @@ test "accept_direct" { else => return err, }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; // register direct file descriptors var registered_fds = [_]posix.fd_t{-1} ** 2; @@ -3779,8 +3784,8 @@ test "accept_direct" { try testing.expectEqual(@as(u32, 1), try ring.submit()); // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // accept completion @@ -3813,8 +3818,8 @@ test "accept_direct" { _ = try ring.accept_direct(accept_userdata, listener_socket, null, null, 0); try testing.expectEqual(@as(u32, 1), try ring.submit()); // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // completion with error const cqe_accept = try ring.copy_cqe(); @@ -3837,7 +3842,10 @@ test "accept_multishot_direct" { }; defer ring.deinit(); - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; var registered_fds = [_]posix.fd_t{-1} ** 2; try ring.register_files(registered_fds[0..]); @@ -3855,8 +3863,8 @@ test "accept_multishot_direct" { for (registered_fds) |_| { // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // accept completion @@ -3870,8 +3878,8 @@ test "accept_multishot_direct" { // Multishot is terminated (more flag is not set). { // connect - const client = try posix.socket(address.any.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); - try posix.connect(client, &address.any, address.getOsSockLen()); + const client = try posix.socket(address.family, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0); + try posix.connect(client, addrAny(&address), @sizeOf(linux.sockaddr.in)); defer posix.close(client); // completion with error const cqe_accept = try ring.copy_cqe(); @@ -3944,7 +3952,10 @@ test "socket_direct/socket_direct_alloc/close_direct" { try testing.expect(cqe_socket.res == 2); // returns registered file index // use sockets from registered_fds in connect operation - var address = try net.Address.parseIp4("127.0.0.1", 0); + var address: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; const listener_socket = try createListenerSocket(&address); defer posix.close(listener_socket); const accept_userdata: u64 = 0xaaaaaaaa; @@ -3954,7 +3965,7 @@ test "socket_direct/socket_direct_alloc/close_direct" { // prepare accept _ = try ring.accept(accept_userdata, listener_socket, null, null, 0); // prepare connect with fixed socket - const connect_sqe = try ring.connect(connect_userdata, @intCast(fd_index), &address.any, address.getOsSockLen()); + const connect_sqe = try ring.connect(connect_userdata, @intCast(fd_index), addrAny(&address), @sizeOf(linux.sockaddr.in)); connect_sqe.flags |= linux.IOSQE_FIXED_FILE; // fd is fixed file index // submit both try testing.expectEqual(@as(u32, 2), try ring.submit()); @@ -4483,12 +4494,15 @@ test "bind/listen/connect" { // LISTEN is higher required operation if (!probe.is_supported(.LISTEN)) return error.SkipZigTest; - var addr = net.Address.initIp4([4]u8{ 127, 0, 0, 1 }, 0); - const proto: u32 = if (addr.any.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP; + var addr: linux.sockaddr.in = .{ + .port = 0, + .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), + }; + const proto: u32 = if (addr.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP; const listen_fd = brk: { // Create socket - _ = try ring.socket(1, addr.any.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); + _ = try ring.socket(1, addr.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); try testing.expectEqual(1, try ring.submit()); var cqe = try ring.copy_cqe(); try testing.expectEqual(1, cqe.user_data); @@ -4500,7 +4514,7 @@ test "bind/listen/connect" { var optval: u32 = 1; (try ring.setsockopt(2, listen_fd, linux.SOL.SOCKET, linux.SO.REUSEADDR, mem.asBytes(&optval))).link_next(); (try ring.setsockopt(3, listen_fd, linux.SOL.SOCKET, linux.SO.REUSEPORT, mem.asBytes(&optval))).link_next(); - (try ring.bind(4, listen_fd, &addr.any, addr.getOsSockLen(), 0)).link_next(); + (try ring.bind(4, listen_fd, addrAny(&addr), @sizeOf(linux.sockaddr.in), 0)).link_next(); _ = try ring.listen(5, listen_fd, 1, 0); // Submit 4 operations try testing.expectEqual(4, try ring.submit()); @@ -4521,15 +4535,15 @@ test "bind/listen/connect" { try testing.expectEqual(1, optval); // Read system assigned port into addr - var addr_len: posix.socklen_t = addr.getOsSockLen(); - try posix.getsockname(listen_fd, &addr.any, &addr_len); + var addr_len: posix.socklen_t = @sizeOf(linux.sockaddr.in); + try posix.getsockname(listen_fd, addrAny(&addr), &addr_len); break :brk listen_fd; }; const connect_fd = brk: { // Create connect socket - _ = try ring.socket(6, addr.any.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); + _ = try ring.socket(6, addr.family, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, proto, 0); try testing.expectEqual(1, try ring.submit()); const cqe = try ring.copy_cqe(); try testing.expectEqual(6, cqe.user_data); @@ -4542,7 +4556,7 @@ test "bind/listen/connect" { // Prepare accept/connect operations _ = try ring.accept(7, listen_fd, null, null, 0); - _ = try ring.connect(8, connect_fd, &addr.any, addr.getOsSockLen()); + _ = try ring.connect(8, connect_fd, addrAny(&addr), @sizeOf(linux.sockaddr.in)); try testing.expectEqual(2, try ring.submit()); // Get listener accepted socket var accept_fd: posix.socket_t = 0; @@ -4604,3 +4618,7 @@ fn testSendRecv(ring: *IoUring, send_fd: posix.socket_t, recv_fd: posix.socket_t try testing.expectEqualSlices(u8, buffer_send, buffer_recv[0..buffer_send.len]); try testing.expectEqualSlices(u8, buffer_send, buffer_recv[buffer_send.len..]); } + +fn addrAny(addr: *linux.sockaddr.in) *linux.sockaddr { + return @ptrCast(addr); +} diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 93676f2a74..c5e61749b2 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3000,31 +3000,7 @@ pub fn mkdiratW(dir_fd: fd_t, sub_path_w: []const u16, mode: mode_t) MakeDirErro windows.CloseHandle(sub_dir_handle); } -pub const MakeDirError = error{ - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to create a new directory relative to it. - AccessDenied, - PermissionDenied, - DiskQuota, - PathAlreadyExists, - SymLinkLoop, - LinkQuotaExceeded, - NameTooLong, - FileNotFound, - SystemResources, - NoSpaceLeft, - NotDir, - ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, - NoDevice, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, -} || UnexpectedError; +pub const MakeDirError = std.Io.Dir.MakeError; /// Create a directory. /// `mode` is ignored on Windows and WASI. diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index f8206b8a5a..b5ce476441 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -731,11 +731,8 @@ test "dup & dup2" { try dup2ed.writeAll("dup2"); } - var file = try tmp.dir.openFile("os_dup_test", .{}); - defer file.close(); - - var buf: [7]u8 = undefined; - try testing.expectEqualStrings("dupdup2", buf[0..try file.readAll(&buf)]); + var buffer: [8]u8 = undefined; + try testing.expectEqualStrings("dupdup2", try tmp.dir.readFile("os_dup_test", &buffer)); } test "writev longer than IOV_MAX" { diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 50157d52d9..c018d77424 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -1,5 +1,9 @@ -const std = @import("../std.zig"); +const ChildProcess = @This(); + const builtin = @import("builtin"); +const native_os = builtin.os.tag; + +const std = @import("../std.zig"); const unicode = std.unicode; const fs = std.fs; const process = std.process; @@ -11,9 +15,7 @@ const mem = std.mem; const EnvMap = std.process.EnvMap; const maxInt = std.math.maxInt; const assert = std.debug.assert; -const native_os = builtin.os.tag; const Allocator = std.mem.Allocator; -const ChildProcess = @This(); const ArrayList = std.ArrayList; pub const Id = switch (native_os) { @@ -317,16 +319,23 @@ pub fn waitForSpawn(self: *ChildProcess) SpawnError!void { const err_pipe = self.err_pipe orelse return; self.err_pipe = null; - // Wait for the child to report any errors in or before `execvpe`. - if (readIntFd(err_pipe)) |child_err_int| { - posix.close(err_pipe); + const report = readIntFd(err_pipe); + posix.close(err_pipe); + if (report) |child_err_int| { const child_err: SpawnError = @errorCast(@errorFromInt(child_err_int)); self.term = child_err; return child_err; - } else |_| { - // Write end closed by CLOEXEC at the time of the `execvpe` call, indicating success! - posix.close(err_pipe); + } else |read_err| switch (read_err) { + error.EndOfStream => { + // Write end closed by CLOEXEC at the time of the `execvpe` call, + // indicating success. + }, + else => { + // Problem reading the error from the error reporting pipe. We + // don't know if the child is alive or dead. Better to assume it is + // alive so the resource does not risk being leaked. + }, } } @@ -1014,8 +1023,14 @@ fn writeIntFd(fd: i32, value: ErrInt) !void { fn readIntFd(fd: i32) !ErrInt { var buffer: [8]u8 = undefined; - var fr: std.fs.File.Reader = .initStreaming(.{ .handle = fd }, &buffer); - return @intCast(fr.interface.takeInt(u64, .little) catch return error.SystemResources); + var i: usize = 0; + while (i < buffer.len) { + const n = try std.posix.read(fd, buffer[i..]); + if (n == 0) return error.EndOfStream; + i += n; + } + const int = mem.readInt(u64, &buffer, .little); + return @intCast(int); } const ErrInt = std.meta.Int(.unsigned, @sizeOf(anyerror) * 8); diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig index bffdb8ee7c..a48e8cc407 100644 --- a/lib/std/tar/Writer.zig +++ b/lib/std/tar/Writer.zig @@ -1,7 +1,9 @@ +const Writer = @This(); + const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const testing = std.testing; -const Writer = @This(); const block_size = @sizeOf(Header); @@ -14,7 +16,7 @@ pub const Options = struct { mtime: u64 = 0, }; -underlying_writer: *std.Io.Writer, +underlying_writer: *Io.Writer, prefix: []const u8 = "", mtime_now: u64 = 0, @@ -36,12 +38,12 @@ pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void { try w.writeHeader(.directory, sub_path, "", 0, options); } -pub const WriteFileError = std.Io.Writer.FileError || Error || std.fs.File.Reader.SizeError; +pub const WriteFileError = Io.Writer.FileError || Error || Io.File.Reader.SizeError; pub fn writeFile( w: *Writer, sub_path: []const u8, - file_reader: *std.fs.File.Reader, + file_reader: *Io.File.Reader, stat_mtime: i128, ) WriteFileError!void { const size = try file_reader.getSize(); @@ -58,7 +60,7 @@ pub fn writeFile( try w.writePadding64(size); } -pub const WriteFileStreamError = Error || std.Io.Reader.StreamError; +pub const WriteFileStreamError = Error || Io.Reader.StreamError; /// Writes file reading file content from `reader`. Reads exactly `size` bytes /// from `reader`, or returns `error.EndOfStream`. @@ -66,7 +68,7 @@ pub fn writeFileStream( w: *Writer, sub_path: []const u8, size: u64, - reader: *std.Io.Reader, + reader: *Io.Reader, options: Options, ) WriteFileStreamError!void { try w.writeHeader(.regular, sub_path, "", size, options); @@ -136,15 +138,15 @@ fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const [ try w.writePadding(len); } -fn writePadding(w: *Writer, bytes: usize) std.Io.Writer.Error!void { +fn writePadding(w: *Writer, bytes: usize) Io.Writer.Error!void { return writePaddingPos(w, bytes % block_size); } -fn writePadding64(w: *Writer, bytes: u64) std.Io.Writer.Error!void { +fn writePadding64(w: *Writer, bytes: u64) Io.Writer.Error!void { return writePaddingPos(w, @intCast(bytes % block_size)); } -fn writePaddingPos(w: *Writer, pos: usize) std.Io.Writer.Error!void { +fn writePaddingPos(w: *Writer, pos: usize) Io.Writer.Error!void { if (pos == 0) return; try w.underlying_writer.splatByteAll(0, block_size - pos); } @@ -153,7 +155,7 @@ fn writePaddingPos(w: *Writer, pos: usize) std.Io.Writer.Error!void { /// "reasonable system must not assume that such a block exists when reading an /// archive". Therefore, the Zig standard library recommends to not call this /// function. -pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void { +pub fn finishPedantically(w: *Writer) Io.Writer.Error!void { try w.underlying_writer.splatByteAll(0, block_size * 2); } @@ -248,7 +250,7 @@ pub const Header = extern struct { try octal(&w.checksum, checksum); } - pub fn write(h: *Header, bw: *std.Io.Writer) error{ OctalOverflow, WriteFailed }!void { + pub fn write(h: *Header, bw: *Io.Writer) error{ OctalOverflow, WriteFailed }!void { try h.updateChecksum(); try bw.writeAll(std.mem.asBytes(h)); } @@ -396,14 +398,14 @@ test "write files" { { const root = "root"; - var output: std.Io.Writer.Allocating = .init(testing.allocator); + var output: Io.Writer.Allocating = .init(testing.allocator); var w: Writer = .{ .underlying_writer = &output.writer }; defer output.deinit(); try w.setRoot(root); for (files) |file| try w.writeFileBytes(file.path, file.content, .{}); - var input: std.Io.Reader = .fixed(output.written()); + var input: Io.Reader = .fixed(output.written()); var it: std.tar.Iterator = .init(&input, .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer, @@ -424,7 +426,7 @@ test "write files" { try testing.expectEqual('/', actual.name[root.len..][0]); try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]); - var content: std.Io.Writer.Allocating = .init(testing.allocator); + var content: Io.Writer.Allocating = .init(testing.allocator); defer content.deinit(); try it.streamRemaining(actual, &content.writer); try testing.expectEqualSlices(u8, expected.content, content.written()); @@ -432,15 +434,15 @@ test "write files" { } // without root { - var output: std.Io.Writer.Allocating = .init(testing.allocator); + var output: Io.Writer.Allocating = .init(testing.allocator); var w: Writer = .{ .underlying_writer = &output.writer }; defer output.deinit(); for (files) |file| { - var content: std.Io.Reader = .fixed(file.content); + var content: Io.Reader = .fixed(file.content); try w.writeFileStream(file.path, file.content.len, &content, .{}); } - var input: std.Io.Reader = .fixed(output.written()); + var input: Io.Reader = .fixed(output.written()); var it: std.tar.Iterator = .init(&input, .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer, @@ -452,7 +454,7 @@ test "write files" { const expected = files[i]; try testing.expectEqualStrings(expected.path, actual.name); - var content: std.Io.Writer.Allocating = .init(testing.allocator); + var content: Io.Writer.Allocating = .init(testing.allocator); defer content.deinit(); try it.streamRemaining(actual, &content.writer); try testing.expectEqualSlices(u8, expected.content, content.written()); diff --git a/lib/std/zig.zig b/lib/std/zig.zig index 04e5c2b221..6d3ccc9b22 100644 --- a/lib/std/zig.zig +++ b/lib/std/zig.zig @@ -559,7 +559,7 @@ test isUnderscore { /// If the source can be UTF-16LE encoded, this function asserts that `gpa` /// will align a byte-sized allocation to at least 2. Allocators that don't do /// this are rare. -pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *std.fs.File.Reader) ![:0]u8 { +pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *Io.File.Reader) ![:0]u8 { var buffer: std.ArrayList(u8) = .empty; defer buffer.deinit(gpa); diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 43d64205a7..7318612151 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -442,6 +442,7 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { error.DeviceBusy, error.InputOutput, error.LockViolation, + error.FileSystem, error.UnableToOpenElfFile, error.UnhelpfulFile, @@ -542,16 +543,15 @@ fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: T return null; } -pub const AbiAndDynamicLinkerFromFileError = error{}; - -pub fn abiAndDynamicLinkerFromFile( +fn abiAndDynamicLinkerFromFile( file_reader: *Io.File.Reader, header: *const elf.Header, cpu: Target.Cpu, os: Target.Os, ld_info_list: []const LdInfo, query: Target.Query, -) AbiAndDynamicLinkerFromFileError!Target { +) !Target { + const io = file_reader.io; var result: Target = .{ .cpu = cpu, .os = os, @@ -623,8 +623,8 @@ pub fn abiAndDynamicLinkerFromFile( try file_reader.seekTo(shstr.sh_offset); try file_reader.interface.readSliceAll(shstrtab); const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: { - var it = header.iterateSectionHeaders(&file_reader.interface); - while (it.next()) |shdr| { + var it = header.iterateSectionHeaders(file_reader); + while (try it.next()) |shdr| { const end = mem.findScalarPos(u8, shstrtab, shdr.sh_name, 0) orelse continue; const sh_name = shstrtab[shdr.sh_name..end :0]; if (mem.eql(u8, sh_name, ".dynstr")) break :find_dyn_str .{ @@ -645,7 +645,7 @@ pub fn abiAndDynamicLinkerFromFile( var it = mem.tokenizeScalar(u8, rpath_list, ':'); while (it.next()) |rpath| { - if (glibcVerFromRPath(rpath)) |ver| { + if (glibcVerFromRPath(io, rpath)) |ver| { result.os.version_range.linux.glibc = ver; return result; } else |err| switch (err) { @@ -660,7 +660,7 @@ pub fn abiAndDynamicLinkerFromFile( // There is no DT_RUNPATH so we try to find libc.so.6 inside the same // directory as the dynamic linker. if (fs.path.dirname(dl_path)) |rpath| { - if (glibcVerFromRPath(rpath)) |ver| { + if (glibcVerFromRPath(io, rpath)) |ver| { result.os.version_range.linux.glibc = ver; return result; } else |err| switch (err) { @@ -725,7 +725,7 @@ pub fn abiAndDynamicLinkerFromFile( @memcpy(path_buf[index..][0..abi.len], abi); index += abi.len; const rpath = path_buf[0..index]; - if (glibcVerFromRPath(rpath)) |ver| { + if (glibcVerFromRPath(io, rpath)) |ver| { result.os.version_range.linux.glibc = ver; return result; } else |err| switch (err) { @@ -842,18 +842,13 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { error.InvalidElfMagic, error.InvalidElfEndian, error.InvalidElfClass, - error.InvalidElfFile, error.InvalidElfVersion, error.InvalidGnuLibCVersion, error.EndOfStream, => return error.GLibCNotFound, - error.SystemResources, - error.UnableToReadElfFile, - error.Unexpected, - error.FileSystem, - error.ProcessNotFound, - => |e| return e, + error.ReadFailed => return file_reader.err.?, + else => |e| return e, }; } @@ -867,8 +862,8 @@ fn glibcVerFromSoFile(file_reader: *Io.File.Reader) !std.SemanticVersion { try file_reader.seekTo(shstr.sh_offset); try file_reader.interface.readSliceAll(shstrtab); const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: { - var it = header.iterateSectionHeaders(&file_reader.interface); - while (it.next()) |shdr| { + var it = header.iterateSectionHeaders(file_reader); + while (try it.next()) |shdr| { const end = mem.findScalarPos(u8, shstrtab, shdr.sh_name, 0) orelse continue; const sh_name = shstrtab[shdr.sh_name..end :0]; if (mem.eql(u8, sh_name, ".dynstr")) break :find_dyn_str .{ @@ -882,19 +877,25 @@ fn glibcVerFromSoFile(file_reader: *Io.File.Reader) !std.SemanticVersion { // strings that start with "GLIBC_2." indicate the existence of such a glibc version, // and furthermore, that the system-installed glibc is at minimum that version. var max_ver: std.SemanticVersion = .{ .major = 2, .minor = 2, .patch = 5 }; - + var offset: u64 = 0; try file_reader.seekTo(dynstr.offset); - while (file_reader.interface.takeSentinel(0)) |s| { - if (mem.startsWith(u8, s, "GLIBC_2.")) { - const chopped = s["GLIBC_".len..]; - const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) { - error.Overflow => return error.InvalidGnuLibCVersion, - error.InvalidVersion => return error.InvalidGnuLibCVersion, - }; - switch (ver.order(max_ver)) { - .gt => max_ver = ver, - .lt, .eq => continue, + while (offset < dynstr.size) { + if (file_reader.interface.takeSentinel(0)) |s| { + if (mem.startsWith(u8, s, "GLIBC_2.")) { + const chopped = s["GLIBC_".len..]; + const ver = Target.Query.parseVersion(chopped) catch |err| switch (err) { + error.Overflow => return error.InvalidGnuLibCVersion, + error.InvalidVersion => return error.InvalidGnuLibCVersion, + }; + switch (ver.order(max_ver)) { + .gt => max_ver = ver, + .lt, .eq => continue, + } } + offset += s.len + 1; + } else |err| switch (err) { + error.EndOfStream, error.StreamTooLong => break, + error.ReadFailed => |e| return e, } } @@ -1091,22 +1092,12 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, error.ProcessNotFound, + error.Canceled, => |e| return e, error.ReadFailed => return file_reader.err.?, - error.UnableToReadElfFile, - error.InvalidElfClass, - error.InvalidElfVersion, - error.InvalidElfEndian, - error.InvalidElfFile, - error.InvalidElfMagic, - error.Unexpected, - error.EndOfStream, - error.NameTooLong, - error.StaticElfFile, - // Finally, we fall back on the standard path. - => |e| { + else => |e| { std.log.warn("encountered {t}; falling back to default ABI and dynamic linker", .{e}); return defaultAbiAndDynamicLinker(cpu, os, query); }, diff --git a/test/src/Cases.zig b/test/src/Cases.zig index 7edb66aede..9f134075b4 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -455,8 +455,7 @@ pub fn lowerToBuildSteps( parent_step: *std.Build.Step, options: CaseTestOptions, ) void { - const host = std.zig.system.resolveTargetQuery(.{}) catch |err| - std.debug.panic("unable to detect native host: {s}\n", .{@errorName(err)}); + const host = b.resolveTargetQuery(.{}); const cases_dir_path = b.build_root.join(b.allocator, &.{ "test", "cases" }) catch @panic("OOM"); for (self.cases.items) |case| { @@ -587,7 +586,7 @@ pub fn lowerToBuildSteps( }, .Execution => |expected_stdout| no_exec: { const run = if (case.target.result.ofmt == .c) run_step: { - if (getExternalExecutor(&host, &case.target.result, .{ .link_libc = true }) != .native) { + if (getExternalExecutor(&host.result, &case.target.result, .{ .link_libc = true }) != .native) { // We wouldn't be able to run the compiled C code. break :no_exec; } @@ -972,14 +971,6 @@ const TestManifest = struct { } }; -fn resolveTargetQuery(query: std.Target.Query) std.Build.ResolvedTarget { - return .{ - .query = query, - .target = std.zig.system.resolveTargetQuery(query) catch - @panic("unable to resolve target query"), - }; -} - fn knownFileExtension(filename: []const u8) bool { // List taken from `Compilation.classifyFileExt` in the compiler. for ([_][]const u8{ From 69b54b0cd12fb90f8ff101bc66e433a81d4b8409 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 8 Oct 2025 19:48:11 -0700 Subject: [PATCH 079/244] remove bad assert --- lib/std/Io/File.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 7d87fab973..dddbf3e66e 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -487,7 +487,6 @@ pub const Reader = struct { fn readVecPositional(r: *Reader, data: [][]u8) Io.Reader.Error!usize { const io = r.io; - assert(r.interface.bufferedLen() == 0); var iovecs_buffer: [max_buffers_len][]u8 = undefined; const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, data); const dest = iovecs_buffer[0..dest_n]; From 89412fda775aecdedf4047355f2c45b48334a285 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 8 Oct 2025 20:35:34 -0700 Subject: [PATCH 080/244] std.Io: implement fileStat --- lib/std/Io/File.zig | 94 +++----------------- lib/std/Io/Threaded.zig | 155 ++++++++++++++++++++++++++++++++- lib/std/debug.zig | 4 +- lib/std/debug/ElfFile.zig | 3 +- lib/std/debug/SelfInfo/Elf.zig | 1 + lib/std/fs/File.zig | 92 +++++++------------ lib/std/posix.zig | 9 +- 7 files changed, 202 insertions(+), 156 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index dddbf3e66e..018825164e 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -47,90 +47,14 @@ pub const Stat = struct { kind: Kind, /// Last access time in nanoseconds, relative to UTC 1970-01-01. + /// TODO change this to Io.Timestamp except don't waste storage on clock atime: i128, /// Last modification time in nanoseconds, relative to UTC 1970-01-01. + /// TODO change this to Io.Timestamp except don't waste storage on clock mtime: i128, /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. + /// TODO change this to Io.Timestamp except don't waste storage on clock ctime: i128, - - pub fn fromPosix(st: std.posix.Stat) Stat { - const atime = st.atime(); - const mtime = st.mtime(); - const ctime = st.ctime(); - return .{ - .inode = st.ino, - .size = @bitCast(st.size), - .mode = st.mode, - .kind = k: { - const m = st.mode & std.posix.S.IFMT; - switch (m) { - std.posix.S.IFBLK => break :k .block_device, - std.posix.S.IFCHR => break :k .character_device, - std.posix.S.IFDIR => break :k .directory, - std.posix.S.IFIFO => break :k .named_pipe, - std.posix.S.IFLNK => break :k .sym_link, - std.posix.S.IFREG => break :k .file, - std.posix.S.IFSOCK => break :k .unix_domain_socket, - else => {}, - } - if (builtin.os.tag == .illumos) switch (m) { - std.posix.S.IFDOOR => break :k .door, - std.posix.S.IFPORT => break :k .event_port, - else => {}, - }; - - break :k .unknown; - }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, - }; - } - - pub fn fromLinux(stx: std.os.linux.Statx) Stat { - const atime = stx.atime; - const mtime = stx.mtime; - const ctime = stx.ctime; - - return .{ - .inode = stx.ino, - .size = stx.size, - .mode = stx.mode, - .kind = switch (stx.mode & std.os.linux.S.IFMT) { - std.os.linux.S.IFDIR => .directory, - std.os.linux.S.IFCHR => .character_device, - std.os.linux.S.IFBLK => .block_device, - std.os.linux.S.IFREG => .file, - std.os.linux.S.IFIFO => .named_pipe, - std.os.linux.S.IFLNK => .sym_link, - std.os.linux.S.IFSOCK => .unix_domain_socket, - else => .unknown, - }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, - }; - } - - pub fn fromWasi(st: std.os.wasi.filestat_t) Stat { - return .{ - .inode = st.ino, - .size = @bitCast(st.size), - .mode = 0, - .kind = switch (st.filetype) { - .BLOCK_DEVICE => .block_device, - .CHARACTER_DEVICE => .character_device, - .DIRECTORY => .directory, - .SYMBOLIC_LINK => .sym_link, - .REGULAR_FILE => .file, - .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, - else => .unknown, - }, - .atime = st.atim, - .mtime = st.mtim, - .ctime = st.ctim, - }; - } }; pub fn stdout() File { @@ -145,13 +69,17 @@ pub fn stdin() File { return .{ .handle = if (is_windows) std.os.windows.peb().ProcessParameters.hStdInput else std.posix.STDIN_FILENO }; } -pub const StatError = std.posix.FStatError || Io.Cancelable; +pub const StatError = error{ + SystemResources, + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to get its filestat information. + AccessDenied, + PermissionDenied, +} || Io.Cancelable || Io.UnexpectedError; /// Returns `Stat` containing basic information about the `File`. pub fn stat(file: File, io: Io) StatError!Stat { - _ = file; - _ = io; - @panic("TODO"); + return io.vtable.fileStat(io.userdata, file); } pub const OpenFlags = std.fs.File.OpenFlags; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 0f217c7f80..16e134251c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -163,7 +163,12 @@ pub fn io(pool: *Pool) Io { .dirMake = dirMake, .dirStat = dirStat, .dirStatPath = dirStatPath, - .fileStat = fileStat, + .fileStat = switch (builtin.os.tag) { + .linux => fileStatLinux, + .windows => fileStatWindows, + .wasi => fileStatWasi, + else => fileStatPosix, + }, .createFile = createFile, .fileOpen = fileOpen, .fileClose = fileClose, @@ -781,14 +786,80 @@ fn dirStatPath(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir. @panic("TODO"); } -fn fileStat(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { +fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; + while (true) { + try pool.checkCancel(); + var stat = std.mem.zeroes(posix.Stat); + switch (posix.errno(fstat_sym(file.handle, &stat))) { + .SUCCESS => return statFromPosix(&stat), + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const linux = std.os.linux; + while (true) { + try pool.checkCancel(); + var statx = std.mem.zeroes(linux.Statx); + const rc = linux.statx( + file.handle, + "", + linux.AT.EMPTY_PATH, + linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, + &statx, + ); + switch (linux.E.init(rc)) { + .SUCCESS => return statFromLinux(&statx), + .INTR => continue, + .ACCES => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .LOOP => |err| return errnoBug(err), + .NAMETOOLONG => |err| return errnoBug(err), + .NOENT => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + .NOTDIR => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); - _ = file; @panic("TODO"); } +fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { + if (builtin.link_libc) return fileStatPosix(userdata, file); + const pool: *Pool = @ptrCast(@alignCast(userdata)); + while (true) { + try pool.checkCancel(); + var stat: std.os.wasi.filestat_t = undefined; + switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) { + .SUCCESS => return statFromWasi(&stat), + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn createFile( userdata: ?*anyopaque, dir: Io.Dir, @@ -2114,3 +2185,81 @@ fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t { .cpu_thread => .THREAD_CPUTIME_ID, }; } + +fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat { + const atime = stx.atime; + const mtime = stx.mtime; + const ctime = stx.ctime; + return .{ + .inode = stx.ino, + .size = stx.size, + .mode = stx.mode, + .kind = switch (stx.mode & std.os.linux.S.IFMT) { + std.os.linux.S.IFDIR => .directory, + std.os.linux.S.IFCHR => .character_device, + std.os.linux.S.IFBLK => .block_device, + std.os.linux.S.IFREG => .file, + std.os.linux.S.IFIFO => .named_pipe, + std.os.linux.S.IFLNK => .sym_link, + std.os.linux.S.IFSOCK => .unix_domain_socket, + else => .unknown, + }, + .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, + .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, + .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, + }; +} + +fn statFromPosix(st: *const std.posix.Stat) Io.File.Stat { + const atime = st.atime(); + const mtime = st.mtime(); + const ctime = st.ctime(); + return .{ + .inode = st.ino, + .size = @bitCast(st.size), + .mode = st.mode, + .kind = k: { + const m = st.mode & std.posix.S.IFMT; + switch (m) { + std.posix.S.IFBLK => break :k .block_device, + std.posix.S.IFCHR => break :k .character_device, + std.posix.S.IFDIR => break :k .directory, + std.posix.S.IFIFO => break :k .named_pipe, + std.posix.S.IFLNK => break :k .sym_link, + std.posix.S.IFREG => break :k .file, + std.posix.S.IFSOCK => break :k .unix_domain_socket, + else => {}, + } + if (builtin.os.tag == .illumos) switch (m) { + std.posix.S.IFDOOR => break :k .door, + std.posix.S.IFPORT => break :k .event_port, + else => {}, + }; + + break :k .unknown; + }, + .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, + .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, + .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, + }; +} + +fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat { + return .{ + .inode = st.ino, + .size = @bitCast(st.size), + .mode = 0, + .kind = switch (st.filetype) { + .BLOCK_DEVICE => .block_device, + .CHARACTER_DEVICE => .character_device, + .DIRECTORY => .directory, + .SYMBOLIC_LINK => .sym_link, + .REGULAR_FILE => .file, + .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, + else => .unknown, + }, + .atime = st.atim, + .mtime = st.mtim, + .ctime = st.ctim, + }; +} diff --git a/lib/std/debug.zig b/lib/std/debug.zig index f6287135e5..7e09bfec8a 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -82,6 +82,7 @@ pub const SelfInfoError = error{ /// The required debug info could not be read from disk due to some IO error. ReadFailed, OutOfMemory, + Canceled, Unexpected, }; @@ -691,6 +692,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri error.UnsupportedDebugInfo => "unwind info unsupported", error.ReadFailed => "filesystem error", error.OutOfMemory => "out of memory", + error.Canceled => "operation canceled", error.Unexpected => "unexpected error", }; if (it.stratOk(options.allow_unsafe_unwind)) { @@ -1079,7 +1081,7 @@ fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer, error.UnsupportedDebugInfo, error.InvalidDebugInfo, => .unknown, - error.ReadFailed, error.Unexpected => s: { + error.ReadFailed, error.Unexpected, error.Canceled => s: { tty_config.setColor(writer, .dim) catch {}; try writer.print("Failed to read debug info from filesystem, trace may be incomplete\n\n", .{}); tty_config.setColor(writer, .reset) catch {}; diff --git a/lib/std/debug/ElfFile.zig b/lib/std/debug/ElfFile.zig index 5be5ee55c5..2062772533 100644 --- a/lib/std/debug/ElfFile.zig +++ b/lib/std/debug/ElfFile.zig @@ -108,6 +108,7 @@ pub const LoadError = error{ LockedMemoryLimitExceeded, ProcessFdQuotaExceeded, SystemFdQuotaExceeded, + Canceled, Unexpected, }; @@ -408,7 +409,7 @@ fn loadInner( arena: Allocator, elf_file: std.fs.File, opt_crc: ?u32, -) (LoadError || error{CrcMismatch})!LoadInnerResult { +) (LoadError || error{ CrcMismatch, Canceled })!LoadInnerResult { const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: { const file_len = std.math.cast( usize, diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index 0f3c46e980..035ed584b2 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -336,6 +336,7 @@ const Module = struct { var elf_file = load_result catch |err| switch (err) { error.OutOfMemory, error.Unexpected, + error.Canceled, => |e| return e, error.Overflow, diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 3de4d9bcb4..191920dc83 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -1,10 +1,12 @@ +const File = @This(); + const builtin = @import("builtin"); -const Os = std.builtin.Os; const native_os = builtin.os.tag; const is_windows = native_os == .windows; -const File = @This(); const std = @import("../std.zig"); +const Io = std.Io; +const Os = std.builtin.Os; const Allocator = std.mem.Allocator; const posix = std.posix; const math = std.math; @@ -17,12 +19,12 @@ const Alignment = std.mem.Alignment; /// The OS-specific file descriptor or file handle. handle: Handle, -pub const Handle = std.Io.File.Handle; -pub const Mode = std.Io.File.Mode; -pub const INode = std.Io.File.INode; +pub const Handle = Io.File.Handle; +pub const Mode = Io.File.Mode; +pub const INode = Io.File.INode; pub const Uid = posix.uid_t; pub const Gid = posix.gid_t; -pub const Kind = std.Io.File.Kind; +pub const Kind = Io.File.Kind; /// This is the default mode given to POSIX operating systems for creating /// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first, @@ -386,7 +388,7 @@ pub fn mode(self: File) ModeError!Mode { return (try self.stat()).mode; } -pub const Stat = std.Io.File.Stat; +pub const Stat = Io.File.Stat; pub const StatError = posix.FStatError; @@ -436,39 +438,9 @@ pub fn stat(self: File) StatError!Stat { }; } - if (builtin.os.tag == .wasi and !builtin.link_libc) { - const st = try std.os.fstat_wasi(self.handle); - return Stat.fromWasi(st); - } - - if (builtin.os.tag == .linux) { - var stx = std.mem.zeroes(linux.Statx); - - const rc = linux.statx( - self.handle, - "", - linux.AT.EMPTY_PATH, - linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, - &stx, - ); - - return switch (linux.E.init(rc)) { - .SUCCESS => Stat.fromLinux(stx), - .ACCES => unreachable, - .BADF => unreachable, - .FAULT => unreachable, - .INVAL => unreachable, - .LOOP => unreachable, - .NAMETOOLONG => unreachable, - .NOENT => unreachable, - .NOMEM => error.SystemResources, - .NOTDIR => unreachable, - else => |err| posix.unexpectedErrno(err), - }; - } - - const st = try posix.fstat(self.handle); - return Stat.fromPosix(st); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.File.stat(.{ .handle = self.handle }, io); } pub const ChmodError = posix.FChmodError; @@ -785,8 +757,8 @@ pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError return posix.pwritev(self.handle, iovecs, offset); } -/// Deprecated in favor of `std.Io.File.Reader`. -pub const Reader = std.Io.File.Reader; +/// Deprecated in favor of `Io.File.Reader`. +pub const Reader = Io.File.Reader; pub const Writer = struct { file: File, @@ -799,7 +771,7 @@ pub const Writer = struct { copy_file_range_err: ?CopyFileRangeError = null, fcopyfile_err: ?FcopyfileError = null, seek_err: ?Writer.SeekError = null, - interface: std.Io.Writer, + interface: Io.Writer, pub const Mode = Reader.Mode; @@ -845,13 +817,13 @@ pub const Writer = struct { }; } - pub fn initInterface(buffer: []u8) std.Io.Writer { + pub fn initInterface(buffer: []u8) Io.Writer { return .{ .vtable = &.{ .drain = drain, .sendFile = switch (builtin.zig_backend) { else => sendFile, - .stage2_aarch64 => std.Io.Writer.unimplementedSendFile, + .stage2_aarch64 => Io.Writer.unimplementedSendFile, }, }, .buffer = buffer, @@ -859,7 +831,7 @@ pub const Writer = struct { } /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted - pub fn moveToReader(w: *Writer, io: std.Io) Reader { + pub fn moveToReader(w: *Writer, io: Io) Reader { defer w.* = undefined; return .{ .io = io, @@ -871,7 +843,7 @@ pub const Writer = struct { }; } - pub fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { + pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); const handle = w.file.handle; const buffered = io_w.buffered(); @@ -1021,10 +993,10 @@ pub const Writer = struct { } pub fn sendFile( - io_w: *std.Io.Writer, - file_reader: *std.Io.File.Reader, - limit: std.Io.Limit, - ) std.Io.Writer.FileError!usize { + io_w: *Io.Writer, + file_reader: *Io.File.Reader, + limit: Io.Limit, + ) Io.Writer.FileError!usize { const reader_buffered = file_reader.interface.buffered(); if (reader_buffered.len >= @intFromEnum(limit)) return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered)); @@ -1288,16 +1260,16 @@ pub const Writer = struct { } fn sendFileBuffered( - io_w: *std.Io.Writer, - file_reader: *std.Io.File.Reader, + io_w: *Io.Writer, + file_reader: *Io.File.Reader, reader_buffered: []const u8, - ) std.Io.Writer.FileError!usize { + ) Io.Writer.FileError!usize { const n = try drain(io_w, &.{reader_buffered}, 1); file_reader.seekBy(@intCast(n)) catch return error.ReadFailed; return n; } - pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || std.Io.Writer.Error)!void { + pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void { try w.interface.flush(); try seekToUnbuffered(w, offset); } @@ -1321,7 +1293,7 @@ pub const Writer = struct { } } - pub const EndError = SetEndPosError || std.Io.Writer.Error; + pub const EndError = SetEndPosError || Io.Writer.Error; /// Flushes any buffered data and sets the end position of the file. /// @@ -1352,14 +1324,14 @@ pub const Writer = struct { /// /// Positional is more threadsafe, since the global seek position is not /// affected. -pub fn reader(file: File, io: std.Io, buffer: []u8) Reader { +pub fn reader(file: File, io: Io, buffer: []u8) Reader { return .init(.{ .handle = file.handle }, io, buffer); } /// Positional is more threadsafe, since the global seek position is not /// affected, but when such syscalls are not available, preemptively /// initializing in streaming mode skips a failed syscall. -pub fn readerStreaming(file: File, io: std.Io, buffer: []u8) Reader { +pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader { return .initStreaming(.{ .handle = file.handle }, io, buffer); } @@ -1541,10 +1513,10 @@ pub fn downgradeLock(file: File) LockError!void { } } -pub fn adaptToNewApi(file: File) std.Io.File { +pub fn adaptToNewApi(file: File) Io.File { return .{ .handle = file.handle }; } -pub fn adaptFromNewApi(file: std.Io.File) File { +pub fn adaptFromNewApi(file: Io.File) File { return .{ .handle = file.handle }; } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index c5e61749b2..e4cd040703 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -4458,14 +4458,7 @@ pub fn wait4(pid: pid_t, flags: u32, ru: ?*rusage) WaitPidResult { } } -pub const FStatError = error{ - SystemResources, - - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to get its filestat information. - AccessDenied, - PermissionDenied, -} || UnexpectedError; +pub const FStatError = std.Io.File.StatError; /// Return information about a file descriptor. pub fn fstat(fd: fd_t) FStatError!Stat { From ebcc6f166c9c34d00b750f26687c9ee36b243cb0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 8 Oct 2025 21:56:20 -0700 Subject: [PATCH 081/244] std.Io: bring back Timestamp but also keep Clock.Timestamp this feels better --- lib/compiler/build_runner.zig | 2 +- lib/std/Build/Cache.zig | 26 ++-- lib/std/Build/WebServer.zig | 17 +-- lib/std/Io.zig | 263 ++++++++++++++++++++-------------- lib/std/Io/Dir.zig | 2 +- lib/std/Io/File.zig | 10 +- lib/std/Io/Threaded.zig | 56 ++++---- lib/std/Io/net/HostName.zig | 12 +- lib/std/fs/File.zig | 16 +-- lib/std/http/Client.zig | 2 +- lib/std/tar/Writer.zig | 8 +- 11 files changed, 235 insertions(+), 179 deletions(-) diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index b6d771302c..545cab6083 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -556,7 +556,7 @@ pub fn main() !void { try run.thread_pool.init(thread_pool_options); defer run.thread_pool.deinit(); - const now = Io.Timestamp.now(io, .awake) catch |err| fatal("failed to collect timestamp: {t}", .{err}); + const now = Io.Clock.Timestamp.now(io, .awake) catch |err| fatal("failed to collect timestamp: {t}", .{err}); run.web_server = if (webui_listen) |listen_address| ws: { if (builtin.single_threaded) unreachable; // `fatal` above diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 8202c4dd15..8f88d840b8 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -21,7 +21,7 @@ io: Io, manifest_dir: fs.Dir, hash: HashHelper = .{}, /// This value is accessed from multiple threads, protected by mutex. -recent_problematic_timestamp: i128 = 0, +recent_problematic_timestamp: Io.Timestamp = .zero, mutex: std.Thread.Mutex = .{}, /// A set of strings such as the zig library directory or project source root, which @@ -155,7 +155,7 @@ pub const File = struct { pub const Stat = struct { inode: fs.File.INode, size: u64, - mtime: i128, + mtime: Io.Timestamp, pub fn fromFs(fs_stat: fs.File.Stat) Stat { return .{ @@ -330,7 +330,7 @@ pub const Manifest = struct { diagnostic: Diagnostic = .none, /// Keeps track of the last time we performed a file system write to observe /// what time the file system thinks it is, according to its own granularity. - recent_problematic_timestamp: i128 = 0, + recent_problematic_timestamp: Io.Timestamp = .zero, pub const Diagnostic = union(enum) { none, @@ -728,7 +728,7 @@ pub const Manifest = struct { file.stat = .{ .size = stat_size, .inode = stat_inode, - .mtime = stat_mtime, + .mtime = .{ .nanoseconds = stat_mtime }, }; file.bin_digest = file_bin_digest; break :f file; @@ -747,7 +747,7 @@ pub const Manifest = struct { .stat = .{ .size = stat_size, .inode = stat_inode, - .mtime = stat_mtime, + .mtime = .{ .nanoseconds = stat_mtime }, }, .bin_digest = file_bin_digest, }; @@ -780,7 +780,7 @@ pub const Manifest = struct { return error.CacheCheckFailed; }; const size_match = actual_stat.size == cache_hash_file.stat.size; - const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime; + const mtime_match = actual_stat.mtime.nanoseconds == cache_hash_file.stat.mtime.nanoseconds; const inode_match = actual_stat.inode == cache_hash_file.stat.inode; if (!size_match or !mtime_match or !inode_match) { @@ -792,7 +792,7 @@ pub const Manifest = struct { if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { // The actual file has an unreliable timestamp, force it to be hashed - cache_hash_file.stat.mtime = 0; + cache_hash_file.stat.mtime = .zero; cache_hash_file.stat.inode = 0; } @@ -848,10 +848,10 @@ pub const Manifest = struct { } } - fn isProblematicTimestamp(man: *Manifest, file_time: i128) bool { + fn isProblematicTimestamp(man: *Manifest, timestamp: Io.Timestamp) bool { // If the file_time is prior to the most recent problematic timestamp // then we don't need to access the filesystem. - if (file_time < man.recent_problematic_timestamp) + if (timestamp.nanoseconds < man.recent_problematic_timestamp.nanoseconds) return false; // Next we will check the globally shared Cache timestamp, which is accessed @@ -861,7 +861,7 @@ pub const Manifest = struct { // Save the global one to our local one to avoid locking next time. man.recent_problematic_timestamp = man.cache.recent_problematic_timestamp; - if (file_time < man.recent_problematic_timestamp) + if (timestamp.nanoseconds < man.recent_problematic_timestamp.nanoseconds) return false; // This flag prevents multiple filesystem writes for the same hit() call. @@ -879,7 +879,7 @@ pub const Manifest = struct { man.cache.recent_problematic_timestamp = man.recent_problematic_timestamp; } - return file_time >= man.recent_problematic_timestamp; + return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds; } fn populateFileHash(self: *Manifest, ch_file: *File) !void { @@ -904,7 +904,7 @@ pub const Manifest = struct { if (self.isProblematicTimestamp(ch_file.stat.mtime)) { // The actual file has an unreliable timestamp, force it to be hashed - ch_file.stat.mtime = 0; + ch_file.stat.mtime = .zero; ch_file.stat.inode = 0; } @@ -1040,7 +1040,7 @@ pub const Manifest = struct { if (self.isProblematicTimestamp(new_file.stat.mtime)) { // The actual file has an unreliable timestamp, force it to be hashed - new_file.stat.mtime = 0; + new_file.stat.mtime = .zero; new_file.stat.inode = 0; } diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 95338c9f08..50e304c950 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -11,7 +11,7 @@ tcp_server: ?net.Server, serve_thread: ?std.Thread, /// Uses `Io.Clock.awake`. -base_timestamp: i96, +base_timestamp: Io.Timestamp, /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. step_names_trailing: []u8, @@ -43,6 +43,8 @@ runner_request: ?RunnerRequest, /// on a fixed interval of this many milliseconds. const default_update_interval_ms = 500; +pub const base_clock: Io.Clock = .awake; + /// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`. pub fn notifyUpdate(ws: *WebServer) void { _ = ws.update_id.rmw(.Add, 1, .release); @@ -58,13 +60,13 @@ pub const Options = struct { root_prog_node: std.Progress.Node, watch: bool, listen_address: net.IpAddress, - base_timestamp: Io.Timestamp, + base_timestamp: Io.Clock.Timestamp, }; pub fn init(opts: Options) WebServer { // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` // instead of threads, so that the web server can function in single-threaded builds. comptime assert(!builtin.single_threaded); - assert(opts.base_timestamp.clock == .awake); + assert(opts.base_timestamp.clock == base_clock); const all_steps = opts.all_steps; @@ -109,7 +111,7 @@ pub fn init(opts: Options) WebServer { .tcp_server = null, .serve_thread = null, - .base_timestamp = opts.base_timestamp.nanoseconds, + .base_timestamp = opts.base_timestamp.raw, .step_names_trailing = step_names_trailing, .step_status_bits = step_status_bits, @@ -248,9 +250,8 @@ pub fn finishBuild(ws: *WebServer, opts: struct { pub fn now(s: *const WebServer) i64 { const io = s.graph.io; - const base: Io.Timestamp = .{ .nanoseconds = s.base_timestamp, .clock = .awake }; - const ts = Io.Timestamp.now(io, base.clock) catch base; - return @intCast(base.durationTo(ts).toNanoseconds()); + const ts = base_clock.now(io) catch s.base_timestamp; + return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds()); } fn accept(ws: *WebServer, stream: net.Stream) void { @@ -519,7 +520,7 @@ pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []cons if (cached_cwd_path == null) cached_cwd_path = try std.process.getCwdAlloc(gpa); break :cwd cached_cwd_path.?; }; - try archiver.writeFile(path.sub_path, &file_reader, stat.mtime); + try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); } // intentionally not calling `archiver.finishPedantically` diff --git a/lib/std/Io.zig b/lib/std/Io.zig index e569c4ec94..8d061ad022 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -669,7 +669,7 @@ pub const VTable = struct { fileSeekBy: *const fn (?*anyopaque, file: File, offset: i64) File.SeekError!void, fileSeekTo: *const fn (?*anyopaque, file: File, offset: u64) File.SeekError!void, - now: *const fn (?*anyopaque, Timestamp.Clock) Timestamp.Error!i96, + now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, @@ -705,118 +705,178 @@ pub const UnexpectedError = error{ pub const Dir = @import("Io/Dir.zig"); pub const File = @import("Io/File.zig"); -pub const Timestamp = struct { - nanoseconds: i96, - clock: Clock, - - pub const Clock = enum { - /// A settable system-wide clock that measures real (i.e. wall-clock) - /// time. This clock is affected by discontinuous jumps in the system - /// time (e.g., if the system administrator manually changes the - /// clock), and by frequency adjust‐ ments performed by NTP and similar - /// applications. - /// - /// This clock normally counts the number of seconds since 1970-01-01 - /// 00:00:00 Coordinated Universal Time (UTC) except that it ignores - /// leap seconds; near a leap second it is typically adjusted by NTP to - /// stay roughly in sync with UTC. - /// - /// The epoch is implementation-defined. For example NTFS/Windows uses - /// 1601-01-01. - real, - /// A nonsettable system-wide clock that represents time since some - /// unspecified point in the past. - /// - /// Monotonic: Guarantees that the time returned by consecutive calls - /// will not go backwards, but successive calls may return identical - /// (not-increased) time values. - /// - /// Not affected by discontinuous jumps in the system time (e.g., if - /// the system administrator manually changes the clock), but may be - /// affected by frequency adjustments. - /// - /// This clock expresses intent to **exclude time that the system is - /// suspended**. However, implementations may be unable to satisify - /// this, and may include that time. - /// - /// * On Linux, corresponds `CLOCK_MONOTONIC`. - /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`. - awake, - /// Identical to `awake` except it expresses intent to **include time - /// that the system is suspended**, however, due to limitations it may - /// behave identically to `awake`. - /// - /// * On Linux, corresponds `CLOCK_BOOTTIME`. - /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`. - boot, - /// Tracks the amount of CPU in user or kernel mode used by the calling - /// process. - cpu_process, - /// Tracks the amount of CPU in user or kernel mode used by the calling - /// thread. - cpu_thread, - }; - - pub fn durationTo(from: Timestamp, to: Timestamp) Duration { - assert(from.clock == to.clock); - return .{ .nanoseconds = to.nanoseconds - from.nanoseconds }; - } - - pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { - return .{ - .nanoseconds = from.nanoseconds + duration.nanoseconds, - .clock = from.clock, - }; - } +pub const Clock = enum { + /// A settable system-wide clock that measures real (i.e. wall-clock) + /// time. This clock is affected by discontinuous jumps in the system + /// time (e.g., if the system administrator manually changes the + /// clock), and by frequency adjust‐ ments performed by NTP and similar + /// applications. + /// + /// This clock normally counts the number of seconds since 1970-01-01 + /// 00:00:00 Coordinated Universal Time (UTC) except that it ignores + /// leap seconds; near a leap second it is typically adjusted by NTP to + /// stay roughly in sync with UTC. + /// + /// The epoch is implementation-defined. For example NTFS/Windows uses + /// 1601-01-01. + real, + /// A nonsettable system-wide clock that represents time since some + /// unspecified point in the past. + /// + /// Monotonic: Guarantees that the time returned by consecutive calls + /// will not go backwards, but successive calls may return identical + /// (not-increased) time values. + /// + /// Not affected by discontinuous jumps in the system time (e.g., if + /// the system administrator manually changes the clock), but may be + /// affected by frequency adjustments. + /// + /// This clock expresses intent to **exclude time that the system is + /// suspended**. However, implementations may be unable to satisify + /// this, and may include that time. + /// + /// * On Linux, corresponds `CLOCK_MONOTONIC`. + /// * On macOS, corresponds to `CLOCK_UPTIME_RAW`. + awake, + /// Identical to `awake` except it expresses intent to **include time + /// that the system is suspended**, however, due to limitations it may + /// behave identically to `awake`. + /// + /// * On Linux, corresponds `CLOCK_BOOTTIME`. + /// * On macOS, corresponds to `CLOCK_MONOTONIC_RAW`. + boot, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// process. + cpu_process, + /// Tracks the amount of CPU in user or kernel mode used by the calling + /// thread. + cpu_thread, pub const Error = error{UnsupportedClock} || UnexpectedError; /// This function is not cancelable because first of all it does not block, /// but more importantly, the cancelation logic itself may want to check /// the time. - pub fn now(io: Io, clock: Clock) Error!Timestamp { - return .{ - .nanoseconds = try io.vtable.now(io.userdata, clock), - .clock = clock, - }; + pub fn now(clock: Clock, io: Io) Error!Io.Timestamp { + return io.vtable.now(io.userdata, clock); } - pub fn fromNow(io: Io, clock: Clock, duration: Duration) Error!Timestamp { - const now_ts = try now(io, clock); - return addDuration(now_ts, duration); + pub const Timestamp = struct { + raw: Io.Timestamp, + clock: Clock, + + /// This function is not cancelable because first of all it does not block, + /// but more importantly, the cancelation logic itself may want to check + /// the time. + pub fn now(io: Io, clock: Clock) Error!Clock.Timestamp { + return .{ + .raw = try io.vtable.now(io.userdata, clock), + .clock = clock, + }; + } + + pub fn wait(t: Clock.Timestamp, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .deadline = t }); + } + + pub fn durationTo(from: Clock.Timestamp, to: Clock.Timestamp) Clock.Duration { + assert(from.clock == to.clock); + return .{ + .raw = from.raw.durationTo(to.raw), + .clock = from.clock, + }; + } + + pub fn addDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.addDuration(duration.raw), + .clock = from.clock, + }; + } + + pub fn fromNow(io: Io, duration: Clock.Duration) Error!Clock.Timestamp { + return .{ + .clock = duration.clock, + .raw = (try duration.clock.now(io)).addDuration(duration.raw), + }; + } + + pub fn untilNow(timestamp: Clock.Timestamp, io: Io) Error!Clock.Duration { + const now_ts = try Clock.Timestamp.now(io, timestamp.clock); + return timestamp.durationTo(now_ts); + } + + pub fn durationFromNow(timestamp: Clock.Timestamp, io: Io) Error!Clock.Duration { + const now_ts = try timestamp.clock.now(io); + return .{ + .clock = timestamp.clock, + .raw = now_ts.durationTo(timestamp.raw), + }; + } + + pub fn toClock(t: Clock.Timestamp, io: Io, clock: Clock) Error!Clock.Timestamp { + if (t.clock == clock) return t; + const now_old = try t.clock.now(io); + const now_new = try clock.now(io); + const duration = now_old.durationTo(t); + return .{ + .clock = clock, + .raw = now_new.addDuration(duration), + }; + } + + pub fn compare(lhs: Clock.Timestamp, op: std.math.CompareOperator, rhs: Clock.Timestamp) bool { + assert(lhs.clock == rhs.clock); + return std.math.compare(lhs.raw.nanoseconds, op, rhs.raw.nanoseconds); + } + }; + + pub const Duration = struct { + raw: Io.Duration, + clock: Clock, + + pub fn sleep(duration: Clock.Duration, io: Io) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .duration = duration }); + } + }; +}; + +pub const Timestamp = struct { + nanoseconds: i96, + + pub const zero: Timestamp = .{ .nanoseconds = 0 }; + + pub fn durationTo(from: Timestamp, to: Timestamp) Duration { + return .{ .nanoseconds = to.nanoseconds - from.nanoseconds }; } - pub fn untilNow(timestamp: Timestamp, io: Io) Error!Duration { - const now_ts = try Timestamp.now(io, timestamp.clock); - return timestamp.durationTo(now_ts); + pub fn addDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds + duration.nanoseconds }; } - pub fn durationFromNow(timestamp: Timestamp, io: Io) Error!Duration { - const now_ts = try now(io, timestamp.clock); - return now_ts.durationTo(timestamp); - } - - pub fn toClock(t: Timestamp, io: Io, clock: Clock) Error!Timestamp { - if (t.clock == clock) return t; - const now_old = try now(io, t.clock); - const now_new = try now(io, clock); - const duration = now_old.durationTo(t); - return now_new.addDuration(duration); - } - - pub fn compare(lhs: Timestamp, op: std.math.CompareOperator, rhs: Timestamp) bool { - assert(lhs.clock == rhs.clock); - return std.math.compare(lhs.nanoseconds, op, rhs.nanoseconds); + pub fn withClock(t: Timestamp, clock: Clock) Clock.Timestamp { + return .{ .nanoseconds = t.nanoseconds, .clock = clock }; } pub fn toSeconds(t: Timestamp) i64 { return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s)); } + + pub fn formatNumber(t: Timestamp, w: *std.Io.Writer, n: std.fmt.Number) std.Io.Writer.Error!void { + return w.printInt(t.nanoseconds, n.mode.base() orelse 10, n.case, .{ + .precision = n.precision, + .width = n.width, + .alignment = n.alignment, + .fill = n.fill, + }); + } }; pub const Duration = struct { nanoseconds: i96, + pub const zero: Duration = .{ .nanoseconds = 0 }; pub const max: Duration = .{ .nanoseconds = std.math.maxInt(i96) }; pub fn fromNanoseconds(x: i96) Duration { @@ -842,38 +902,29 @@ pub const Duration = struct { pub fn toNanoseconds(d: Duration) i96 { return d.nanoseconds; } - - pub fn sleep(duration: Duration, io: Io) SleepError!void { - return io.vtable.sleep(io.userdata, .{ .duration = .{ .duration = duration, .clock = .awake } }); - } }; /// Declares under what conditions an operation should return `error.Timeout`. pub const Timeout = union(enum) { none, - duration: ClockAndDuration, - deadline: Timestamp, + duration: Clock.Duration, + deadline: Clock.Timestamp, pub const Error = error{ Timeout, UnsupportedClock }; - pub const ClockAndDuration = struct { - clock: Timestamp.Clock, - duration: Duration, - }; - - pub fn toDeadline(t: Timeout, io: Io) Timestamp.Error!?Timestamp { + pub fn toDeadline(t: Timeout, io: Io) Clock.Error!?Clock.Timestamp { return switch (t) { .none => null, - .duration => |d| try .fromNow(io, d.clock, d.duration), + .duration => |d| try .fromNow(io, d), .deadline => |d| d, }; } - pub fn toDurationFromNow(t: Timeout, io: Io) Timestamp.Error!?ClockAndDuration { + pub fn toDurationFromNow(t: Timeout, io: Io) Clock.Error!?Clock.Duration { return switch (t) { .none => null, .duration => |d| d, - .deadline => |d| .{ .clock = d.clock, .duration = try d.durationFromNow(io) }, + .deadline => |d| try d.durationFromNow(io), }; } diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 3c9772076b..8ee450bf34 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -85,7 +85,7 @@ pub fn updateFile( }; if (src_stat.size == dest_stat.size and - src_stat.mtime == dest_stat.mtime and + src_stat.mtime.nanoseconds == dest_stat.mtime.nanoseconds and actual_mode == dest_stat.mode) { return .fresh; diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 018825164e..c4e4e5c9c4 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -45,16 +45,12 @@ pub const Stat = struct { /// This is available on POSIX systems and is always 0 otherwise. mode: Mode, kind: Kind, - /// Last access time in nanoseconds, relative to UTC 1970-01-01. - /// TODO change this to Io.Timestamp except don't waste storage on clock - atime: i128, + atime: Io.Timestamp, /// Last modification time in nanoseconds, relative to UTC 1970-01-01. - /// TODO change this to Io.Timestamp except don't waste storage on clock - mtime: i128, + mtime: Io.Timestamp, /// Last status/metadata change time in nanoseconds, relative to UTC 1970-01-01. - /// TODO change this to Io.Timestamp except don't waste storage on clock - ctime: i128, + ctime: Io.Timestamp, }; pub fn stdout() File { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 16e134251c..c5f1634f97 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1147,26 +1147,26 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posi }; } -fn nowPosix(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { +fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; const clock_id: posix.clockid_t = clockToPosix(clock); var tp: posix.timespec = undefined; switch (posix.errno(posix.system.clock_gettime(clock_id, &tp))) { - .SUCCESS => return @intCast(@as(i128, tp.sec) * std.time.ns_per_s + tp.nsec), + .SUCCESS => return timestampFromPosix(&tp), .INVAL => return error.UnsupportedClock, else => |err| return posix.unexpectedErrno(err), } } -fn nowWindows(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { +fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; switch (clock) { .realtime => { // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds // and uses the NTFS/Windows epoch, which is 1601-01-01. - return @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100; + return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 }; }, .monotonic, .uptime => { // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. @@ -1178,7 +1178,7 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Err } } -fn nowWasi(userdata: ?*anyopaque, clock: Io.Timestamp.Clock) Io.Timestamp.Error!i96 { +fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; var ns: std.os.wasi.timestamp_t = undefined; @@ -1196,13 +1196,10 @@ fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { }); const deadline_nanoseconds: i96 = switch (timeout) { .none => std.math.maxInt(i96), - .duration => |d| d.duration.nanoseconds, - .deadline => |deadline| deadline.nanoseconds, - }; - var timespec: posix.timespec = .{ - .sec = @intCast(@divFloor(deadline_nanoseconds, std.time.ns_per_s)), - .nsec = @intCast(@mod(deadline_nanoseconds, std.time.ns_per_s)), + .duration => |duration| duration.raw.nanoseconds, + .deadline => |deadline| deadline.raw.nanoseconds, }; + var timespec: posix.timespec = timestampToPosix(deadline_nanoseconds); while (true) { try pool.checkCancel(); switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clock_id, .{ .ABSTIME = switch (timeout) { @@ -1267,11 +1264,7 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { .sec = std.math.maxInt(sec_type), .nsec = std.math.maxInt(nsec_type), }; - const ns = d.duration.nanoseconds; - break :t .{ - .sec = @intCast(@divFloor(ns, std.time.ns_per_s)), - .nsec = @intCast(@mod(ns, std.time.ns_per_s)), - }; + break :t timestampToPosix(d.duration.nanoseconds); }; while (true) { try pool.checkCancel(); @@ -1879,8 +1872,8 @@ fn netReceive( const max_poll_ms = std.math.maxInt(u31); const timeout_ms: u31 = if (deadline) |d| t: { const duration = d.durationFromNow(pool.io()) catch |err| return .{ err, message_i }; - if (duration.nanoseconds <= 0) return .{ error.Timeout, message_i }; - break :t @intCast(@min(max_poll_ms, duration.toMilliseconds())); + if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i }; + break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds())); } else max_poll_ms; const poll_rc = posix.system.poll(&poll_fds, poll_fds.len, timeout_ms); @@ -2160,7 +2153,7 @@ fn recoverableOsBugDetected() void { if (builtin.mode == .Debug) unreachable; } -fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t { +fn clockToPosix(clock: Io.Clock) posix.clockid_t { return switch (clock) { .real => posix.CLOCK.REALTIME, .awake => switch (builtin.os.tag) { @@ -2176,7 +2169,7 @@ fn clockToPosix(clock: Io.Timestamp.Clock) posix.clockid_t { }; } -fn clockToWasi(clock: Io.Timestamp.Clock) std.os.wasi.clockid_t { +fn clockToWasi(clock: Io.Clock) std.os.wasi.clockid_t { return switch (clock) { .realtime => .REALTIME, .awake => .MONOTONIC, @@ -2204,9 +2197,9 @@ fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat { std.os.linux.S.IFSOCK => .unix_domain_socket, else => .unknown, }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, + .atime = .{ .nanoseconds = @intCast(@as(i128, atime.sec) * std.time.ns_per_s + atime.nsec) }, + .mtime = .{ .nanoseconds = @intCast(@as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec) }, + .ctime = .{ .nanoseconds = @intCast(@as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec) }, }; } @@ -2238,9 +2231,9 @@ fn statFromPosix(st: *const std.posix.Stat) Io.File.Stat { break :k .unknown; }, - .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, - .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, - .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, + .atime = timestampFromPosix(&atime), + .mtime = timestampFromPosix(&mtime), + .ctime = timestampFromPosix(&ctime), }; } @@ -2263,3 +2256,14 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat { .ctime = st.ctim, }; } + +fn timestampFromPosix(timespec: *const std.posix.timespec) Io.Timestamp { + return .{ .nanoseconds = @intCast(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec) }; +} + +fn timestampToPosix(nanoseconds: i96) std.posix.timespec { + return .{ + .sec = @intCast(@divFloor(nanoseconds, std.time.ns_per_s)), + .nsec = @intCast(@mod(nanoseconds, std.time.ns_per_s)), + }; +} diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 72b5d39038..94c87ab0a4 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -79,7 +79,7 @@ pub const LookupError = error{ NameServerFailure, /// Failed to open or read "/etc/hosts" or "/etc/resolv.conf". DetectingNetworkConfigurationFailed, -} || Io.Timestamp.Error || IpAddress.BindError || Io.Cancelable; +} || Io.Clock.Error || IpAddress.BindError || Io.Cancelable; pub const LookupResult = struct { /// How many `LookupOptions.addresses_buffer` elements are populated. @@ -294,13 +294,14 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio // boot clock is chosen because time the computer is suspended should count // against time spent waiting for external messages to arrive. - var now_ts = try Io.Timestamp.now(io, .boot); + const clock: Io.Clock = .boot; + var now_ts = try clock.now(io); const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); const attempt_duration: Io.Duration = .{ .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, }; - send: while (now_ts.compare(.lt, final_ts)) : (now_ts = try Io.Timestamp.now(io, .boot)) { + send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(io)) { const max_messages = queries_buffer.len * ResolvConf.max_nameservers; { var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; @@ -319,7 +320,10 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio _ = io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{}); } - const timeout: Io.Timeout = .{ .deadline = now_ts.addDuration(attempt_duration) }; + const timeout: Io.Timeout = .{ .deadline = .{ + .raw = now_ts.addDuration(attempt_duration), + .clock = clock, + } }; while (true) { var message_buffer: [max_messages]Io.net.IncomingMessage = undefined; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 191920dc83..b2130bc5da 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -637,23 +637,23 @@ pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError; pub fn updateTimes( self: File, /// access timestamp in nanoseconds - atime: i128, + atime: Io.Timestamp, /// last modification timestamp in nanoseconds - mtime: i128, + mtime: Io.Timestamp, ) UpdateTimesError!void { if (builtin.os.tag == .windows) { - const atime_ft = windows.nanoSecondsToFileTime(atime); - const mtime_ft = windows.nanoSecondsToFileTime(mtime); + const atime_ft = windows.nanoSecondsToFileTime(atime.nanoseconds); + const mtime_ft = windows.nanoSecondsToFileTime(mtime.nanoseconds); return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); } const times = [2]posix.timespec{ posix.timespec{ - .sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize), - .nsec = math.cast(isize, @mod(atime, std.time.ns_per_s)) orelse maxInt(isize), + .sec = math.cast(isize, @divFloor(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), + .nsec = math.cast(isize, @mod(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), }, posix.timespec{ - .sec = math.cast(isize, @divFloor(mtime, std.time.ns_per_s)) orelse maxInt(isize), - .nsec = math.cast(isize, @mod(mtime, std.time.ns_per_s)) orelse maxInt(isize), + .sec = math.cast(isize, @divFloor(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), + .nsec = math.cast(isize, @mod(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize), }, }; try posix.futimens(self.handle, ×); diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index dbd547611f..91167a4b0a 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -320,7 +320,7 @@ pub const Connection = struct { const tls: *Tls = @ptrCast(base); var random_buffer: [176]u8 = undefined; std.crypto.random.bytes(&random_buffer); - const now_ts = if (Io.Timestamp.now(io, .real)) |ts| ts.toSeconds() else |_| return error.TlsInitializationFailed; + const now_ts = if (Io.Clock.real.now(io)) |ts| ts.toSeconds() else |_| return error.TlsInitializationFailed; tls.* = .{ .connection = .{ .client = client, diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig index a48e8cc407..129f1e7ede 100644 --- a/lib/std/tar/Writer.zig +++ b/lib/std/tar/Writer.zig @@ -18,7 +18,6 @@ pub const Options = struct { underlying_writer: *Io.Writer, prefix: []const u8 = "", -mtime_now: u64 = 0, const Error = error{ WriteFailed, @@ -44,10 +43,12 @@ pub fn writeFile( w: *Writer, sub_path: []const u8, file_reader: *Io.File.Reader, - stat_mtime: i128, + /// If you want to match the file format's expectations, it wants number of + /// seconds since POSIX epoch. Zero is also a great option here to make + /// generated tarballs more reproducible. + mtime: u64, ) WriteFileError!void { const size = try file_reader.getSize(); - const mtime: u64 = @intCast(@divFloor(stat_mtime, std.time.ns_per_s)); var header: Header = .{}; try w.setPath(&header, sub_path); @@ -238,7 +239,6 @@ pub const Header = extern struct { } // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time. - // mtime == 0 will use current time pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void { try octal(&w.mtime, mtime); } From e85df854aa86c4f41c3a64d2af1858aed3482fbc Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 8 Oct 2025 22:25:49 -0700 Subject: [PATCH 082/244] std.mem: improve containsAtLeastScalar implementation and rename --- lib/std/mem.zig | 53 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/std/mem.zig b/lib/std/mem.zig index 5042356db9..ade7d1e777 100644 --- a/lib/std/mem.zig +++ b/lib/std/mem.zig @@ -1746,6 +1746,7 @@ test countScalar { // /// See also: `containsAtLeastScalar` pub fn containsAtLeast(comptime T: type, haystack: []const T, expected_count: usize, needle: []const T) bool { + if (needle.len == 1) return containsAtLeastScalar(T, haystack, expected_count, needle[0]); assert(needle.len > 0); if (expected_count == 0) return true; @@ -1776,32 +1777,52 @@ test containsAtLeast { try testing.expect(!containsAtLeast(u8, " radar radar ", 3, "radar")); } -/// Returns true if the haystack contains expected_count or more needles -// -/// See also: `containsAtLeast` -pub fn containsAtLeastScalar(comptime T: type, haystack: []const T, expected_count: usize, needle: T) bool { - if (expected_count == 0) return true; +/// Deprecated in favor of `containsAtLeastScalar2`. +pub fn containsAtLeastScalar(comptime T: type, list: []const T, minimum: usize, element: T) bool { + return containsAtLeastScalar2(T, list, element, minimum); +} +/// Returns true if `element` appears at least `minimum` number of times in `list`. +// +/// Related: +/// * `containsAtLeast` +/// * `countScalar` +pub fn containsAtLeastScalar2(comptime T: type, list: []const T, element: T, minimum: usize) bool { + const n = list.len; + var i: usize = 0; var found: usize = 0; - for (haystack) |item| { - if (item == needle) { - found += 1; - if (found == expected_count) return true; + if (use_vectors_for_comparison and + (@typeInfo(T) == .int or @typeInfo(T) == .float) and std.math.isPowerOfTwo(@bitSizeOf(T))) + { + if (std.simd.suggestVectorLength(T)) |block_size| { + const Block = @Vector(block_size, T); + + const letter_mask: Block = @splat(element); + while (n - i >= block_size) : (i += block_size) { + const haystack_block: Block = list[i..][0..block_size].*; + found += std.simd.countTrues(letter_mask == haystack_block); + if (found >= minimum) return true; + } } } + for (list[i..n]) |item| { + found += @intFromBool(item == element); + if (found >= minimum) return true; + } + return false; } -test containsAtLeastScalar { - try testing.expect(containsAtLeastScalar(u8, "aa", 0, 'a')); - try testing.expect(containsAtLeastScalar(u8, "aa", 1, 'a')); - try testing.expect(containsAtLeastScalar(u8, "aa", 2, 'a')); - try testing.expect(!containsAtLeastScalar(u8, "aa", 3, 'a')); +test containsAtLeastScalar2 { + try testing.expect(containsAtLeastScalar2(u8, "aa", 'a', 0)); + try testing.expect(containsAtLeastScalar2(u8, "aa", 'a', 1)); + try testing.expect(containsAtLeastScalar2(u8, "aa", 'a', 2)); + try testing.expect(!containsAtLeastScalar2(u8, "aa", 'a', 3)); - try testing.expect(containsAtLeastScalar(u8, "adadda", 3, 'd')); - try testing.expect(!containsAtLeastScalar(u8, "adadda", 4, 'd')); + try testing.expect(containsAtLeastScalar2(u8, "adadda", 'd', 3)); + try testing.expect(!containsAtLeastScalar2(u8, "adadda", 'd', 4)); } /// Reads an integer from memory with size equal to bytes.len. From eadfefa002b642d1b9173fa7d9a281a70a61e233 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 8 Oct 2025 22:26:18 -0700 Subject: [PATCH 083/244] std.Io: implement dirMake In the future, it might be nice to introduce a type for file system path names. This would be a way to avoid having InvalidFileName in the error set, since construction of such type could validate it above the interface. --- lib/std/Io/Dir.zig | 4 ++-- lib/std/Io/Threaded.zig | 50 ++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 8ee450bf34..79824c56a6 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -155,8 +155,6 @@ pub const MakeError = error{ NoSpaceLeft, NotDir, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, /// Windows-only; file paths provided by the user must be valid WTF-8. /// https://simonsapin.github.io/wtf-8/ InvalidWtf8, @@ -164,6 +162,8 @@ pub const MakeError = error{ NoDevice, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, + /// File system cannot encode the requested file name bytes. + InvalidFileName, } || Io.Cancelable || Io.UnexpectedError; /// Creates a single directory with a relative or absolute path. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c5f1634f97..cab383b7dd 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -160,7 +160,11 @@ pub fn io(pool: *Pool) Io { .conditionWait = conditionWait, .conditionWake = conditionWake, - .dirMake = dirMake, + .dirMake = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => dirMakePosix, + }, .dirStat = dirStat, .dirStatPath = dirStatPath, .fileStat = switch (builtin.os.tag) { @@ -759,14 +763,35 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. } } -fn dirMake(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { +fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - - _ = dir; - _ = sub_path; - _ = mode; - @panic("TODO"); + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try toPosixPath(sub_path, &path_buffer); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) { + .SUCCESS => return, + .INTR => continue, + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), + .PERM => return error.PermissionDenied, + .DQUOT => return error.DiskQuota, + .EXIST => return error.PathAlreadyExists, + .FAULT => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .MLINK => return error.LinkQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + // dragonfly: when dir_fd is unlinked from filesystem + .NOTCONN => return error.FileNotFound, + .ILSEQ => return error.InvalidFileName, + else => |err| return posix.unexpectedErrno(err), + } + } } fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { @@ -2267,3 +2292,12 @@ fn timestampToPosix(nanoseconds: i96) std.posix.timespec { .nsec = @intCast(@mod(nanoseconds, std.time.ns_per_s)), }; } + +fn toPosixPath(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) error{ NameTooLong, InvalidFileName }![:0]u8 { + if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.InvalidFileName; + // >= rather than > to make room for the null byte + if (file_path.len >= buffer.len) return error.NameTooLong; + @memcpy(buffer[0..file_path.len], file_path); + buffer[file_path.len] = 0; + return buffer[0..file_path.len :0]; +} From 750b1431bf34b238bd991b2ee3b50e81ff20b667 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 8 Oct 2025 22:37:39 -0700 Subject: [PATCH 084/244] std: fix some Io compilation errors --- lib/std/Io/Dir.zig | 2 +- lib/std/fs/test.zig | 2 +- lib/std/http/Client.zig | 13 +++++++++---- lib/std/os/linux/IoUring.zig | 4 ++-- lib/std/posix.zig | 2 ++ lib/std/time.zig | 2 +- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 79824c56a6..2647efd2c3 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -263,6 +263,6 @@ pub const StatPathError = File.OpenError || File.StatError; /// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). /// * On WASI, `sub_path` should be encoded as valid UTF-8. /// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn statPath(dir: Dir, io: Io, sub_path: []const u8) StatPathError!File.Stat { +pub fn statPath(dir: Dir, io: Io, sub_path: []const u8) StatPathError!Stat { return io.vtable.dirStatPath(io.userdata, dir, sub_path); } diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index e60e3c7911..015d595a3a 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1928,7 +1928,7 @@ test "'.' and '..' in fs.Dir functions" { try ctx.dir.writeFile(.{ .sub_path = update_path, .data = "something" }); var dir = ctx.dir.adaptToNewApi(); const prev_status = try dir.updateFile(io, file_path, dir, update_path, .{}); - try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); + try testing.expectEqual(Io.Dir.PrevStatus.stale, prev_status); try ctx.dir.deleteDir(subdir_path); } diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 91167a4b0a..b05a0a317e 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -300,7 +300,7 @@ pub const Connection = struct { remote_host: HostName, port: u16, stream: Io.net.Stream, - ) error{ OutOfMemory, TlsInitializationFailed }!*Tls { + ) !*Tls { const io = client.io; const gpa = client.allocator; const alloc_len = allocLen(client, remote_host.bytes.len); @@ -320,7 +320,7 @@ pub const Connection = struct { const tls: *Tls = @ptrCast(base); var random_buffer: [176]u8 = undefined; std.crypto.random.bytes(&random_buffer); - const now_ts = if (Io.Clock.real.now(io)) |ts| ts.toSeconds() else |_| return error.TlsInitializationFailed; + const now_ts = if (Io.Clock.real.now(io)) |ts| ts.toSeconds() else |err| return err; tls.* = .{ .connection = .{ .client = client, @@ -349,7 +349,11 @@ pub const Connection = struct { // the content length which is used to detect truncation attacks. .allow_truncation_attacks = true, }, - ) catch return error.TlsInitializationFailed, + ) catch |err| switch (err) { + error.WriteFailed => return tls.connection.stream_writer.err.?, + error.ReadFailed => return tls.connection.stream_reader.err.?, + else => |e| return e, + }, }; return tls; } @@ -1446,7 +1450,8 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp const tc = Connection.Tls.create(client, proxied_host, proxied_port, stream) catch |err| switch (err) { error.OutOfMemory => |e| return e, error.Unexpected => |e| return e, - error.UnsupportedClock => return error.TlsInitializationFailed, + error.Canceled => |e| return e, + else => return error.TlsInitializationFailed, }; client.connection_pool.addUsed(&tc.connection); return &tc.connection; diff --git a/lib/std/os/linux/IoUring.zig b/lib/std/os/linux/IoUring.zig index eaaa7643a9..5bcb4ec1a9 100644 --- a/lib/std/os/linux/IoUring.zig +++ b/lib/std/os/linux/IoUring.zig @@ -2459,12 +2459,12 @@ test "timeout (after a relative time)" { const margin = 5; const ts: linux.kernel_timespec = .{ .sec = 0, .nsec = ms * 1000000 }; - const started = try std.Io.Timestamp.now(io, .awake); + const started = try std.Io.Clock.awake.now(io); const sqe = try ring.timeout(0x55555555, &ts, 0, 0); try testing.expectEqual(linux.IORING_OP.TIMEOUT, sqe.opcode); try testing.expectEqual(@as(u32, 1), try ring.submit()); const cqe = try ring.copy_cqe(); - const stopped = try std.Io.Timestamp.now(io, .awake); + const stopped = try std.Io.Clock.awake.now(io); try testing.expectEqual(linux.io_uring_cqe{ .user_data = 0x55555555, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index e4cd040703..245e4bac9a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -357,6 +357,7 @@ pub const FChmodAtError = FChmodError || error{ ProcessFdQuotaExceeded, /// The procfs fallback was used but the system exceeded it open file limit. SystemFdQuotaExceeded, + Canceled, }; /// Changes the `mode` of `path` relative to the directory referred to by @@ -487,6 +488,7 @@ fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtEr error.NameTooLong => unreachable, error.FileNotFound => unreachable, error.InvalidUtf8 => unreachable, + error.Canceled => return error.Canceled, else => |e| return e, }; if ((stat.mode & S.IFMT) == S.IFLNK) diff --git a/lib/std/time.zig b/lib/std/time.zig index b821775f4e..66a38051ba 100644 --- a/lib/std/time.zig +++ b/lib/std/time.zig @@ -204,7 +204,7 @@ test Timer { var timer = try Timer.start(); - try std.Io.Duration.sleep(.fromMilliseconds(10), io); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(10) }, io); const time_0 = timer.read(); try testing.expect(time_0 > 0); From 8a1e6c8c394058a25cb21f60ad414f28cd37db22 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 9 Oct 2025 00:19:44 -0700 Subject: [PATCH 085/244] std.Io: implement dirStatPath --- lib/std/Build/Cache.zig | 18 +-- lib/std/Io.zig | 34 ++++-- lib/std/Io/Dir.zig | 42 +++++-- lib/std/Io/File.zig | 58 ++++++++- lib/std/Io/Threaded.zig | 222 ++++++++++++++++++++++++++++++++--- lib/std/Io/test.zig | 12 +- lib/std/Thread.zig | 5 +- lib/std/Thread/Condition.zig | 15 ++- lib/std/Uri.zig | 2 +- lib/std/dynamic_library.zig | 1 + lib/std/fs/Dir.zig | 90 +++----------- lib/std/fs/File.zig | 29 +---- lib/std/posix.zig | 76 +----------- lib/std/zig/system.zig | 93 +++++++-------- 14 files changed, 406 insertions(+), 291 deletions(-) diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 8f88d840b8..b966c25efc 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -1305,7 +1305,7 @@ fn hashFile(file: fs.File, bin_digest: *[Hasher.mac_length]u8) fs.File.PReadErro } // Create/Write a file, close it, then grab its stat.mtime timestamp. -fn testGetCurrentFileTimestamp(dir: fs.Dir) !i128 { +fn testGetCurrentFileTimestamp(dir: fs.Dir) !Io.Timestamp { const test_out_file = "test-filetimestamp.tmp"; var file = try dir.createFile(test_out_file, .{ @@ -1333,8 +1333,8 @@ test "cache file and then recall it" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1399,8 +1399,8 @@ test "check that changing a file makes cache fail" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1517,8 +1517,8 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1568,8 +1568,8 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time2 = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time2) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time2.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } { diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 8d061ad022..320ff3ba2d 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -654,20 +654,20 @@ pub const VTable = struct { conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, - dirMake: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, - dirStat: *const fn (?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat, - dirStatPath: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8) Dir.StatError!File.Stat, - fileStat: *const fn (?*anyopaque, file: File) File.StatError!File.Stat, - createFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File, - fileOpen: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File, + dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, + dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, + dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, + fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, + createFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, + fileOpen: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileClose: *const fn (?*anyopaque, File) void, - pwrite: *const fn (?*anyopaque, file: File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, + pwrite: *const fn (?*anyopaque, File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, /// Returns 0 on end of stream. - fileReadStreaming: *const fn (?*anyopaque, file: File, data: [][]u8) File.ReadStreamingError!usize, + fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.ReadStreamingError!usize, /// Returns 0 on end of stream. - fileReadPositional: *const fn (?*anyopaque, file: File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, - fileSeekBy: *const fn (?*anyopaque, file: File, offset: i64) File.SeekError!void, - fileSeekTo: *const fn (?*anyopaque, file: File, offset: u64) File.SeekError!void, + fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, + fileSeekBy: *const fn (?*anyopaque, File, offset: i64) File.SeekError!void, + fileSeekTo: *const fn (?*anyopaque, File, offset: u64) File.SeekError!void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, @@ -795,6 +795,14 @@ pub const Clock = enum { }; } + pub fn subDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.subDuration(duration.raw), + .clock = from.clock, + }; + } + pub fn fromNow(io: Io, duration: Clock.Duration) Error!Clock.Timestamp { return .{ .clock = duration.clock, @@ -855,6 +863,10 @@ pub const Timestamp = struct { return .{ .nanoseconds = from.nanoseconds + duration.nanoseconds }; } + pub fn subDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds - duration.nanoseconds }; + } + pub fn withClock(t: Timestamp, clock: Clock) Clock.Timestamp { return .{ .nanoseconds = t.nanoseconds, .clock = clock }; } diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 2647efd2c3..5460f6bd60 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -15,6 +15,29 @@ pub fn cwd() Dir { pub const Handle = std.posix.fd_t; +pub const PathNameError = error{ + NameTooLong, + /// File system cannot encode the requested file name bytes. + /// Could be due to invalid WTF-8 on Windows, invalid UTF-8 on WASI, + /// invalid characters on Windows, etc. Filesystem and operating specific. + BadPathName, +}; + +pub const OpenError = error{ + FileNotFound, + NotDir, + AccessDenied, + PermissionDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + DeviceBusy, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); } @@ -149,22 +172,15 @@ pub const MakeError = error{ PathAlreadyExists, SymLinkLoop, LinkQuotaExceeded, - NameTooLong, FileNotFound, SystemResources, NoSpaceLeft, NotDir, ReadOnlyFileSystem, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://simonsapin.github.io/wtf-8/ - InvalidWtf8, - BadPathName, NoDevice, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, - /// File system cannot encode the requested file name bytes. - InvalidFileName, -} || Io.Cancelable || Io.UnexpectedError; +} || PathNameError || Io.Cancelable || Io.UnexpectedError; /// Creates a single directory with a relative or absolute path. /// @@ -225,7 +241,7 @@ pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!Make // could cause an infinite loop check_dir: { // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = statPath(dir, io, component.path) catch |stat_err| switch (stat_err) { + const fstat = statPath(dir, io, component.path, .{}) catch |stat_err| switch (stat_err) { error.IsDir => break :check_dir, else => |e| return e, }; @@ -251,6 +267,10 @@ pub fn stat(dir: Dir, io: Io) StatError!Stat { pub const StatPathError = File.OpenError || File.StatError; +pub const StatPathOptions = struct { + follow_symlinks: bool = true, +}; + /// Returns metadata for a file inside the directory. /// /// On Windows, this requires three syscalls. On other operating systems, it @@ -263,6 +283,6 @@ pub const StatPathError = File.OpenError || File.StatError; /// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). /// * On WASI, `sub_path` should be encoded as valid UTF-8. /// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn statPath(dir: Dir, io: Io, sub_path: []const u8) StatPathError!Stat { - return io.vtable.dirStatPath(io.userdata, dir, sub_path); +pub fn statPath(dir: Dir, io: Io, sub_path: []const u8, options: StatPathOptions) StatPathError!Stat { + return io.vtable.dirStatPath(io.userdata, dir, sub_path, options); } diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index c4e4e5c9c4..d5d08587c7 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -81,7 +81,63 @@ pub fn stat(file: File, io: Io) StatError!Stat { pub const OpenFlags = std.fs.File.OpenFlags; pub const CreateFlags = std.fs.File.CreateFlags; -pub const OpenError = std.fs.File.OpenError || Io.Cancelable; +pub const OpenError = error{ + SharingViolation, + PipeBusy, + NoDevice, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + ProcessNotFound, + /// On Windows, antivirus software is enabled by default. It can be + /// disabled, but Windows Update sometimes ignores the user's preference + /// and re-enables it. When enabled, antivirus software on Windows + /// intercepts file system operations and makes them significantly slower + /// in addition to possibly failing with this error code. + AntivirusInterference, + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to open a new resource relative to it. + AccessDenied, + PermissionDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + /// Either: + /// * One of the path components does not exist. + /// * Cwd was used, but cwd has been deleted. + /// * The path associated with the open directory handle has been deleted. + /// * On macOS, multiple processes or threads raced to create the same file + /// with `O.EXCL` set to `false`. + FileNotFound, + /// The path exceeded `max_path_bytes` bytes. + /// Insufficient kernel memory was available, or + /// the named file is a FIFO and per-user hard limit on + /// memory allocation for pipes has been reached. + SystemResources, + /// The file is too large to be opened. This error is unreachable + /// for 64-bit targets, as well as when opening directories. + FileTooBig, + /// The path refers to directory but the `DIRECTORY` flag was not provided. + IsDir, + /// A new path cannot be created because the device has no room for the new file. + /// This error is only reachable when the `CREAT` flag is provided. + NoSpaceLeft, + /// A component used as a directory in the path was not, in fact, a directory, or + /// `DIRECTORY` was specified and the path was not a directory. + NotDir, + /// The path already exists and the `CREAT` and `EXCL` flags were provided. + PathAlreadyExists, + DeviceBusy, + FileLocksNotSupported, + /// One of these three things: + /// * pathname refers to an executable image which is currently being + /// executed and write access was requested. + /// * pathname refers to a file that is currently in use as a swap + /// file, and the O_TRUNC flag was specified. + /// * pathname refers to a file that is currently being read by the + /// kernel (e.g., for module/firmware loading), and write access was + /// requested. + FileBusy, +} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub fn close(file: File, io: Io) void { return io.vtable.fileClose(io.userdata, file); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index cab383b7dd..02e68f85bb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -166,14 +166,23 @@ pub fn io(pool: *Pool) Io { else => dirMakePosix, }, .dirStat = dirStat, - .dirStatPath = dirStatPath, + .dirStatPath = switch (builtin.os.tag) { + .linux => dirStatPathLinux, + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => dirStatPathPosix, + }, .fileStat = switch (builtin.os.tag) { .linux => fileStatLinux, .windows => fileStatWindows, .wasi => fileStatWasi, else => fileStatPosix, }, - .createFile = createFile, + .createFile = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => createFilePosix, + }, .fileOpen = fileOpen, .fileClose = fileClose, .pwrite = pwrite, @@ -765,8 +774,10 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); + var path_buffer: [posix.PATH_MAX]u8 = undefined; - const sub_path_posix = try toPosixPath(sub_path, &path_buffer); + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + while (true) { try pool.checkCancel(); switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) { @@ -788,7 +799,7 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: .ROFS => return error.ReadOnlyFileSystem, // dragonfly: when dir_fd is unlinked from filesystem .NOTCONN => return error.FileNotFound, - .ILSEQ => return error.InvalidFileName, + .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), } } @@ -802,13 +813,82 @@ fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { @panic("TODO"); } -fn dirStatPath(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.StatError!Io.File.Stat { +fn dirStatPathLinux( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const linux = std.os.linux; - _ = dir; - _ = sub_path; - @panic("TODO"); + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = linux.AT.NO_AUTOMOUNT | + @as(u32, if (!options.follow_symlinks) linux.AT.SYMLINK_NOFOLLOW else 0); + + while (true) { + try pool.checkCancel(); + var statx = std.mem.zeroes(linux.Statx); + const rc = linux.statx( + dir.handle, + sub_path_posix, + flags, + linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, + &statx, + ); + switch (linux.E.init(rc)) { + .SUCCESS => return statFromLinux(&statx), + .INTR => continue, + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .NAMETOOLONG => |err| return errnoBug(err), // Handled by pathToPosix() above. + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirStatPathPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; + const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; + + while (true) { + try pool.checkCancel(); + var stat = std.mem.zeroes(posix.Stat); + switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) { + .SUCCESS => return statFromPosix(stat), + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } } fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { @@ -885,17 +965,127 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. } } -fn createFile( +const have_flock = @TypeOf(posix.system.flock) != void; + +fn createFilePosix( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, flags: Io.File.CreateFlags, ) Io.File.OpenError!Io.File { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; - const fs_file = try fs_dir.createFile(sub_path, flags); - return .{ .handle = fs_file.handle }; + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + 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 openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; + + const fd: posix.fd_t = while (true) { + try pool.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), + .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, + .SRCH => return error.ProcessNotFound, + .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, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(fd); + + if (have_flock and !has_flock_open_flags and flags.lock != .none) { + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }; + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.flock(fd, lock_flags))) { + .SUCCESS => break, + .INTR => continue, + + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOLCK => return error.SystemResources, + //.AGAIN => return error.WouldBlock, + .OPNOTSUPP => return error.FileLocksNotSupported, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + var fl_flags: usize = while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + return .{ .handle = fd }; } fn fileOpen( @@ -2293,8 +2483,8 @@ fn timestampToPosix(nanoseconds: i96) std.posix.timespec { }; } -fn toPosixPath(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) error{ NameTooLong, InvalidFileName }![:0]u8 { - if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.InvalidFileName; +fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNameError![:0]u8 { + if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.BadPathName; // >= rather than > to make room for the null byte if (file_path.len >= buffer.len) return error.NameTooLong; @memcpy(buffer[0..file_path.len], file_path); diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 22dc163dc6..22f211d6e4 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -11,6 +11,8 @@ const native_endian = @import("builtin").target.cpu.arch.endian(); const tmpDir = std.testing.tmpDir; test "write a file, read it, then delete it" { + const io = std.testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -45,7 +47,7 @@ test "write a file, read it, then delete it" { try expectEqual(expected_file_size, file_size); var file_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); const contents = try file_reader.interface.allocRemaining(std.testing.allocator, .limited(2 * 1024)); defer std.testing.allocator.free(contents); @@ -114,10 +116,10 @@ test "updateTimes" { const stat_old = try file.stat(); // Set atime and mtime to 5s before try file.updateTimes( - stat_old.atime - 5 * std.time.ns_per_s, - stat_old.mtime - 5 * std.time.ns_per_s, + stat_old.atime.subDuration(.fromSeconds(5)), + stat_old.mtime.subDuration(.fromSeconds(5)), ); const stat_new = try file.stat(); - try expect(stat_new.atime < stat_old.atime); - try expect(stat_new.mtime < stat_old.mtime); + try expect(stat_new.atime.nanoseconds < stat_old.atime.nanoseconds); + try expect(stat_new.mtime.nanoseconds < stat_old.mtime.nanoseconds); } diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 618f1209ef..9a602fbd6b 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -318,10 +318,13 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co var buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.io(); + const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); - var file_reader = file.readerStreaming(&.{}); + var file_reader = file.readerStreaming(io, &.{}); const data_len = file_reader.interface.readSliceShort(buffer_ptr[0 .. max_name_len + 1]) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, }; diff --git a/lib/std/Thread/Condition.zig b/lib/std/Thread/Condition.zig index 91974a44b4..b298e598df 100644 --- a/lib/std/Thread/Condition.zig +++ b/lib/std/Thread/Condition.zig @@ -123,14 +123,9 @@ const SingleThreadedImpl = struct { fn wait(self: *Impl, mutex: *Mutex, timeout: ?u64) error{Timeout}!void { _ = self; _ = mutex; - // There are no other threads to wake us up. // So if we wait without a timeout we would never wake up. - const timeout_ns = timeout orelse { - unreachable; // deadlock detected - }; - - std.Thread.sleep(timeout_ns); + assert(timeout != null); // Deadlock detected. return error.Timeout; } @@ -323,6 +318,8 @@ test "wait and signal" { return error.SkipZigTest; } + const io = testing.io; + const num_threads = 4; const MultiWait = struct { @@ -348,7 +345,7 @@ test "wait and signal" { } while (true) { - std.Thread.sleep(100 * std.time.ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(100) }, io); multi_wait.mutex.lock(); defer multi_wait.mutex.unlock(); @@ -368,6 +365,8 @@ test signal { return error.SkipZigTest; } + const io = testing.io; + const num_threads = 4; const SignalTest = struct { @@ -405,7 +404,7 @@ test signal { } while (true) { - std.Thread.sleep(10 * std.time.ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(10) }, io); signal_test.mutex.lock(); defer signal_test.mutex.unlock(); diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 46903ecf33..a27f208e65 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -441,7 +441,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE }; } -fn validateHost(bytes: []const u8) []const u8 { +fn validateHost(bytes: []const u8) HostName.ValidateError![]const u8 { try HostName.validate(bytes); return bytes; } diff --git a/lib/std/dynamic_library.zig b/lib/std/dynamic_library.zig index 92aea780bf..a7326cfd66 100644 --- a/lib/std/dynamic_library.zig +++ b/lib/std/dynamic_library.zig @@ -137,6 +137,7 @@ const ElfDynLibError = error{ ElfStringSectionNotFound, ElfSymSectionNotFound, ElfHashTableNotFound, + Canceled, } || posix.OpenError || posix.MMapError; pub const ElfDynLib = struct { diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index c2ddf98627..f6455215ac 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -36,10 +36,6 @@ const IteratorError = error{ AccessDenied, PermissionDenied, SystemResources, - /// WASI-only. The path of an entry could not be encoded as valid UTF-8. - /// WASI is unable to handle paths that cannot be encoded as well-formed UTF-8. - /// https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353 - InvalidUtf8, } || posix.UnexpectedError; pub const Iterator = switch (native_os) { @@ -553,7 +549,6 @@ pub const Iterator = switch (native_os) { .INVAL => unreachable, .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, // An entry's name cannot be encoded as UTF-8. else => |err| return posix.unexpectedErrno(err), } if (bufused == 0) return null; @@ -844,28 +839,7 @@ pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker { }; } -pub const OpenError = error{ - FileNotFound, - NotDir, - AccessDenied, - PermissionDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - NameTooLong, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, - DeviceBusy, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, -} || posix.UnexpectedError; +pub const OpenError = Io.Dir.OpenError; pub fn close(self: *Dir) void { posix.close(self.fd); @@ -1311,7 +1285,7 @@ pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenOptio }; } -pub const RealPathError = posix.RealPathError; +pub const RealPathError = posix.RealPathError || error{Canceled}; /// This function returns the canonicalized absolute pathname of /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this @@ -1369,7 +1343,6 @@ pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) RealPathE error.FileLocksNotSupported => return error.Unexpected, error.FileBusy => return error.Unexpected, error.WouldBlock => return error.Unexpected, - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; defer posix.close(fd); @@ -1639,6 +1612,10 @@ fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError error.FileLocksNotSupported => unreachable, // locking folders is not supported error.WouldBlock => unreachable, // can't happen for directories error.FileBusy => unreachable, // can't happen for directories + error.SharingViolation => unreachable, // can't happen for directories + error.PipeBusy => unreachable, // can't happen for directories + error.AntivirusInterference => unreachable, // can't happen for directories + error.ProcessNotFound => unreachable, // can't happen for directories else => |e| return e, }; return Dir{ .fd = fd }; @@ -2095,24 +2072,21 @@ pub const DeleteTreeError = error{ FileBusy, DeviceBusy, ProcessNotFound, - /// One of the path components was not a directory. /// This error is unreachable if `sub_path` does not contain a path separator. NotDir, - /// WASI-only; file paths must be valid UTF-8. InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ InvalidWtf8, - /// On Windows, file paths cannot contain these characters: /// '/', '*', '?', '"', '<', '>', '|' BadPathName, - /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, + + Canceled, } || posix.UnexpectedError; /// Whether `sub_path` describes a symlink, file, or directory, this function @@ -2169,17 +2143,15 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.PermissionDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, - error.ProcessNotFound, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; stack.appendAssumeCapacity(.{ @@ -2266,18 +2238,16 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, error.SymLinkLoop, - error.ProcessNotFound, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; } else { @@ -2374,18 +2344,16 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint error.AccessDenied, error.PermissionDenied, error.SymLinkLoop, - error.ProcessNotFound, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; if (cleanup_dir_parent) |*d| d.close(); @@ -2476,17 +2444,15 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File error.PermissionDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, - error.ProcessNotFound, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.DeviceBusy, error.NetworkNotFound, + error.Canceled, => |e| return e, }; } else { @@ -2589,7 +2555,7 @@ pub const CopyFileOptions = struct { pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || AtomicFile.FinishError || - File.ReadError || File.WriteError; + File.ReadError || File.WriteError || error{InvalidFileName}; /// Atomically creates a new file at `dest_path` within `dest_dir` with the /// same contents as `source_path` within `source_dir`, overwriting any already @@ -2690,33 +2656,9 @@ pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { const st = try std.os.fstatat_wasi(self.fd, sub_path, .{ .SYMLINK_FOLLOW = true }); return Stat.fromWasi(st); } - if (native_os == .linux) { - const sub_path_c = try posix.toPosixPath(sub_path); - var stx = std.mem.zeroes(linux.Statx); - - const rc = linux.statx( - self.fd, - &sub_path_c, - linux.AT.NO_AUTOMOUNT, - linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, - &stx, - ); - - return switch (linux.E.init(rc)) { - .SUCCESS => Stat.fromLinux(stx), - .ACCES => error.AccessDenied, - .BADF => unreachable, - .FAULT => unreachable, - .INVAL => unreachable, - .LOOP => error.SymLinkLoop, - .NAMETOOLONG => unreachable, // Handled by posix.toPosixPath() above. - .NOENT, .NOTDIR => error.FileNotFound, - .NOMEM => error.SystemResources, - else => |err| posix.unexpectedErrno(err), - }; - } - const st = try posix.fstatat(self.fd, sub_path, 0); - return Stat.fromPosix(st); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{}); } pub const ChmodError = File.ChmodError; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index b2130bc5da..8253fe61e7 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -38,33 +38,8 @@ pub const default_mode = switch (builtin.os.tag) { else => 0o666, }; -pub const OpenError = error{ - SharingViolation, - PathAlreadyExists, - FileNotFound, - AccessDenied, - PipeBusy, - NoDevice, - NameTooLong, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - Unexpected, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, - /// On Windows, antivirus software is enabled by default. It can be - /// disabled, but Windows Update sometimes ignores the user's preference - /// and re-enables it. When enabled, antivirus software on Windows - /// intercepts file system operations and makes them significantly slower - /// in addition to possibly failing with this error code. - AntivirusInterference, -} || posix.OpenError || posix.FlockError; +/// Deprecated in favor of `Io.File.OpenError`. +pub const OpenError = Io.File.OpenError || error{WouldBlock}; pub const OpenMode = enum { read_only, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 245e4bac9a..50d63ad63d 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1542,81 +1542,7 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) PWriteError!usiz } } -pub const OpenError = error{ - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to open a new resource relative to it. - AccessDenied, - PermissionDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - NoDevice, - /// Either: - /// * One of the path components does not exist. - /// * Cwd was used, but cwd has been deleted. - /// * The path associated with the open directory handle has been deleted. - /// * On macOS, multiple processes or threads raced to create the same file - /// with `O.EXCL` set to `false`. - FileNotFound, - - /// The path exceeded `max_path_bytes` bytes. - NameTooLong, - - /// Insufficient kernel memory was available, or - /// the named file is a FIFO and per-user hard limit on - /// memory allocation for pipes has been reached. - SystemResources, - - /// The file is too large to be opened. This error is unreachable - /// for 64-bit targets, as well as when opening directories. - FileTooBig, - - /// The path refers to directory but the `DIRECTORY` flag was not provided. - IsDir, - - /// A new path cannot be created because the device has no room for the new file. - /// This error is only reachable when the `CREAT` flag is provided. - NoSpaceLeft, - - /// A component used as a directory in the path was not, in fact, a directory, or - /// `DIRECTORY` was specified and the path was not a directory. - NotDir, - - /// The path already exists and the `CREAT` and `EXCL` flags were provided. - PathAlreadyExists, - DeviceBusy, - - /// The underlying filesystem does not support file locks - FileLocksNotSupported, - - /// Path contains characters that are disallowed by the underlying filesystem. - BadPathName, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - - /// This error occurs in Linux if the process to be open was not found. - ProcessNotFound, - - /// One of these three things: - /// * pathname refers to an executable image which is currently being - /// executed and write access was requested. - /// * pathname refers to a file that is currently in use as a swap - /// file, and the O_TRUNC flag was specified. - /// * pathname refers to a file that is currently being read by the - /// kernel (e.g., for module/firmware loading), and write access was - /// requested. - FileBusy, - - WouldBlock, -} || UnexpectedError; +pub const OpenError = std.Io.File.OpenError || error{WouldBlock}; /// Open and possibly create a file. Keeps trying if it gets interrupted. /// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 7318612151..7bf836b583 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -766,27 +766,23 @@ test glibcVerFromLinkName { fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows-only - error.BadPathName => unreachable, - error.DeviceBusy => unreachable, - error.NetworkNotFound => unreachable, // Windows-only + error.NameTooLong => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.DeviceBusy => return error.Unexpected, + error.NetworkNotFound => return error.Unexpected, // Windows-only - error.FileNotFound, - error.NotDir, - error.AccessDenied, - error.PermissionDenied, - error.NoDevice, - => return error.GLibCNotFound, + error.FileNotFound => return error.GLibCNotFound, + error.NotDir => return error.GLibCNotFound, + error.AccessDenied => return error.GLibCNotFound, + error.PermissionDenied => return error.GLibCNotFound, + error.NoDevice => return error.GLibCNotFound, - error.ProcessNotFound, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.SystemResources => |e| return e, + error.SymLinkLoop => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, }; defer dir.close(); @@ -799,38 +795,34 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { // that start with "GLIBC_2.". const glibc_so_basename = "libc.so.6"; var file = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.PipeBusy => unreachable, // Windows-only - error.SharingViolation => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - error.AntivirusInterference => unreachable, // Windows-only - error.FileLocksNotSupported => unreachable, // No lock requested. - error.NoSpaceLeft => unreachable, // read-only - error.PathAlreadyExists => unreachable, // read-only - error.DeviceBusy => unreachable, // read-only - error.FileBusy => unreachable, // read-only - error.WouldBlock => unreachable, // not using O_NONBLOCK - error.NoDevice => unreachable, // not asking for a special device - - error.AccessDenied, - error.PermissionDenied, - error.FileNotFound, - error.NotDir, - error.IsDir, - => return error.GLibCNotFound, - + error.NameTooLong => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.PipeBusy => return error.Unexpected, // Windows-only + error.SharingViolation => return error.Unexpected, // Windows-only + error.NetworkNotFound => return error.Unexpected, // Windows-only + error.AntivirusInterference => return error.Unexpected, // Windows-only + error.FileLocksNotSupported => return error.Unexpected, // No lock requested. + error.NoSpaceLeft => return error.Unexpected, // read-only + error.PathAlreadyExists => return error.Unexpected, // read-only + error.DeviceBusy => return error.Unexpected, // read-only + error.FileBusy => return error.Unexpected, // read-only + error.NoDevice => return error.Unexpected, // not asking for a special device error.FileTooBig => return error.Unexpected, + error.WouldBlock => return error.Unexpected, // not opened in non-blocking - error.ProcessNotFound, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, + error.AccessDenied => return error.GLibCNotFound, + error.PermissionDenied => return error.GLibCNotFound, + error.FileNotFound => return error.GLibCNotFound, + error.NotDir => return error.GLibCNotFound, + error.IsDir => return error.GLibCNotFound, + + error.ProcessNotFound => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.SystemResources => |e| return e, + error.SymLinkLoop => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, }; defer file.close(); @@ -1016,12 +1008,9 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ error.NameTooLong => return error.Unexpected, error.PathAlreadyExists => return error.Unexpected, error.SharingViolation => return error.Unexpected, - error.InvalidUtf8 => return error.Unexpected, // WASI only - error.InvalidWtf8 => return error.Unexpected, // Windows only error.BadPathName => return error.Unexpected, error.PipeBusy => return error.Unexpected, error.FileLocksNotSupported => return error.Unexpected, - error.WouldBlock => return error.Unexpected, error.FileBusy => return error.Unexpected, // opened without write permissions error.AntivirusInterference => return error.Unexpected, // Windows-only error From b1733b7bcec44ddd73a210ede09c3c9eeb411d27 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 9 Oct 2025 00:47:04 -0700 Subject: [PATCH 086/244] std.Io: implement dirOpenFile --- lib/std/Io.zig | 4 +- lib/std/Io/Dir.zig | 4 +- lib/std/Io/Threaded.zig | 140 +++++++++++++++++++++++++++++--- lib/std/fs.zig | 50 +----------- lib/std/fs/Dir.zig | 171 ++-------------------------------------- 5 files changed, 140 insertions(+), 229 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 320ff3ba2d..9d51c54309 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -657,9 +657,9 @@ pub const VTable = struct { dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, + dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, + dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, - createFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, - fileOpen: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileClose: *const fn (?*anyopaque, File) void, pwrite: *const fn (?*anyopaque, File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, /// Returns 0 on end of stream. diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 5460f6bd60..f07b8f64a8 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -39,11 +39,11 @@ pub const OpenError = error{ } || PathNameError || Io.Cancelable || Io.UnexpectedError; pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); + return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - return io.vtable.createFile(io.userdata, dir, sub_path, flags); + return io.vtable.dirCreateFile(io.userdata, dir, sub_path, flags); } pub const WriteFileOptions = struct { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 02e68f85bb..fac093909c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -178,12 +178,12 @@ pub fn io(pool: *Pool) Io { .wasi => fileStatWasi, else => fileStatPosix, }, - .createFile = switch (builtin.os.tag) { + .dirCreateFile = switch (builtin.os.tag) { .windows => @panic("TODO"), .wasi => @panic("TODO"), - else => createFilePosix, + else => dirCreateFilePosix, }, - .fileOpen = fileOpen, + .dirOpenFile = dirOpenFile, .fileClose = fileClose, .pwrite = pwrite, .fileReadStreaming = fileReadStreaming, @@ -966,8 +966,9 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. } const have_flock = @TypeOf(posix.system.flock) != void; +const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; -fn createFilePosix( +fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, @@ -1003,8 +1004,6 @@ fn createFilePosix( }, }; - const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; - const fd: posix.fd_t = while (true) { try pool.checkCancel(); const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode); @@ -1088,24 +1087,139 @@ fn createFilePosix( return .{ .handle = fd }; } -fn fileOpen( +fn dirOpenFile( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; - const fs_file = try fs_dir.openFile(sub_path, flags); - return .{ .handle = fs_file.handle }; + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var os_flags: posix.O = switch (native_os) { + .wasi => .{ + .read = flags.mode != .write_only, + .write = flags.mode != .read_only, + }, + else => .{ + .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 fd: posix.fd_t = while (true) { + try pool.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, 0); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), + .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, + .SRCH => return error.ProcessNotFound, + .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, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(fd); + + if (have_flock and !has_flock_open_flags and flags.lock != .none) { + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }; + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.flock(fd, lock_flags))) { + .SUCCESS => break, + .INTR => continue, + + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOLCK => return error.SystemResources, + //.AGAIN => return error.WouldBlock, + .OPNOTSUPP => return error.FileLocksNotSupported, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + var fl_flags: usize = while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + return .{ .handle = fd }; } fn fileClose(userdata: ?*anyopaque, file: Io.File) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; - const fs_file: std.fs.File = .{ .handle = file.handle }; - return fs_file.close(); + posix.close(file.handle); } fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 7d5ac75276..e03d426276 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -260,12 +260,6 @@ pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) File.O return cwd().openFile(absolute_path, flags); } -/// Same as `openFileAbsolute` but the path parameter is null-terminated. -pub fn openFileAbsoluteZ(absolute_path_c: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().openFileZ(absolute_path_c, flags); -} - /// Same as `openFileAbsolute` but the path parameter is WTF-16-encoded. pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { assert(path.isAbsoluteWindowsWTF16(absolute_path_w)); @@ -284,11 +278,6 @@ pub fn accessAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Dir.Acce assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } -/// Same as `accessAbsolute` but the path parameter is null-terminated. -pub fn accessAbsoluteZ(absolute_path: [*:0]const u8, flags: File.OpenFlags) Dir.AccessError!void { - assert(path.isAbsoluteZ(absolute_path)); - try cwd().accessZ(absolute_path, flags); -} /// Same as `accessAbsolute` but the path parameter is WTF-16 encoded. pub fn accessAbsoluteW(absolute_path: [*:0]const u16, flags: File.OpenFlags) Dir.AccessError!void { assert(path.isAbsoluteWindowsW(absolute_path)); @@ -309,12 +298,6 @@ pub fn createFileAbsolute(absolute_path: []const u8, flags: File.CreateFlags) Fi return cwd().createFile(absolute_path, flags); } -/// Same as `createFileAbsolute` but the path parameter is null-terminated. -pub fn createFileAbsoluteZ(absolute_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().createFileZ(absolute_path_c, flags); -} - /// Same as `createFileAbsolute` but the path parameter is WTF-16 encoded. pub fn createFileAbsoluteW(absolute_path_w: [*:0]const u16, flags: File.CreateFlags) File.OpenError!File { assert(path.isAbsoluteWindowsW(absolute_path_w)); @@ -333,12 +316,6 @@ pub fn deleteFileAbsolute(absolute_path: []const u8) Dir.DeleteFileError!void { return cwd().deleteFile(absolute_path); } -/// Same as `deleteFileAbsolute` except the parameter is null-terminated. -pub fn deleteFileAbsoluteZ(absolute_path_c: [*:0]const u8) Dir.DeleteFileError!void { - assert(path.isAbsoluteZ(absolute_path_c)); - return cwd().deleteFileZ(absolute_path_c); -} - /// Same as `deleteFileAbsolute` except the parameter is WTF-16 encoded. pub fn deleteFileAbsoluteW(absolute_path_w: [*:0]const u16) Dir.DeleteFileError!void { assert(path.isAbsoluteWindowsW(absolute_path_w)); @@ -383,12 +360,6 @@ pub fn readlinkAbsoluteW(pathname_w: [*:0]const u16, buffer: *[max_path_bytes]u8 return posix.readlinkW(mem.span(pathname_w), buffer); } -/// Same as `readLink`, except the path parameter is null-terminated. -pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsoluteZ(pathname_c)); - return posix.readlinkZ(pathname_c, buffer); -} - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. @@ -426,28 +397,11 @@ pub fn symLinkAbsoluteW( return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); } -/// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. -/// See also `symLinkAbsolute`. -pub fn symLinkAbsoluteZ( - target_path_c: [*:0]const u8, - sym_link_path_c: [*:0]const u8, - flags: Dir.SymLinkFlags, -) !void { - assert(path.isAbsoluteZ(target_path_c)); - assert(path.isAbsoluteZ(sym_link_path_c)); - if (native_os == .windows) { - const target_path_w = try windows.cStrToPrefixedFileW(null, target_path_c); - const sym_link_path_w = try windows.cStrToPrefixedFileW(null, sym_link_path_c); - return windows.CreateSymbolicLink(null, sym_link_path_w.span(), target_path_w.span(), flags.is_directory); - } - return posix.symlinkZ(target_path_c, sym_link_path_c); -} - pub const OpenSelfExeError = posix.OpenError || SelfExePathError || posix.FlockError; pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { if (native_os == .linux or native_os == .serenity) { - return openFileAbsoluteZ("/proc/self/exe", flags); + return openFileAbsolute("/proc/self/exe", flags); } if (native_os == .windows) { // If ImagePathName is a symlink, then it will contain the path of the symlink, @@ -463,7 +417,7 @@ pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { var buf: [max_path_bytes]u8 = undefined; const self_exe_path = try selfExePath(&buf); buf[self_exe_path.len] = 0; - return openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, flags); + return openFileAbsolute(buf[0..self_exe_path.len :0], flags); } // This is `posix.ReadLinkError || posix.RealPathError` with impossible errors excluded diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index f6455215ac..1b5f3808ff 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -885,94 +885,9 @@ pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.Ope const fd = try posix.openatWasi(self.fd, sub_path, .{}, .{}, .{}, base, .{}); return .{ .handle = fd }; } - const path_c = try posix.toPosixPath(sub_path); - return self.openFileZ(&path_c, flags); -} - -/// Same as `openFile` but the path parameter is null-terminated. -pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { - switch (native_os) { - .windows => { - const path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - }, - // Use the libc API when libc is linked because it implements things - // such as opening absolute file paths. - .wasi => if (!builtin.link_libc) { - return openFile(self, mem.sliceTo(sub_path, 0), flags); - }, - else => {}, - } - - var os_flags: posix.O = switch (native_os) { - .wasi => .{ - .read = flags.mode != .write_only, - .write = flags.mode != .read_only, - }, - else => .{ - .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 fd = try posix.openatZ(self.fd, sub_path, os_flags, 0); - errdefer posix.close(fd); - - if (have_flock and !has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - try posix.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => posix.LOCK.SH | lock_nonblocking, - .exclusive => posix.LOCK.EX | lock_nonblocking, - }); - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); - _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return .{ .handle = fd }; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags)); } /// Same as `openFile` but Windows-only and the path parameter is @@ -1048,82 +963,10 @@ pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File }, .{}), }; } - const path_c = try posix.toPosixPath(sub_path); - return self.createFileZ(&path_c, flags); -} - -/// Same as `createFile` but the path parameter is null-terminated. -pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { - switch (native_os) { - .windows => { - const path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.createFileW(path_w.span(), flags); - }, - .wasi => { - return createFile(self, mem.sliceTo(sub_path_c, 0), flags); - }, - else => {}, - } - - 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 fd = try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode); - errdefer posix.close(fd); - - if (have_flock and !has_flock_open_flags and flags.lock != .none) { - // TODO: integrate async I/O - const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; - try posix.flock(fd, switch (flags.lock) { - .none => unreachable, - .shared => posix.LOCK.SH | lock_nonblocking, - .exclusive => posix.LOCK.EX | lock_nonblocking, - }); - } - - if (has_flock_open_flags and flags.lock_nonblocking) { - var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); - _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { - error.FileBusy => unreachable, - error.Locked => unreachable, - error.PermissionDenied => unreachable, - error.DeadLock => unreachable, - error.LockedRegionLimitExceeded => unreachable, - else => |e| return e, - }; - } - - return .{ .handle = fd }; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags); + return .adaptFromNewApi(new_file); } /// Same as `createFile` but Windows-only and the path parameter is From 7d478114ec5ae74f977bf22050088a470f471721 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 9 Oct 2025 11:11:41 -0700 Subject: [PATCH 087/244] fix compilation errors introduced by rebasing --- lib/std/Io/Writer.zig | 4 +++- lib/std/fs/test.zig | 39 +++++++++++++++++++++------------------ lib/std/process/Child.zig | 4 ++++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig index 32aae1673e..b41aa24ccb 100644 --- a/lib/std/Io/Writer.zig +++ b/lib/std/Io/Writer.zig @@ -2873,6 +2873,8 @@ test "allocating sendFile" { } test sendFileReading { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2883,7 +2885,7 @@ test sendFileReading { try file_writer.interface.writeAll("abcd"); try file_writer.interface.flush(); - var file_reader = file_writer.moveToReader(); + var file_reader = file_writer.moveToReader(io); try file_reader.seekTo(0); try file_reader.interface.fill(2); diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 015d595a3a..22451acb66 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1460,6 +1460,8 @@ test "access file" { } test "sendfile" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1470,21 +1472,14 @@ test "sendfile" { const line1 = "line1\n"; const line2 = "second line\n"; - var vecs = [_]posix.iovec_const{ - .{ - .base = line1, - .len = line1.len, - }, - .{ - .base = line2, - .len = line2.len, - }, - }; + var vecs = [_][]const u8{ line1, line2 }; var src_file = try dir.createFile("sendfile1.txt", .{ .read = true }); defer src_file.close(); - - try src_file.writevAll(&vecs); + { + var fw = src_file.writer(&.{}); + try fw.interface.writeVecAll(&vecs); + } var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true }); defer dest_file.close(); @@ -1497,7 +1492,7 @@ test "sendfile" { var trailers: [2][]const u8 = .{ trailer1, trailer2 }; var written_buf: [100]u8 = undefined; - var file_reader = src_file.reader(&.{}); + var file_reader = src_file.reader(io, &.{}); var fallback_buffer: [50]u8 = undefined; var file_writer = dest_file.writer(&fallback_buffer); try file_writer.interface.writeVecAll(&headers); @@ -1505,11 +1500,15 @@ test "sendfile" { try testing.expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10))); try file_writer.interface.writeVecAll(&trailers); try file_writer.interface.flush(); - const amt = try dest_file.preadAll(&written_buf, 0); + var fr = file_writer.moveToReader(io); + try fr.seekTo(0); + const amt = try fr.interface.readSliceShort(&written_buf); try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]); } test "sendfile with buffered data" { + const io = testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -1527,7 +1526,7 @@ test "sendfile with buffered data" { defer dest_file.close(); var src_buffer: [32]u8 = undefined; - var file_reader = src_file.reader(&src_buffer); + var file_reader = src_file.reader(io, &src_buffer); try file_reader.seekTo(0); try file_reader.interface.fill(8); @@ -1538,7 +1537,9 @@ test "sendfile with buffered data" { try std.testing.expectEqual(4, try file_writer.interface.sendFileAll(&file_reader, .limited(4))); var written_buf: [8]u8 = undefined; - const amt = try dest_file.preadAll(&written_buf, 0); + var fr = file_writer.moveToReader(io); + try fr.seekTo(0); + const amt = try fr.interface.readSliceShort(&written_buf); try std.testing.expectEqual(4, amt); try std.testing.expectEqualSlices(u8, "AAAA", written_buf[0..amt]); @@ -2250,6 +2251,8 @@ test "seekTo flushes buffered data" { } test "File.Writer sendfile with buffered contents" { + const io = testing.io; + var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -2261,7 +2264,7 @@ test "File.Writer sendfile with buffered contents" { defer out.close(); var in_buf: [2]u8 = undefined; - var in_r = in.reader(&in_buf); + var in_r = in.reader(io, &in_buf); _ = try in_r.getSize(); // Catch seeks past end by populating size try in_r.interface.fill(2); @@ -2275,7 +2278,7 @@ test "File.Writer sendfile with buffered contents" { var check = try tmp_dir.dir.openFile("b", .{}); defer check.close(); var check_buf: [4]u8 = undefined; - var check_r = check.reader(&check_buf); + var check_r = check.reader(io, &check_buf); try testing.expectEqualStrings("abcd", try check_r.interface.take(4)); try testing.expectError(error.EndOfStream, check_r.interface.takeByte()); } diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index c018d77424..7ca919ca46 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -572,6 +572,10 @@ fn spawnPosix(self: *ChildProcess) SpawnError!void { error.BadPathName => unreachable, // Windows-only error.WouldBlock => unreachable, error.NetworkNotFound => unreachable, // Windows-only + error.Canceled => unreachable, // temporarily in the posix error set + error.SharingViolation => unreachable, // Windows-only + error.PipeBusy => unreachable, // not a pipe + error.AntivirusInterference => unreachable, // Windows-only else => |e| return e, } else From fcac8617b4e3c8fcb3a8c888fe77fa8bff1c2057 Mon Sep 17 00:00:00 2001 From: Lukas Lalinsky Date: Thu, 9 Oct 2025 08:52:52 +0200 Subject: [PATCH 088/244] Add missing clobbers to context switching This only shows in release mode, the compiler tries to preserve some value in rdi, but that gets replaced inside the fiber. This would not happen in the C calling convention, but in these normal Zig functions, it can happen. --- lib/std/Io/EventLoop.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index d1d7799907..4cb5745e31 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -644,6 +644,7 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { : [received_message] "={x1}" (-> *const @FieldType(SwitchMessage, "contexts")), : [message_to_send] "{x1}" (&message.contexts), : .{ + .x0 = true, .x1 = true, .x2 = true, .x3 = true, @@ -745,6 +746,7 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { .rdx = true, .rbx = true, .rsi = true, + .rdi = true, .r8 = true, .r9 = true, .r10 = true, From 63801c4b058feae3a9b6ca2bbe8effeae267abbc Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 9 Oct 2025 15:24:11 -0700 Subject: [PATCH 089/244] std.crypto.Certificate.Bundle: remove use of File.readAll --- lib/std/crypto/Certificate/Bundle.zig | 103 ++++++++++++++------------ lib/std/http/Client.zig | 4 +- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index dd65b4cc4a..1a96688a0f 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -4,6 +4,20 @@ //! concatenated together in the `bytes` array. The `map` field contains an //! index from the DER-encoded subject name to the index of the containing //! certificate within `bytes`. +const Bundle = @This(); +const builtin = @import("builtin"); + +const std = @import("../../std.zig"); +const Io = std.Io; +const assert = std.debug.assert; +const fs = std.fs; +const mem = std.mem; +const crypto = std.crypto; +const Allocator = std.mem.Allocator; +const Certificate = std.crypto.Certificate; +const der = Certificate.der; + +const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); /// The key is the contents slice of the subject. map: std.HashMapUnmanaged(der.Element.Slice, u32, MapContext, std.hash_map.default_max_load_percentage) = .empty, @@ -56,17 +70,17 @@ pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathErro /// file system standard locations for certificates. /// For operating systems that do not have standard CA installations to be /// found, this function clears the set of certificates. -pub fn rescan(cb: *Bundle, gpa: Allocator) RescanError!void { +pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io) RescanError!void { switch (builtin.os.tag) { - .linux => return rescanLinux(cb, gpa), + .linux => return rescanLinux(cb, gpa, io), .macos => return rescanMac(cb, gpa), - .freebsd, .openbsd => return rescanWithPath(cb, gpa, "/etc/ssl/cert.pem"), - .netbsd => return rescanWithPath(cb, gpa, "/etc/openssl/certs/ca-certificates.crt"), - .dragonfly => return rescanWithPath(cb, gpa, "/usr/local/etc/ssl/cert.pem"), - .illumos => return rescanWithPath(cb, gpa, "/etc/ssl/cacert.pem"), - .haiku => return rescanWithPath(cb, gpa, "/boot/system/data/ssl/CARootCertificates.pem"), + .freebsd, .openbsd => return rescanWithPath(cb, gpa, io, "/etc/ssl/cert.pem"), + .netbsd => return rescanWithPath(cb, gpa, io, "/etc/openssl/certs/ca-certificates.crt"), + .dragonfly => return rescanWithPath(cb, gpa, io, "/usr/local/etc/ssl/cert.pem"), + .illumos => return rescanWithPath(cb, gpa, io, "/etc/ssl/cacert.pem"), + .haiku => return rescanWithPath(cb, gpa, io, "/boot/system/data/ssl/CARootCertificates.pem"), // https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19 - .serenity => return rescanWithPath(cb, gpa, "/etc/ssl/certs/ca-certificates.crt"), + .serenity => return rescanWithPath(cb, gpa, io, "/etc/ssl/certs/ca-certificates.crt"), .windows => return rescanWindows(cb, gpa), else => {}, } @@ -77,7 +91,7 @@ const RescanMacError = @import("Bundle/macos.zig").RescanMacError; const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError; -fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { +fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io) RescanLinuxError!void { // Possible certificate files; stop after finding one. const cert_file_paths = [_][]const u8{ "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. @@ -100,7 +114,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { scan: { for (cert_file_paths) |cert_file_path| { - if (addCertsFromFilePathAbsolute(cb, gpa, cert_file_path)) |_| { + if (addCertsFromFilePathAbsolute(cb, gpa, io, cert_file_path)) |_| { break :scan; } else |err| switch (err) { error.FileNotFound => continue, @@ -109,7 +123,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { } for (cert_dir_paths) |cert_dir_path| { - addCertsFromDirPathAbsolute(cb, gpa, cert_dir_path) catch |err| switch (err) { + addCertsFromDirPathAbsolute(cb, gpa, io, cert_dir_path) catch |err| switch (err) { error.FileNotFound => continue, else => |e| return e, }; @@ -121,10 +135,10 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator) RescanLinuxError!void { const RescanWithPathError = AddCertsFromFilePathError; -fn rescanWithPath(cb: *Bundle, gpa: Allocator, cert_file_path: []const u8) RescanWithPathError!void { +fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, cert_file_path: []const u8) RescanWithPathError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); - try addCertsFromFilePathAbsolute(cb, gpa, cert_file_path); + try addCertsFromFilePathAbsolute(cb, gpa, io, cert_file_path); cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); } @@ -160,28 +174,30 @@ pub const AddCertsFromDirPathError = fs.File.OpenError || AddCertsFromDirError; pub fn addCertsFromDirPath( cb: *Bundle, gpa: Allocator, + io: Io, dir: fs.Dir, sub_dir_path: []const u8, ) AddCertsFromDirPathError!void { var iterable_dir = try dir.openDir(sub_dir_path, .{ .iterate = true }); defer iterable_dir.close(); - return addCertsFromDir(cb, gpa, iterable_dir); + return addCertsFromDir(cb, gpa, io, iterable_dir); } pub fn addCertsFromDirPathAbsolute( cb: *Bundle, gpa: Allocator, + io: Io, abs_dir_path: []const u8, ) AddCertsFromDirPathError!void { assert(fs.path.isAbsolute(abs_dir_path)); var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true }); defer iterable_dir.close(); - return addCertsFromDir(cb, gpa, iterable_dir); + return addCertsFromDir(cb, gpa, io, iterable_dir); } pub const AddCertsFromDirError = AddCertsFromFilePathError; -pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCertsFromDirError!void { +pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, iterable_dir: fs.Dir) AddCertsFromDirError!void { var it = iterable_dir.iterate(); while (try it.next()) |entry| { switch (entry.kind) { @@ -189,32 +205,37 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, iterable_dir: fs.Dir) AddCer else => continue, } - try addCertsFromFilePath(cb, gpa, iterable_dir, entry.name); + try addCertsFromFilePath(cb, gpa, io, iterable_dir.adaptToNewApi(), entry.name); } } -pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError; +pub const AddCertsFromFilePathError = fs.File.OpenError || AddCertsFromFileError || Io.Clock.Error; pub fn addCertsFromFilePathAbsolute( cb: *Bundle, gpa: Allocator, + io: Io, abs_file_path: []const u8, ) AddCertsFromFilePathError!void { - assert(fs.path.isAbsolute(abs_file_path)); + const now = try Io.Clock.real.now(io); var file = try fs.openFileAbsolute(abs_file_path, .{}); defer file.close(); - return addCertsFromFile(cb, gpa, file); + var file_reader = file.reader(io, &.{}); + return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); } pub fn addCertsFromFilePath( cb: *Bundle, gpa: Allocator, - dir: fs.Dir, + io: Io, + dir: Io.Dir, sub_file_path: []const u8, ) AddCertsFromFilePathError!void { - var file = try dir.openFile(sub_file_path, .{}); - defer file.close(); - return addCertsFromFile(cb, gpa, file); + const now = try Io.Clock.real.now(io); + var file = try dir.openFile(io, sub_file_path, .{}); + defer file.close(io); + var file_reader = file.reader(io, &.{}); + return addCertsFromFile(cb, gpa, &file_reader, now.toSeconds()); } pub const AddCertsFromFileError = Allocator.Error || @@ -222,10 +243,10 @@ pub const AddCertsFromFileError = Allocator.Error || fs.File.ReadError || ParseCertError || std.base64.Error || - error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker }; + error{ CertificateAuthorityBundleTooBig, MissingEndCertificateMarker, Streaming }; -pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFromFileError!void { - const size = try file.getEndPos(); +pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file_reader: *Io.File.Reader, now_sec: i64) AddCertsFromFileError!void { + const size = try file_reader.getSize(); // We borrow `bytes` as a temporary buffer for the base64-encoded data. // This is possible by computing the decoded length and reserving the space @@ -236,14 +257,14 @@ pub fn addCertsFromFile(cb: *Bundle, gpa: Allocator, file: fs.File) AddCertsFrom try cb.bytes.ensureUnusedCapacity(gpa, needed_capacity); const end_reserved: u32 = @intCast(cb.bytes.items.len + decoded_size_upper_bound); const buffer = cb.bytes.allocatedSlice()[end_reserved..]; - const end_index = try file.readAll(buffer); + const end_index = file_reader.interface.readSliceShort(buffer) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; const encoded_bytes = buffer[0..end_index]; const begin_marker = "-----BEGIN CERTIFICATE-----"; const end_marker = "-----END CERTIFICATE-----"; - const now_sec = std.time.timestamp(); - var start_index: usize = 0; while (mem.indexOfPos(u8, encoded_bytes, start_index, begin_marker)) |begin_marker_start| { const cert_start = begin_marker_start + begin_marker.len; @@ -288,19 +309,6 @@ pub fn parseCert(cb: *Bundle, gpa: Allocator, decoded_start: u32, now_sec: i64) } } -const builtin = @import("builtin"); -const std = @import("../../std.zig"); -const assert = std.debug.assert; -const fs = std.fs; -const mem = std.mem; -const crypto = std.crypto; -const Allocator = std.mem.Allocator; -const Certificate = std.crypto.Certificate; -const der = Certificate.der; -const Bundle = @This(); - -const base64 = std.base64.standard.decoderWithIgnore(" \t\r\n"); - const MapContext = struct { cb: *const Bundle, @@ -321,8 +329,11 @@ const MapContext = struct { test "scan for OS-provided certificates" { if (builtin.os.tag == .wasi) return error.SkipZigTest; - var bundle: Bundle = .{}; - defer bundle.deinit(std.testing.allocator); + const io = std.testing.io; + const gpa = std.testing.allocator; - try bundle.rescan(std.testing.allocator); + var bundle: Bundle = .{}; + defer bundle.deinit(gpa); + + try bundle.rescan(gpa, io); } diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index b05a0a317e..7106698681 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -1666,6 +1666,8 @@ pub fn request( uri: Uri, options: RequestOptions, ) RequestError!Request { + const io = client.io; + if (std.debug.runtime_safety) { for (options.extra_headers) |header| { assert(header.name.len != 0); @@ -1689,7 +1691,7 @@ pub fn request( defer client.ca_bundle_mutex.unlock(); if (client.next_https_rescan_certs) { - client.ca_bundle.rescan(client.allocator) catch + client.ca_bundle.rescan(client.allocator, io) catch return error.CertificateBundleLoadFailure; @atomicStore(bool, &client.next_https_rescan_certs, false, .release); } From 9e681cab56eb306f3e5ba88a951625408f72f696 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 9 Oct 2025 16:30:27 -0700 Subject: [PATCH 090/244] std.Uri: fix compilation error --- lib/std/Uri.zig | 26 +++++++++++++++++++------- lib/std/http/Client.zig | 1 + 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index a27f208e65..1e2b4fb921 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -197,7 +197,12 @@ pub fn percentDecodeInPlace(buffer: []u8) []u8 { return percentDecodeBackwards(buffer, buffer); } -pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort }; +pub const ParseError = error{ + UnexpectedCharacter, + InvalidFormat, + InvalidPort, + InvalidHostName, +}; /// Parses the URI or returns an error. This function is not compliant, but is required to parse /// some forms of URIs in the wild, such as HTTP Location headers. @@ -400,7 +405,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = new_parsed.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = try validateHost(new_parsed.host), + .host = try validateHostComponent(new_parsed.host), .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -411,7 +416,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = new_parsed.user, .password = new_parsed.password, - .host = try validateHost(host), + .host = try validateHostComponent(host), .port = new_parsed.port, .path = remove_dot_segments(new_path), .query = new_parsed.query, @@ -433,7 +438,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE .scheme = base.scheme, .user = base.user, .password = base.password, - .host = try validateHost(base.host), + .host = try validateHostComponent(base.host), .port = base.port, .path = path, .query = query, @@ -441,9 +446,16 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE }; } -fn validateHost(bytes: []const u8) HostName.ValidateError![]const u8 { - try HostName.validate(bytes); - return bytes; +fn validateHostComponent(optional_component: ?Component) error{InvalidHostName}!?Component { + const component = optional_component orelse return null; + switch (component) { + .raw => |raw| HostName.validate(raw) catch return error.InvalidHostName, + .percent_encoded => |encoded| { + // TODO validate decoded name instead + HostName.validate(encoded) catch return error.InvalidHostName; + }, + } + return component; } /// In-place implementation of RFC 3986, Section 5.2.4. diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 7106698681..2660d22be9 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -1212,6 +1212,7 @@ pub const Request = struct { error.UnexpectedCharacter => return error.HttpRedirectLocationInvalid, error.InvalidFormat => return error.HttpRedirectLocationInvalid, error.InvalidPort => return error.HttpRedirectLocationInvalid, + error.InvalidHostName => return error.HttpRedirectLocationInvalid, error.NoSpaceLeft => return error.HttpRedirectLocationOversize, }; From 3b346223681b40ce1271688cbe4d650a43b8e613 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 10 Oct 2025 22:37:54 -0700 Subject: [PATCH 091/244] std.Io: add unix domain sockets API note that "reuseaddr" does nothing for these --- lib/std/Io.zig | 10 ++++++---- lib/std/Io/Threaded.zig | 41 +++++++++++++++++++++++++++------------- lib/std/Io/net.zig | 42 +++++++++++++++++++++++++++++++++++++---- lib/std/Io/net/test.zig | 26 +++++-------------------- 4 files changed, 77 insertions(+), 42 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 9d51c54309..8f8e9a122d 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -672,10 +672,12 @@ pub const VTable = struct { now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, - listen: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, - accept: *const fn (?*anyopaque, server: *net.Server) net.Server.AcceptError!net.Stream, - ipBind: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, - ipConnect: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream, + netListenIp: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, + netAccept: *const fn (?*anyopaque, server: net.Socket.Handle) net.Server.AcceptError!net.Stream, + netBindIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, + netConnectIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream, + netListenUnix: *const fn (?*anyopaque, net.UnixAddress) net.UnixAddress.ListenError!net.Socket.Handle, + netConnectUnix: *const fn (?*anyopaque, net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle, netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) struct { ?net.Socket.SendError, usize }, netReceive: *const fn (?*anyopaque, net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, net.ReceiveFlags, Timeout) struct { ?net.Socket.ReceiveTimeoutError, usize }, netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index fac093909c..88120f4b04 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -203,22 +203,24 @@ pub fn io(pool: *Pool) Io { else => sleepPosix, }, - .listen = switch (builtin.os.tag) { + .netListenIp = switch (builtin.os.tag) { .windows => @panic("TODO"), - else => listenPosix, + else => netListenIpPosix, }, - .accept = switch (builtin.os.tag) { + .netListenUnix = netListenUnix, + .netAccept = switch (builtin.os.tag) { .windows => @panic("TODO"), - else => acceptPosix, + else => netAcceptPosix, }, - .ipBind = switch (builtin.os.tag) { + .netBindIp = switch (builtin.os.tag) { .windows => @panic("TODO"), - else => ipBindPosix, + else => netBindIpPosix, }, - .ipConnect = switch (builtin.os.tag) { + .netConnectIp = switch (builtin.os.tag) { .windows => @panic("TODO"), - else => ipConnectPosix, + else => netConnectIpPosix, }, + .netConnectUnix = netConnectUnix, .netClose = netClose, .netRead = switch (builtin.os.tag) { .windows => @panic("TODO"), @@ -1636,7 +1638,7 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { return result.?; } -fn listenPosix( +fn netListenIpPosix( userdata: ?*anyopaque, address: Io.net.IpAddress, options: Io.net.IpAddress.ListenOptions, @@ -1702,6 +1704,13 @@ fn listenPosix( }; } +fn netListenUnix(userdata: ?*anyopaque, address: Io.net.UnixAddress) Io.net.UnixAddress.ListenError!Io.net.Socket.Handle { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + _ = pool; + _ = address; + @panic("TODO"); +} + fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { try pool.checkCancel(); @@ -1784,7 +1793,7 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio } } -fn ipConnectPosix( +fn netConnectIpPosix( userdata: ?*anyopaque, address: *const Io.net.IpAddress, options: Io.net.IpAddress.ConnectOptions, @@ -1806,7 +1815,14 @@ fn ipConnectPosix( } }; } -fn ipBindPosix( +fn netConnectUnix(userdata: ?*anyopaque, address: Io.net.UnixAddress) Io.net.UnixAddress.ConnectError!Io.net.Socket.Handle { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + _ = pool; + _ = address; + @panic("TODO"); +} + +fn netBindIpPosix( userdata: ?*anyopaque, address: *const Io.net.IpAddress, options: Io.net.IpAddress.BindOptions, @@ -1871,9 +1887,8 @@ fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: Io.net.IpAdd const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 const have_accept4 = !socket_flags_unsupported; -fn acceptPosix(userdata: ?*anyopaque, server: *Io.net.Server) Io.net.Server.AcceptError!Io.net.Stream { +fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: Io.net.Socket.Handle) Io.net.Server.AcceptError!Io.net.Stream { const pool: *Pool = @ptrCast(@alignCast(userdata)); - const listen_fd = server.socket.handle; var storage: PosixAddress = undefined; var addr_len: posix.socklen_t = @sizeOf(PosixAddress); const fd = while (true) { diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index dc1080ab38..fe2f7e9973 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -219,7 +219,7 @@ pub const IpAddress = union(enum) { /// Waits for a TCP connection. When using this API, `bind` does not need /// to be called. The returned `Server` has an open `stream`. pub fn listen(address: IpAddress, io: Io, options: ListenOptions) ListenError!Server { - return io.vtable.listen(io.userdata, address, options); + return io.vtable.netListenIp(io.userdata, address, options); } pub const BindError = error{ @@ -262,7 +262,7 @@ pub const IpAddress = union(enum) { /// One bound `Socket` can be used to receive messages from multiple /// different addresses. pub fn bind(address: *const IpAddress, io: Io, options: BindOptions) BindError!Socket { - return io.vtable.ipBind(io.userdata, address, options); + return io.vtable.netBindIp(io.userdata, address, options); } pub const ConnectError = error{ @@ -298,7 +298,7 @@ pub const IpAddress = union(enum) { /// Initiates a connection-oriented network stream. pub fn connect(address: *const IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { - return io.vtable.ipConnect(io.userdata, address, options); + return io.vtable.netConnectIp(io.userdata, address, options); } }; @@ -775,6 +775,39 @@ pub const Ip6Address = struct { }; }; +pub const UnixAddress = struct { + path: []const u8, + + pub const max_len = 108; + + pub const InitError = error{NameTooLong}; + + pub fn init(p: []const u8) InitError!UnixAddress { + if (p.len > max_len) return error.NameTooLong; + return .{ .path = p }; + } + + pub const ListenError = error{}; + + pub fn listen(ua: UnixAddress, io: Io) ListenError!Server { + assert(ua.path.len <= max_len); + return .{ .socket = .{ + .handle = try io.vtable.netListenUnix(io.userdata, ua), + .address = .{ .ip4 = .loopback(0) }, + } }; + } + + pub const ConnectError = error{}; + + pub fn connect(ua: UnixAddress, io: Io) ConnectError!Stream { + assert(ua.path.len <= max_len); + return .{ .socket = .{ + .handle = try io.vtable.netConnectUnix(io.userdata, ua), + .address = .{ .ip4 = .loopback(0) }, + } }; + } +}; + pub const ReceiveFlags = packed struct(u8) { oob: bool = false, peek: bool = false, @@ -917,6 +950,7 @@ pub const Socket = struct { else => std.posix.fd_t, }; + /// Leaves `address` in a valid state. pub fn close(s: *Socket, io: Io) void { io.vtable.netClose(io.userdata, s.handle); s.handle = undefined; @@ -1156,7 +1190,7 @@ pub const Server = struct { /// Blocks until a client connects to the server. pub fn accept(s: *Server, io: Io) AcceptError!Stream { - return io.vtable.accept(io.userdata, s); + return io.vtable.netAccept(io.userdata, s.socket.handle); } }; diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index f1fb6cd57c..2663227e19 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -290,15 +290,16 @@ test "listen on a unix socket, send bytes, receive bytes" { const socket_path = try generateFileName("socket.unix"); defer testing.allocator.free(socket_path); - const socket_addr = try net.IpAddress.initUnix(socket_path); + const socket_addr = try net.UnixAddress.init(socket_path); defer std.fs.cwd().deleteFile(socket_path) catch {}; - var server = try socket_addr.listen(io, .{}); - defer server.deinit(io); + var server = try socket_addr.listen(io); + defer server.socket.close(io); const S = struct { fn clientFn(path: []const u8) !void { - var stream = try net.connectUnixSocket(path); + const server_path: net.UnixAddress = try .init(path); + var stream = try server_path.connect(io); defer stream.close(io); var stream_writer = stream.writer(io, &.{}); @@ -319,23 +320,6 @@ test "listen on a unix socket, send bytes, receive bytes" { try testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); } -test "listen on a unix socket with reuse_address option" { - if (!net.has_unix_sockets) return error.SkipZigTest; - // Windows doesn't implement reuse port option. - if (builtin.os.tag == .windows) return error.SkipZigTest; - - const io = testing.io; - - const socket_path = try generateFileName("socket.unix"); - defer testing.allocator.free(socket_path); - - const socket_addr = try net.Address.initUnix(socket_path); - defer std.fs.cwd().deleteFile(socket_path) catch {}; - - var server = try socket_addr.listen(io, .{ .reuse_address = true }); - server.deinit(io); -} - fn generateFileName(base_name: []const u8) ![]const u8 { const random_bytes_count = 12; const sub_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count); From d40803284e6395e7a4a065a73c9fb09d1c0ff7a8 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 10 Oct 2025 22:48:12 -0700 Subject: [PATCH 092/244] progress towards compiler building again --- lib/std/zig/ErrorBundle.zig | 12 ++++---- src/Compilation.zig | 12 ++++---- src/main.zig | 55 ++++++++++++++++++------------------- src/print_env.zig | 3 +- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index 8ec3ae18b6..bbc758f5e8 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -6,12 +6,13 @@ //! There is one special encoding for this data structure. If both arrays are //! empty, it means there are no errors. This special encoding exists so that //! heap allocation is not needed in the common case of no errors. +const ErrorBundle = @This(); const std = @import("std"); -const ErrorBundle = @This(); +const Io = std.Io; +const Writer = std.Io.Writer; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const Writer = std.Io.Writer; string_bytes: []const u8, /// The first thing in this array is an `ErrorMessageList`. @@ -156,7 +157,7 @@ pub fn nullTerminatedString(eb: ErrorBundle, index: String) [:0]const u8 { } pub const RenderOptions = struct { - ttyconf: std.Io.tty.Config, + ttyconf: Io.tty.Config, include_reference_trace: bool = true, include_source_line: bool = true, include_log_text: bool = true, @@ -190,7 +191,7 @@ fn renderErrorMessageToWriter( err_msg_index: MessageIndex, w: *Writer, kind: []const u8, - color: std.Io.tty.Color, + color: Io.tty.Color, indent: usize, ) (Writer.Error || std.posix.UnexpectedError)!void { const ttyconf = options.ttyconf; @@ -320,6 +321,7 @@ fn writeMsg(eb: ErrorBundle, err_msg: ErrorMessage, w: *Writer, indent: usize) ! pub const Wip = struct { gpa: Allocator, + io: Io, string_bytes: std.ArrayListUnmanaged(u8), /// The first thing in this array is a ErrorMessageList. extra: std.ArrayListUnmanaged(u32), @@ -806,7 +808,7 @@ pub const Wip = struct { }; defer bundle.deinit(std.testing.allocator); - const ttyconf: std.Io.tty.Config = .no_color; + const ttyconf: Io.tty.Config = .no_color; var bundle_buf: Writer.Allocating = .init(std.testing.allocator); const bundle_bw = &bundle_buf.interface; diff --git a/src/Compilation.zig b/src/Compilation.zig index 760c8f8b11..0692c4ce80 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -1,7 +1,9 @@ const Compilation = @This(); +const builtin = @import("builtin"); const std = @import("std"); -const builtin = @import("builtin"); +const Io = std.Io; +const Writer = std.Io.Writer; const fs = std.fs; const mem = std.mem; const Allocator = std.mem.Allocator; @@ -12,7 +14,6 @@ const ThreadPool = std.Thread.Pool; const WaitGroup = std.Thread.WaitGroup; const ErrorBundle = std.zig.ErrorBundle; const fatal = std.process.fatal; -const Writer = std.Io.Writer; const Value = @import("Value.zig"); const Type = @import("Type.zig"); @@ -1095,6 +1096,7 @@ pub const CObject = struct { bundle: Bundle, notes_len: u32, ) !ErrorBundle.ErrorMessage { + const io = eb.io; var start = diag.src_loc.offset; var end = diag.src_loc.offset; for (diag.src_ranges) |src_range| { @@ -1117,7 +1119,7 @@ pub const CObject = struct { const file = fs.cwd().openFile(file_name, .{}) catch break :source_line 0; defer file.close(); var buffer: [1024]u8 = undefined; - var file_reader = file.reader(&buffer); + var file_reader = file.reader(io, &buffer); file_reader.seekTo(diag.src_loc.offset + 1 - diag.src_loc.column) catch break :source_line 0; var aw: Writer.Allocating = .init(eb.gpa); defer aw.deinit(); @@ -1155,7 +1157,7 @@ pub const CObject = struct { gpa.destroy(bundle); } - pub fn parse(gpa: Allocator, path: []const u8) !*Bundle { + pub fn parse(gpa: Allocator, io: Io, path: []const u8) !*Bundle { const BlockId = enum(u32) { Meta = 8, Diag, @@ -1191,7 +1193,7 @@ pub const CObject = struct { var buffer: [1024]u8 = undefined; const file = try fs.cwd().openFile(path, .{}); defer file.close(); - var file_reader = file.reader(&buffer); + var file_reader = file.reader(io, &buffer); var bc = std.zig.llvm.BitcodeReader.init(gpa, .{ .reader = &file_reader.interface }); defer bc.deinit(); diff --git a/src/main.zig b/src/main.zig index 76c77a7b83..6ab768aac7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -319,7 +319,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { .root_src_path = "objcopy.zig", }); } else if (mem.eql(u8, cmd, "fetch")) { - return cmdFetch(gpa, arena, cmd_args); + return cmdFetch(gpa, arena, io, cmd_args); } else if (mem.eql(u8, cmd, "libc")) { return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "libc", @@ -348,12 +348,14 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { return; } else if (mem.eql(u8, cmd, "env")) { dev.check(.env_command); + const host = std.zig.resolveTargetQueryOrFatal(io, .{}); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try @import("print_env.zig").cmdEnv( arena, &stdout_writer.interface, args, if (native_os == .wasi) wasi_preopens, + &host, ); return stdout_writer.interface.flush(); } else if (mem.eql(u8, cmd, "reduce")) { @@ -368,11 +370,11 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { dev.check(.help_command); return fs.File.stdout().writeAll(usage); } else if (mem.eql(u8, cmd, "ast-check")) { - return cmdAstCheck(arena, cmd_args); + return cmdAstCheck(arena, io, cmd_args); } else if (mem.eql(u8, cmd, "detect-cpu")) { return cmdDetectCpu(io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "changelist")) { - return cmdChangelist(arena, cmd_args); + return cmdChangelist(arena, io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "dump-zir")) { return cmdDumpZir(arena, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "llvm-ints")) { @@ -741,7 +743,7 @@ const ArgMode = union(enum) { const Listen = union(enum) { none, stdio: if (dev.env.supports(.stdio_listen)) void else noreturn, - ip4: if (dev.env.supports(.network_listen)) std.net.Ip4Address else noreturn, + ip4: if (dev.env.supports(.network_listen)) Io.net.Ip4Address else noreturn, }; const ArgsIterator = struct { @@ -1335,7 +1337,7 @@ fn buildOutputType( const host, const port_text = mem.cutScalar(u8, next_arg, ':') orelse .{ next_arg, "14735" }; const port = std.fmt.parseInt(u16, port_text, 10) catch |err| fatal("invalid port number: '{s}': {s}", .{ port_text, @errorName(err) }); - listen = .{ .ip4 = std.net.Ip4Address.parse(host, port) catch |err| + listen = .{ .ip4 = Io.net.Ip4Address.parse(host, port) catch |err| fatal("invalid host: '{s}': {s}", .{ host, @errorName(err) }) }; } } else if (mem.eql(u8, arg, "--listen=-")) { @@ -3318,7 +3320,7 @@ fn buildOutputType( var file_writer = f.writer(&.{}); var buffer: [1000]u8 = undefined; var hasher = file_writer.interface.hashed(Cache.Hasher.init("0123456789abcdef"), &buffer); - var stdin_reader = fs.File.stdin().readerStreaming(&.{}); + var stdin_reader = fs.File.stdin().readerStreaming(io, &.{}); _ = hasher.writer.sendFileAll(&stdin_reader, .unlimited) catch |err| switch (err) { error.WriteFailed => fatal("failed to write {s}: {t}", .{ dump_path, file_writer.err.? }), else => fatal("failed to pipe stdin to {s}: {t}", .{ dump_path, err }), @@ -3549,7 +3551,7 @@ fn buildOutputType( switch (listen) { .none => {}, .stdio => { - var stdin_reader = fs.File.stdin().reader(&stdin_buffer); + var stdin_reader = fs.File.stdin().reader(io, &stdin_buffer); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try serve( io, @@ -3565,23 +3567,23 @@ fn buildOutputType( return cleanExit(); }, .ip4 => |ip4_addr| { - const addr: std.net.Address = .{ .in = ip4_addr }; + const addr: Io.net.IpAddress = .{ .ip4 = ip4_addr }; - var server = try addr.listen(.{ + var server = try addr.listen(io, .{ .reuse_address = true, }); - defer server.deinit(); + defer server.deinit(io); - const conn = try server.accept(); - defer conn.stream.close(); + var stream = try server.accept(io); + defer stream.close(io); - var input = conn.stream.reader(&stdin_buffer); - var output = conn.stream.writer(&stdout_buffer); + var input = stream.reader(io, &stdin_buffer); + var output = stream.writer(io, &stdout_buffer); try serve( io, comp, - input.interface(), + &input.interface, &output.interface, test_exec_args.items, self_exe_path, @@ -5062,7 +5064,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct { allocator: Allocator, fn deinit(_: @This()) void {} - } = .{ .allocator = gpa }; + } = .{ .allocator = gpa, .io = io }; defer http_client.deinit(); var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{}; @@ -5600,7 +5602,7 @@ fn jitCmd( try child.spawn(); if (options.capture) |ptr| { - var stdout_reader = child.stdout.?.readerStreaming(&.{}); + var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); ptr.* = try stdout_reader.interface.allocRemaining(arena, .limited(std.math.maxInt(u32))); } @@ -6055,10 +6057,7 @@ const usage_ast_check = \\ ; -fn cmdAstCheck( - arena: Allocator, - args: []const []const u8, -) !void { +fn cmdAstCheck(arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.ast_check_command); const Zir = std.zig.Zir; @@ -6106,7 +6105,7 @@ fn cmdAstCheck( }; } else fs.File.stdin(); defer if (zig_source_path != null) f.close(); - var file_reader: fs.File.Reader = f.reader(&stdin_buffer); + var file_reader: fs.File.Reader = f.reader(io, &stdin_buffer); break :s std.zig.readSourceFileToEndAlloc(arena, &file_reader) catch |err| { fatal("unable to load file '{s}' for ast-check: {s}", .{ display_path, @errorName(err) }); }; @@ -6448,10 +6447,7 @@ fn cmdDumpZir( } /// This is only enabled for debug builds. -fn cmdChangelist( - arena: Allocator, - args: []const []const u8, -) !void { +fn cmdChangelist(arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.changelist_command); const color: Color = .auto; @@ -6464,7 +6460,7 @@ fn cmdChangelist( var f = fs.cwd().openFile(old_source_path, .{}) catch |err| fatal("unable to open old source file '{s}': {s}", .{ old_source_path, @errorName(err) }); defer f.close(); - var file_reader: fs.File.Reader = f.reader(&stdin_buffer); + var file_reader: fs.File.Reader = f.reader(io, &stdin_buffer); break :source std.zig.readSourceFileToEndAlloc(arena, &file_reader) catch |err| fatal("unable to read old source file '{s}': {s}", .{ old_source_path, @errorName(err) }); }; @@ -6472,7 +6468,7 @@ fn cmdChangelist( var f = fs.cwd().openFile(new_source_path, .{}) catch |err| fatal("unable to open new source file '{s}': {s}", .{ new_source_path, @errorName(err) }); defer f.close(); - var file_reader: fs.File.Reader = f.reader(&stdin_buffer); + var file_reader: fs.File.Reader = f.reader(io, &stdin_buffer); break :source std.zig.readSourceFileToEndAlloc(arena, &file_reader) catch |err| fatal("unable to read new source file '{s}': {s}", .{ new_source_path, @errorName(err) }); }; @@ -6829,6 +6825,7 @@ const usage_fetch = fn cmdFetch( gpa: Allocator, arena: Allocator, + io: Io, args: []const []const u8, ) !void { dev.check(.fetch_command); @@ -6884,7 +6881,7 @@ fn cmdFetch( try thread_pool.init(.{ .allocator = gpa }); defer thread_pool.deinit(); - var http_client: std.http.Client = .{ .allocator = gpa }; + var http_client: std.http.Client = .{ .allocator = gpa, .io = io }; defer http_client.deinit(); try http_client.initDefaultProxies(arena); diff --git a/src/print_env.zig b/src/print_env.zig index e1b2b1eb83..e1847688ad 100644 --- a/src/print_env.zig +++ b/src/print_env.zig @@ -14,6 +14,7 @@ pub fn cmdEnv( .wasi => std.fs.wasi.Preopens, else => void, }, + host: *const std.Target, ) !void { const override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena); const override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena); @@ -38,8 +39,6 @@ pub fn cmdEnv( const zig_lib_dir = dirs.zig_lib.path orelse ""; const zig_std_dir = try dirs.zig_lib.join(arena, &.{"std"}); const global_cache_dir = dirs.global_cache.path orelse ""; - - const host = try std.zig.system.resolveTargetQuery(.{}); const triple = try host.zigTriple(arena); var serializer: std.zon.Serializer = .{ .writer = out }; From 71ff6e0ef748fbc78cd469317f546c99c0dc5074 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 00:00:05 -0700 Subject: [PATCH 093/244] std: fix seekBy unit test --- lib/std/Io.zig | 4 +-- lib/std/Io/File.zig | 57 ++++++++++++++++++++++++----- lib/std/Io/Threaded.zig | 66 ++++++++++++++++++++++++++++++---- lib/std/debug/ElfFile.zig | 3 +- lib/std/debug/SelfInfo/Elf.zig | 1 + lib/std/dynamic_library.zig | 1 + lib/std/posix.zig | 2 +- 7 files changed, 114 insertions(+), 20 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 8f8e9a122d..677d197422 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -666,8 +666,8 @@ pub const VTable = struct { fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.ReadStreamingError!usize, /// Returns 0 on end of stream. fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, - fileSeekBy: *const fn (?*anyopaque, File, offset: i64) File.SeekError!void, - fileSeekTo: *const fn (?*anyopaque, File, offset: u64) File.SeekError!void, + fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void, + fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index d5d08587c7..43ce6108ed 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -71,6 +71,8 @@ pub const StatError = error{ /// not hold the required rights to get its filestat information. AccessDenied, PermissionDenied, + /// Attempted to stat a non-file stream. + Streaming, } || Io.Cancelable || Io.UnexpectedError; /// Returns `Stat` containing basic information about the `File`. @@ -357,10 +359,6 @@ pub const Reader = struct { setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); }, .streaming, .streaming_reading => { - if (std.posix.SEEK == void) { - r.seek_err = error.Unseekable; - return error.Unseekable; - } const seek_err = r.seek_err orelse e: { if (io.vtable.fileSeekBy(io.userdata, r.file, offset)) |_| { setLogicalPos(r, @intCast(@as(i64, @intCast(logicalPos(r))) + offset)); @@ -384,6 +382,7 @@ pub const Reader = struct { } } + /// Repositions logical read offset relative to the beginning of the file. pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void { const io = r.io; switch (r.mode) { @@ -527,25 +526,65 @@ pub const Reader = struct { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); const io = r.io; const file = r.file; - const pos = r.pos; switch (r.mode) { .positional, .positional_reading => { const size = r.getSize() catch { r.mode = r.mode.toStreaming(); return 0; }; - const delta = @min(@intFromEnum(limit), size - pos); - r.pos = pos + delta; + const logical_pos = logicalPos(r); + const delta = @min(@intFromEnum(limit), size - logical_pos); + setLogicalPos(r, logical_pos + delta); return delta; }, .streaming, .streaming_reading => { + // Unfortunately we can't seek forward without knowing the + // size because the seek syscalls provided to us will not + // return the true end position if a seek would exceed the + // end. + fallback: { + if (r.size_err == null and r.seek_err == null) break :fallback; + + const buffered_len = r.interface.bufferedLen(); + var remaining = @intFromEnum(limit); + if (remaining <= buffered_len) { + r.interface.seek += remaining; + return remaining; + } + remaining -= buffered_len; + r.interface.seek = 0; + r.interface.end = 0; + + var trash_buffer: [128]u8 = undefined; + var data: [1][]u8 = .{trash_buffer[0..@min(trash_buffer.len, remaining)]}; + var iovecs_buffer: [max_buffers_len][]u8 = undefined; + const dest_n, const data_size = try r.interface.writableVector(&iovecs_buffer, &data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.fileReadStreaming(io.userdata, file, dest) catch |err| { + r.err = err; + return error.ReadFailed; + }; + if (n == 0) { + r.size = r.pos; + return error.EndOfStream; + } + r.pos += n; + if (n > data_size) { + r.interface.end += n - data_size; + remaining -= data_size; + } else { + remaining -= n; + } + return @intFromEnum(limit) - remaining; + } const size = r.getSize() catch return 0; - const n = @min(size - pos, std.math.maxInt(i64), @intFromEnum(limit)); + const n = @min(size - r.pos, std.math.maxInt(i64), @intFromEnum(limit)); io.vtable.fileSeekBy(io.userdata, file, n) catch |err| { r.seek_err = err; return 0; }; - r.pos = pos + n; + r.pos += n; return n; }, .failure => return error.ReadFailed, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 88120f4b04..345d9728e3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -869,7 +869,6 @@ fn dirStatPathPosix( const sub_path_posix = try pathToPosix(sub_path, &path_buffer); const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; - const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; while (true) { try pool.checkCancel(); @@ -895,7 +894,9 @@ fn dirStatPathPosix( fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { const pool: *Pool = @ptrCast(@alignCast(userdata)); - const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; + + if (posix.Stat == void) return error.Streaming; + while (true) { try pool.checkCancel(); var stat = std.mem.zeroes(posix.Stat); @@ -969,6 +970,10 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. const have_flock = @TypeOf(posix.system.flock) != void; const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; +const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; +const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; +const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; +const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; fn dirCreateFilePosix( userdata: ?*anyopaque, @@ -1423,7 +1428,6 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset } }; - const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; while (true) { try pool.checkCancel(); const rc = preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset)); @@ -1461,11 +1465,59 @@ fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekErr fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const fd = file.handle; - _ = file; - _ = offset; - @panic("TODO"); + if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) while (true) { + try pool.checkCancel(); + var result: u64 = undefined; + switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.SET))) { + .SUCCESS => return, + .INTR => continue, + .BADF => |err| return errnoBug(err), // Always a race condition. + .INVAL => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .SPIPE => return error.Unseekable, + .NXIO => return error.Unseekable, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (native_os == .windows) { + try pool.checkCancel(); + return windows.SetFilePointerEx_BEGIN(fd, offset); + } + + if (native_os == .wasi and !builtin.link_libc) while (true) { + try pool.checkCancel(); + var new_offset: std.os.wasi.filesize_t = undefined; + switch (std.os.wasi.fd_seek(fd, @bitCast(offset), .SET, &new_offset)) { + .SUCCESS => return, + .INTR => continue, + .BADF => |err| return errnoBug(err), // Always a race condition. + .INVAL => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .SPIPE => return error.Unseekable, + .NXIO => return error.Unseekable, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (posix.SEEK == void) return error.Unseekable; + + while (true) { + try pool.checkCancel(); + switch (posix.errno(lseek_sym(fd, @bitCast(offset), posix.SEEK.SET))) { + .SUCCESS => return, + .INTR => continue, + .BADF => |err| return errnoBug(err), // Always a race condition. + .INVAL => return error.Unseekable, + .OVERFLOW => return error.Unseekable, + .SPIPE => return error.Unseekable, + .NXIO => return error.Unseekable, + else => |err| return posix.unexpectedErrno(err), + } + } } fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posix.off_t) Io.File.PWriteError!usize { diff --git a/lib/std/debug/ElfFile.zig b/lib/std/debug/ElfFile.zig index 2062772533..427a8fd1b1 100644 --- a/lib/std/debug/ElfFile.zig +++ b/lib/std/debug/ElfFile.zig @@ -108,6 +108,7 @@ pub const LoadError = error{ LockedMemoryLimitExceeded, ProcessFdQuotaExceeded, SystemFdQuotaExceeded, + Streaming, Canceled, Unexpected, }; @@ -409,7 +410,7 @@ fn loadInner( arena: Allocator, elf_file: std.fs.File, opt_crc: ?u32, -) (LoadError || error{ CrcMismatch, Canceled })!LoadInnerResult { +) (LoadError || error{ CrcMismatch, Streaming, Canceled })!LoadInnerResult { const mapped_mem: []align(std.heap.page_size_min) const u8 = mapped: { const file_len = std.math.cast( usize, diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index 035ed584b2..bf8330d235 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -354,6 +354,7 @@ const Module = struct { error.LockedMemoryLimitExceeded, error.ProcessFdQuotaExceeded, error.SystemFdQuotaExceeded, + error.Streaming, => return error.ReadFailed, }; errdefer elf_file.deinit(gpa); diff --git a/lib/std/dynamic_library.zig b/lib/std/dynamic_library.zig index a7326cfd66..90f655b95b 100644 --- a/lib/std/dynamic_library.zig +++ b/lib/std/dynamic_library.zig @@ -138,6 +138,7 @@ const ElfDynLibError = error{ ElfSymSectionNotFound, ElfHashTableNotFound, Canceled, + Streaming, } || posix.OpenError || posix.MMapError; pub const ElfDynLib = struct { diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 50d63ad63d..eebff5479d 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -488,6 +488,7 @@ fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtEr error.NameTooLong => unreachable, error.FileNotFound => unreachable, error.InvalidUtf8 => unreachable, + error.Streaming => unreachable, error.Canceled => return error.Canceled, else => |e| return e, }; @@ -5262,7 +5263,6 @@ pub fn gettimeofday(tv: ?*timeval, tz: ?*timezone) void { pub const SeekError = std.Io.File.SeekError; -/// Repositions read/write file offset relative to the beginning. pub fn lseek_SET(fd: fd_t, offset: u64) SeekError!void { if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) { var result: u64 = undefined; From be1ae430a1d6dff12c6cd7199dd6504fda880caf Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 15:53:19 -0700 Subject: [PATCH 094/244] std.Io.Threaded.netReadPosix: support cancelation --- lib/std/Io/Threaded.zig | 57 ++++++++++++++++++++++++++++++++++++----- lib/std/Io/net.zig | 17 +++++++----- lib/std/posix.zig | 48 +++++++++++++++++----------------- 3 files changed, 86 insertions(+), 36 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 345d9728e3..fa3c63d609 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1988,7 +1988,7 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: Io.net.Socket.Handle) Io.net fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.net.Stream.Reader.Error!usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const fd = stream.socket.handle; var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; @@ -2001,9 +2001,54 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n } const dest = iovecs_buffer[0..i]; assert(dest[0].len > 0); - const n = try posix.readv(stream.socket.handle, dest); - if (n == 0) return error.EndOfStream; - return n; + + if (native_os == .wasi and !builtin.link_libc) while (true) { + try pool.checkCancel(); + var n: usize = undefined; + switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) { + .SUCCESS => { + if (n == 0) return error.EndOfStream; + return n; + }, + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.ConnectionTimedOut, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return posix.unexpectedErrno(err), + } + }; + + while (true) { + try pool.checkCancel(); + const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); + switch (posix.errno(rc)) { + .SUCCESS => { + const n: usize = @intCast(rc); + if (n == 0) return error.EndOfStream; + return n; + }, + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.ConnectionTimedOut, + .PIPE => return error.BrokenPipe, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } } const have_sendmmsg = builtin.os.tag == .linux; @@ -2071,7 +2116,7 @@ fn netSendOne( .WSAEHOSTUNREACH => return error.NetworkUnreachable, // TODO: WSAEINPROGRESS, WSAEINTR .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENETRESET => return error.ConnectionResetByPeer, .WSAENETUNREACH => return error.NetworkUnreachable, .WSAENOTCONN => return error.SocketUnconnected, @@ -2114,7 +2159,7 @@ fn netSendOne( .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTCONN => return error.SocketUnconnected, - .NETDOWN => return error.NetworkSubsystemFailed, + .NETDOWN => return error.NetworkDown, else => |err| return posix.unexpectedErrno(err), } } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index fe2f7e9973..d76a7a9b34 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1087,13 +1087,18 @@ pub const Stream = struct { stream: Stream, err: ?Error, - pub const Error = std.posix.ReadError || error{ - SocketNotBound, - MessageTooBig, - NetworkSubsystemFailed, + pub const Error = error{ + SystemResources, + BrokenPipe, ConnectionResetByPeer, + ConnectionTimedOut, SocketUnconnected, - } || Io.Cancelable || Io.Writer.Error || error{EndOfStream}; + /// The file descriptor does not hold the required rights to read + /// from it. + AccessDenied, + NetworkDown, + EndOfStream, + } || Io.Cancelable || Io.UnexpectedError; pub fn init(stream: Stream, io: Io, buffer: []u8) Reader { return .{ @@ -1140,7 +1145,7 @@ pub const Stream = struct { ConnectionResetByPeer, SocketNotBound, MessageTooBig, - NetworkSubsystemFailed, + NetworkDown, SystemResources, SocketUnconnected, Unexpected, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index eebff5479d..47ca1e080c 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3613,7 +3613,7 @@ pub const ShutdownError = error{ BlockingOperationInProgress, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The socket is not connected (connection-oriented sockets only). SocketUnconnected, @@ -3635,7 +3635,7 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { .WSAECONNRESET => return error.ConnectionResetByPeer, .WSAEINPROGRESS => return error.BlockingOperationInProgress, .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENOTCONN => return error.SocketUnconnected, .WSAENOTSOCK => unreachable, .WSANOTINITIALISED => unreachable, @@ -3697,7 +3697,7 @@ pub const BindError = error{ ReadOnlyFileSystem, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, FileDescriptorNotASocket, @@ -3718,7 +3718,7 @@ pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!voi .WSAEFAULT => unreachable, // invalid pointers .WSAEINVAL => return error.AlreadyBound, .WSAENOBUFS => return error.SystemResources, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, else => |err| return windows.unexpectedWSAError(err), } unreachable; @@ -3763,7 +3763,7 @@ pub const ListenError = error{ OperationNotSupported, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// Ran out of system resources /// On Windows it can either run out of socket descriptors or buffer space @@ -3782,7 +3782,7 @@ pub fn listen(sock: socket_t, backlog: u31) ListenError!void { if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAEADDRINUSE => return error.AddressInUse, .WSAEISCONN => return error.AlreadyConnected, .WSAEINVAL => return error.SocketNotBound, @@ -3840,7 +3840,7 @@ pub const AcceptError = error{ ConnectionResetByPeer, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The referenced socket is not a type that supports connection-oriented service. OperationNotSupported, @@ -3893,7 +3893,7 @@ pub fn accept( .WSAENOTSOCK => return error.FileDescriptorNotASocket, .WSAEINVAL => return error.SocketNotListening, .WSAEMFILE => return error.ProcessFdQuotaExceeded, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENOBUFS => return error.FileDescriptorNotASocket, .WSAEOPNOTSUPP => return error.OperationNotSupported, .WSAEWOULDBLOCK => return error.WouldBlock, @@ -3964,7 +3964,7 @@ fn setSockFlags(sock: socket_t, flags: u32) !void { if (windows.ws2_32.ioctlsocket(sock, windows.ws2_32.FIONBIO, &mode) == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENOTSOCK => return error.FileDescriptorNotASocket, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), @@ -4105,7 +4105,7 @@ pub const GetSockNameError = error{ SystemResources, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// Socket hasn't been bound yet SocketNotBound, @@ -4119,7 +4119,7 @@ pub fn getsockname(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAEFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value .WSAENOTSOCK => return error.FileDescriptorNotASocket, .WSAEINVAL => return error.SocketNotBound, @@ -4148,7 +4148,7 @@ pub fn getpeername(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAEFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value .WSAENOTSOCK => return error.FileDescriptorNotASocket, .WSAEINVAL => return error.SocketNotBound, @@ -6057,7 +6057,7 @@ pub const SendError = error{ NetworkUnreachable, /// The local network interface used to reach the destination is down. - NetworkSubsystemFailed, + NetworkDown, /// The destination address is not listening. ConnectionRefused, @@ -6106,7 +6106,7 @@ pub fn sendmsg( .WSAEHOSTUNREACH => return error.NetworkUnreachable, // TODO: WSAEINPROGRESS, WSAEINTR .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENETRESET => return error.ConnectionResetByPeer, .WSAENETUNREACH => return error.NetworkUnreachable, .WSAENOTCONN => return error.SocketUnconnected, @@ -6146,7 +6146,7 @@ pub fn sendmsg( .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTCONN => return error.SocketUnconnected, - .NETDOWN => return error.NetworkSubsystemFailed, + .NETDOWN => return error.NetworkDown, else => |err| return unexpectedErrno(err), } } @@ -6209,7 +6209,7 @@ pub fn sendto( .WSAEHOSTUNREACH => return error.NetworkUnreachable, // TODO: WSAEINPROGRESS, WSAEINTR .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENETRESET => return error.ConnectionResetByPeer, .WSAENETUNREACH => return error.NetworkUnreachable, .WSAENOTCONN => return error.SocketUnconnected, @@ -6251,7 +6251,7 @@ pub fn sendto( .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTCONN => return error.SocketUnconnected, - .NETDOWN => return error.NetworkSubsystemFailed, + .NETDOWN => return error.NetworkDown, else => |err| return unexpectedErrno(err), } } @@ -6390,7 +6390,7 @@ pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len pub const PollError = error{ /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The kernel had no space to allocate file descriptor tables. SystemResources, @@ -6401,7 +6401,7 @@ pub fn poll(fds: []pollfd, timeout: i32) PollError!usize { switch (windows.poll(fds.ptr, @intCast(fds.len), timeout)) { windows.ws2_32.SOCKET_ERROR => switch (windows.ws2_32.WSAGetLastError()) { .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENOBUFS => return error.SystemResources, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), @@ -6473,7 +6473,7 @@ pub const RecvFromError = error{ MessageTooBig, /// The network subsystem has failed. - NetworkSubsystemFailed, + NetworkDown, /// The socket is not connected (connection-oriented sockets only). SocketUnconnected, @@ -6504,7 +6504,7 @@ pub fn recvfrom( .WSAECONNRESET => return error.ConnectionResetByPeer, .WSAEINVAL => return error.SocketNotBound, .WSAEMSGSIZE => return error.MessageTooBig, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAENOTCONN => return error.SocketUnconnected, .WSAEWOULDBLOCK => return error.WouldBlock, .WSAETIMEDOUT => return error.ConnectionTimedOut, @@ -6578,7 +6578,7 @@ pub fn recvmsg( .PIPE => return error.BrokenPipe, .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .CONNRESET => return error.ConnectionResetByPeer, - .NETDOWN => return error.NetworkSubsystemFailed, + .NETDOWN => return error.NetworkDown, else => |err| return unexpectedErrno(err), } } @@ -6601,7 +6601,7 @@ pub const SetSockOptError = error{ PermissionDenied, OperationNotSupported, - NetworkSubsystemFailed, + NetworkDown, FileDescriptorNotASocket, SocketNotBound, NoDevice, @@ -6614,7 +6614,7 @@ pub fn setsockopt(fd: socket_t, level: i32, optname: u32, opt: []const u8) SetSo if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkSubsystemFailed, + .WSAENETDOWN => return error.NetworkDown, .WSAEFAULT => unreachable, .WSAENOTSOCK => return error.FileDescriptorNotASocket, .WSAEINVAL => return error.SocketNotBound, From 4d62f0839382a9aa611f9c898a1ef7a6050bd92a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 17:33:04 -0700 Subject: [PATCH 095/244] add BRANCH_TODO file to be deleted before merging into master --- BRANCH_TODO | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 BRANCH_TODO diff --git a/BRANCH_TODO b/BRANCH_TODO new file mode 100644 index 0000000000..21a23fd74d --- /dev/null +++ b/BRANCH_TODO @@ -0,0 +1,14 @@ +* Threaded: finish linux impl (all tests passing) +* Threaded: finish macos impl +* Threaded: finish windows impl + +* fix Group.wait not handling cancelation (need to move impl of ResetEvent to Threaded) +* implement cancelRequest for non-linux posix +* finish converting all Threaded into directly calling system functions and handling EINTR + +* move max_iovecs_len to std.Io +* address the cancelation race condition (signal received between checkCancel and syscall) +* update signal values to be an enum +* move fs.File.Writer to Io +* finish moving std.fs to Io +* finish moving all of std.posix into Threaded From e0e463bcf711f660cf8df5e8d31e82f855c0a967 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 17:34:07 -0700 Subject: [PATCH 096/244] std.Io.net.Stream.Reader: fix not using buffer --- lib/std/Io.zig | 5 +++-- lib/std/Io/Threaded.zig | 20 +++++++------------- lib/std/Io/net.zig | 20 +++++++++++++++++--- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 677d197422..8d6be76173 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -680,8 +680,9 @@ pub const VTable = struct { netConnectUnix: *const fn (?*anyopaque, net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle, netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) struct { ?net.Socket.SendError, usize }, netReceive: *const fn (?*anyopaque, net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, net.ReceiveFlags, Timeout) struct { ?net.Socket.ReceiveTimeoutError, usize }, - netRead: *const fn (?*anyopaque, src: net.Stream, data: [][]u8) net.Stream.Reader.Error!usize, - netWrite: *const fn (?*anyopaque, dest: net.Stream, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, + /// Returns 0 on end of stream. + netRead: *const fn (?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize, + netWrite: *const fn (?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index fa3c63d609..2c05ff279c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1986,9 +1986,8 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: Io.net.Socket.Handle) Io.net } }; } -fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.net.Stream.Reader.Error!usize { +fn netReadPosix(userdata: ?*anyopaque, fd: Io.net.Socket.Handle, data: [][]u8) Io.net.Stream.Reader.Error!usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); - const fd = stream.socket.handle; var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; @@ -2006,11 +2005,9 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n try pool.checkCancel(); var n: usize = undefined; switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) { - .SUCCESS => { - if (n == 0) return error.EndOfStream; - return n; - }, + .SUCCESS => return n, .INTR => continue, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), @@ -2029,12 +2026,9 @@ fn netReadPosix(userdata: ?*anyopaque, stream: Io.net.Stream, data: [][]u8) Io.n try pool.checkCancel(); const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); switch (posix.errno(rc)) { - .SUCCESS => { - const n: usize = @intCast(rc); - if (n == 0) return error.EndOfStream; - return n; - }, + .SUCCESS => return @intCast(rc), .INTR => continue, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), @@ -2359,7 +2353,7 @@ fn netReceive( fn netWritePosix( userdata: ?*anyopaque, - stream: Io.net.Stream, + fd: Io.net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize, @@ -2406,7 +2400,7 @@ fn netWritePosix( }, }; const flags = posix.MSG.NOSIGNAL; - return posix.sendmsg(stream.socket.handle, &msg, flags); + return posix.sendmsg(fd, &msg, flags); } fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index d76a7a9b34..dca4dda0a5 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1076,6 +1076,8 @@ pub const Socket = struct { pub const Stream = struct { socket: Socket, + const max_iovecs_len = 8; + pub fn close(s: *Stream, io: Io) void { io.vtable.netClose(io.userdata, s.socket.handle); s.* = undefined; @@ -1097,7 +1099,6 @@ pub const Stream = struct { /// from it. AccessDenied, NetworkDown, - EndOfStream, } || Io.Cancelable || Io.UnexpectedError; pub fn init(stream: Stream, io: Io, buffer: []u8) Reader { @@ -1128,10 +1129,22 @@ pub const Stream = struct { fn readVec(io_r: *Io.Reader, data: [][]u8) Io.Reader.Error!usize { const r: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); const io = r.io; - return io.vtable.netRead(io.userdata, r.stream, data) catch |err| { + var iovecs_buffer: [max_iovecs_len][]u8 = undefined; + const dest_n, const data_size = try io_r.writableVector(&iovecs_buffer, data); + const dest = iovecs_buffer[0..dest_n]; + assert(dest[0].len > 0); + const n = io.vtable.netRead(io.userdata, r.stream.socket.handle, dest) catch |err| { r.err = err; return error.ReadFailed; }; + if (n == 0) { + return error.EndOfStream; + } + if (n > data_size) { + r.interface.end += n - data_size; + return data_size; + } + return n; } }; @@ -1166,7 +1179,8 @@ pub const Stream = struct { const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); const io = w.io; const buffered = io_w.buffered(); - const n = io.vtable.netWrite(io.userdata, w.stream, buffered, data, splat) catch |err| { + const handle = w.stream.socket.handle; + const n = io.vtable.netWrite(io.userdata, handle, buffered, data, splat) catch |err| { w.err = err; return error.WriteFailed; }; From 923a7bdd7e390c5dbb0fcf2686998180ba41cc88 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 17:58:25 -0700 Subject: [PATCH 097/244] std.Io.net: fix parsing IPv4-mapped IPv6 addresses --- lib/std/Io/net.zig | 9 +++++- lib/std/Io/net/test.zig | 64 +++++++++++++---------------------------- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index dca4dda0a5..241a97467a 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -438,6 +438,13 @@ pub const Ip6Address = struct { pub fn parse(text: []const u8) Parsed { if (text.len < 2) return .unexpected_end; + if (std.ascii.startsWithIgnoreCase(text, "::ffff:")) ip4_mapped: { + const a4 = (Ip4Address.parse(text["::ffff:".len..], 0) catch break :ip4_mapped).bytes; + return .{ .success = .{ + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, a4[0], a4[1], a4[2], a4[3] }, + .interface_name = null, + } }; + } // Has to be u16 elements to handle 3-digit hex numbers from compression. var parts: [8]u16 = @splat(0); var parts_i: u8 = 0; @@ -623,7 +630,7 @@ pub const Ip6Address = struct { /// This is a pure function but it cannot handle IPv6 addresses that have /// scope ids ("%foo" at the end). To also handle those, `resolve` must be - /// called instead. + /// called instead, or the lower level `Unresolved` API may be used. pub fn parse(buffer: []const u8, port: u16) ParseError!Ip6Address { switch (Unresolved.parse(buffer)) { .success => |p| return .{ diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 2663227e19..cc95237efb 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -46,46 +46,26 @@ test "parse IPv6 address, check raw bytes" { } test "parse and render IPv6 addresses" { - // TODO make this test parsing and rendering only, then it doesn't need I/O - const io = testing.io; + try testParseAndRenderIp6Address("FF01:0:0:0:0:0:0:FB", "ff01::fb"); + try testParseAndRenderIp6Address("FF01::Fb", "ff01::fb"); + try testParseAndRenderIp6Address("::1", "::1"); + try testParseAndRenderIp6Address("::", "::"); + try testParseAndRenderIp6Address("1::", "1::"); + try testParseAndRenderIp6Address("2001:db8::", "2001:db8::"); + try testParseAndRenderIp6Address("::1234:5678", "::1234:5678"); + try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678"); + try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234"); + try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5"); +} +fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) !void { var buffer: [100]u8 = undefined; - const ips = [_][]const u8{ - "FF01:0:0:0:0:0:0:FB", - "FF01::Fb", - "::1", - "::", - "1::", - "2001:db8::", - "::1234:5678", - "2001:db8::1234:5678", - "FF01::FB%1234", - "::ffff:123.5.123.5", - }; - const printed = [_][]const u8{ - "ff01::fb", - "ff01::fb", - "::1", - "::", - "1::", - "2001:db8::", - "::1234:5678", - "2001:db8::1234:5678", - "ff01::fb%1234", - "::ffff:123.5.123.5", - }; - for (ips, 0..) |ip, i| { - const addr = net.IpAddress.parseIp6(ip, 0) catch unreachable; - var newIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr}) catch unreachable; - try testing.expect(std.mem.eql(u8, printed[i], newIp[1 .. newIp.len - 3])); - - if (builtin.os.tag == .linux) { - const addr_via_resolve = net.IpAddress.resolveIp6(io, ip, 0) catch unreachable; - var newResolvedIp = std.fmt.bufPrint(buffer[0..], "{f}", .{addr_via_resolve}) catch unreachable; - try testing.expect(std.mem.eql(u8, printed[i], newResolvedIp[1 .. newResolvedIp.len - 3])); - } - } + const parsed = net.Ip6Address.Unresolved.parse(input); + const actual_printed = try std.fmt.bufPrint(&buffer, "{f}", .{parsed.success}); + try testing.expectEqualStrings(expected_output, actual_printed); +} +test "IPv6 address parse failures" { try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6(":::", 0)); try testing.expectError(error.Overflow, net.IpAddress.parseIp6("FF001::FB", 0)); try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6("FF01::Fb:zig", 0)); @@ -93,13 +73,9 @@ test "parse and render IPv6 addresses" { try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("FF01:", 0)); try testing.expectError(error.InvalidIpv4Mapping, net.IpAddress.parseIp6("::123.123.123.123", 0)); try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("1", 0)); - // TODO Make this test pass on other operating systems. - if (builtin.os.tag == .linux or comptime builtin.os.tag.isDarwin() or builtin.os.tag == .windows) { - try testing.expectError(error.Incomplete, net.IpAddress.resolveIp6(io, "ff01::fb%", 0)); - // Assumes IFNAMESIZE will always be a multiple of 2 - try testing.expectError(error.Overflow, net.IpAddress.resolveIp6(io, "ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); - try testing.expectError(error.Overflow, net.IpAddress.resolveIp6(io, "ff01::fb%12345678901234", 0)); - } + try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("ff01::fb%", 0)); + try testing.expectError(error.Overflow, net.IpAddress.parseIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); + try testing.expectError(error.Overflow, net.IpAddress.parseIp6("ff01::fb%12345678901234", 0)); } test "invalid but parseable IPv6 scope ids" { From d3f0c460ecb0ca5b1ba9c57f443463e1f3f49012 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 18:06:44 -0700 Subject: [PATCH 098/244] std.Io.net.HostName: fix DNS resolution * merge conflict with changing behavior of takeDelimiterExclusive * check bounds before adding to result array --- lib/std/Io/net/HostName.zig | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 94c87ab0a4..4d0df744c4 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -396,20 +396,24 @@ fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, optio std.posix.RR.A => { const data = record.packet[record.data_off..][0..record.data_len]; if (data.len != 4) return error.InvalidDnsARecord; - options.addresses_buffer[addresses_len] = .{ .ip4 = .{ - .bytes = data[0..4].*, - .port = options.port, - } }; - addresses_len += 1; + if (addresses_len < options.addresses_buffer.len) { + options.addresses_buffer[addresses_len] = .{ .ip4 = .{ + .bytes = data[0..4].*, + .port = options.port, + } }; + addresses_len += 1; + } }, std.posix.RR.AAAA => { const data = record.packet[record.data_off..][0..record.data_len]; if (data.len != 16) return error.InvalidDnsAAAARecord; - options.addresses_buffer[addresses_len] = .{ .ip6 = .{ - .bytes = data[0..16].*, - .port = options.port, - } }; - addresses_len += 1; + if (addresses_len < options.addresses_buffer.len) { + options.addresses_buffer[addresses_len] = .{ .ip6 = .{ + .bytes = data[0..16].*, + .port = options.port, + } }; + addresses_len += 1; + } }, std.posix.RR.CNAME => { _, canonical_name = expand(record.packet, record.data_off, options.canonical_name_buffer) catch @@ -472,6 +476,7 @@ fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Re error.ReadFailed => return error.ReadFailed, error.EndOfStream => break, }; + reader.toss(1); var split_it = std.mem.splitScalar(u8, line, '#'); const no_comment_line = split_it.first(); From 539808239ecae050e383dd11f395a363b4404e21 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 18:59:40 -0700 Subject: [PATCH 099/244] std.net: IPv6 parsing fixes --- lib/std/Io/net.zig | 32 +++++++++++++++++++++----------- lib/std/Io/net/test.zig | 24 ++++++++++++++---------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 241a97467a..4c0c9405a7 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -70,7 +70,7 @@ pub const IpAddress = union(enum) { pub fn parseLiteral(text: []const u8) ParseLiteralError!IpAddress { if (text.len == 0) return error.InvalidAddress; if (text[0] == '[') { - const addr_end = std.mem.indexOfScalar(u8, text, ']') orelse + const addr_end = std.mem.findScalar(u8, text, ']') orelse return error.InvalidAddress; const addr_text = text[1..addr_end]; const port: u16 = p: { @@ -80,7 +80,7 @@ pub const IpAddress = union(enum) { }; return parseIp6(addr_text, port) catch error.InvalidAddress; } - if (std.mem.indexOfScalar(u8, text, ':')) |i| { + if (std.mem.findScalar(u8, text, ':')) |i| { const addr = Ip4Address.parse(text[0..i], 0) catch return error.InvalidAddress; return .{ .ip4 = .{ .bytes = addr.bytes, @@ -431,17 +431,22 @@ pub const Ip6Address = struct { pub const Parsed = union(enum) { success: Unresolved, invalid_byte: usize, - unexpected_end, + incomplete, junk_after_end: usize, interface_name_oversized: usize, + invalid_ip4_mapping: usize, + overflow: usize, }; pub fn parse(text: []const u8) Parsed { - if (text.len < 2) return .unexpected_end; - if (std.ascii.startsWithIgnoreCase(text, "::ffff:")) ip4_mapped: { - const a4 = (Ip4Address.parse(text["::ffff:".len..], 0) catch break :ip4_mapped).bytes; + if (text.len < 2) return .incomplete; + const ip4_prefix = "::ffff:"; + if (std.ascii.startsWithIgnoreCase(text, ip4_prefix)) { + const parsed = Ip4Address.parse(text[ip4_prefix.len..], 0) catch + return .{ .invalid_ip4_mapping = ip4_prefix.len }; + const b = parsed.bytes; return .{ .success = .{ - .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, a4[0], a4[1], a4[2], a4[3] }, + .bytes = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, b[0], b[1], b[2], b[3] }, .interface_name = null, } }; } @@ -457,7 +462,9 @@ pub const Ip6Address = struct { .digit => c: switch (text[text_i]) { 'a'...'f' => |c| { const digit = c - 'a' + 10; - parts[parts_i] = parts[parts_i] * 16 + digit; + parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{ + .overflow = text_i, + }) + digit; if (digit_i == 4) return .{ .invalid_byte = text_i }; digit_i += 1; text_i += 1; @@ -470,7 +477,9 @@ pub const Ip6Address = struct { 'A'...'F' => |c| continue :c c - 'A' + 'a', '0'...'9' => |c| { const digit = c - '0'; - parts[parts_i] = parts[parts_i] * 16 + digit; + parts[parts_i] = (std.math.mul(u16, parts[parts_i], 16) catch return .{ + .overflow = text_i, + }) + digit; if (digit_i == 4) return .{ .invalid_byte = text_i }; digit_i += 1; text_i += 1; @@ -497,7 +506,7 @@ pub const Ip6Address = struct { if (parts.len - parts_i == 0) continue :state .end; digit_i = 0; text_i += 1; - if (text.len - text_i == 0) return .unexpected_end; + if (text.len - text_i == 0) return .incomplete; continue :c text[text_i]; } }, @@ -507,6 +516,7 @@ pub const Ip6Address = struct { text_i += 1; const name = text[text_i..]; if (name.len > Interface.Name.max_len) return .{ .interface_name_oversized = text_i }; + if (name.len == 0) return .incomplete; interface_name_text = name; text_i = @intCast(text.len); continue :state .end; @@ -521,7 +531,7 @@ pub const Ip6Address = struct { @memmove(parts[parts.len - src.len ..], src); @memset(parts[s..][0..remaining], 0); } else { - if (remaining != 0) return .unexpected_end; + if (remaining != 0) return .incomplete; } // Workaround that can be removed when this proposal is diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index cc95237efb..9bd4618429 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -56,6 +56,7 @@ test "parse and render IPv6 addresses" { try testParseAndRenderIp6Address("2001:db8::1234:5678", "2001:db8::1234:5678"); try testParseAndRenderIp6Address("FF01::FB%1234", "ff01::fb%1234"); try testParseAndRenderIp6Address("::ffff:123.5.123.5", "::ffff:123.5.123.5"); + try testParseAndRenderIp6Address("ff01::fb%12345678901234", "ff01::fb%12345678901234"); } fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) !void { @@ -66,16 +67,19 @@ fn testParseAndRenderIp6Address(input: []const u8, expected_output: []const u8) } test "IPv6 address parse failures" { - try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6(":::", 0)); - try testing.expectError(error.Overflow, net.IpAddress.parseIp6("FF001::FB", 0)); - try testing.expectError(error.InvalidCharacter, net.IpAddress.parseIp6("FF01::Fb:zig", 0)); - try testing.expectError(error.InvalidEnd, net.IpAddress.parseIp6("FF01:0:0:0:0:0:0:FB:", 0)); - try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("FF01:", 0)); - try testing.expectError(error.InvalidIpv4Mapping, net.IpAddress.parseIp6("::123.123.123.123", 0)); - try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("1", 0)); - try testing.expectError(error.Incomplete, net.IpAddress.parseIp6("ff01::fb%", 0)); - try testing.expectError(error.Overflow, net.IpAddress.parseIp6("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2), 0)); - try testing.expectError(error.Overflow, net.IpAddress.parseIp6("ff01::fb%12345678901234", 0)); + try testing.expectError(error.ParseFailed, net.IpAddress.parseIp6(":::", 0)); + + const Unresolved = net.Ip6Address.Unresolved; + + try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 2 }, Unresolved.parse(":::")); + try testing.expectEqual(Unresolved.Parsed{ .overflow = 4 }, Unresolved.parse("FF001::FB")); + try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 9 }, Unresolved.parse("FF01::Fb:zig")); + try testing.expectEqual(Unresolved.Parsed{ .junk_after_end = 19 }, Unresolved.parse("FF01:0:0:0:0:0:0:FB:")); + try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("FF01:")); + try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123")); + try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1")); + try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%")); + try testing.expectEqual(Unresolved.Parsed{ .interface_name_oversized = 9 }, Unresolved.parse("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2))); } test "invalid but parseable IPv6 scope ids" { From d680b9e9b2320612a4e988763f5090c4ae8f9a8f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 19:04:02 -0700 Subject: [PATCH 100/244] std.Io.File: add WouldBlock to the error set Even in an asynchronous world, the concept of a non-blocking flag is useful because it determines under what conditions the operation completes. --- lib/std/Io/File.zig | 2 ++ lib/std/Io/Threaded.zig | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 43ce6108ed..c2ceaa95b7 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -139,6 +139,8 @@ pub const OpenError = error{ /// kernel (e.g., for module/firmware loading), and write access was /// requested. FileBusy, + /// Non-blocking was requested and the operation cannot return immediately. + WouldBlock, } || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub fn close(file: File, io: Io) void { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 2c05ff279c..a200632764 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1039,7 +1039,7 @@ fn dirCreateFilePosix( .EXIST => return error.PathAlreadyExists, .BUSY => return error.DeviceBusy, .OPNOTSUPP => return error.FileLocksNotSupported, - //.AGAIN => return error.WouldBlock, + .AGAIN => return error.WouldBlock, .TXTBSY => return error.FileBusy, .NXIO => return error.NoDevice, .ILSEQ => return error.BadPathName, @@ -1064,7 +1064,7 @@ fn dirCreateFilePosix( .BADF => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), // invalid parameters .NOLCK => return error.SystemResources, - //.AGAIN => return error.WouldBlock, + .AGAIN => return error.WouldBlock, .OPNOTSUPP => return error.FileLocksNotSupported, else => |err| return posix.unexpectedErrno(err), } @@ -1168,7 +1168,7 @@ fn dirOpenFile( .EXIST => return error.PathAlreadyExists, .BUSY => return error.DeviceBusy, .OPNOTSUPP => return error.FileLocksNotSupported, - //.AGAIN => return error.WouldBlock, + .AGAIN => return error.WouldBlock, .TXTBSY => return error.FileBusy, .NXIO => return error.NoDevice, .ILSEQ => return error.BadPathName, @@ -1193,7 +1193,7 @@ fn dirOpenFile( .BADF => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), // invalid parameters .NOLCK => return error.SystemResources, - //.AGAIN => return error.WouldBlock, + .AGAIN => return error.WouldBlock, .OPNOTSUPP => return error.FileLocksNotSupported, else => |err| return posix.unexpectedErrno(err), } From e8cea8accb7f289fd00ee82063df32882748ac48 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 20:17:51 -0700 Subject: [PATCH 101/244] std.Io.Threaded: implement netListenUnix --- BRANCH_TODO | 1 + lib/std/Io.zig | 6 +-- lib/std/Io/Threaded.zig | 98 ++++++++++++++++++++++++++++++++++++++--- lib/std/Io/net.zig | 40 ++++++++++++++--- lib/std/Io/net/test.zig | 2 +- 5 files changed, 130 insertions(+), 17 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index 21a23fd74d..afb955968d 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -10,5 +10,6 @@ * address the cancelation race condition (signal received between checkCancel and syscall) * update signal values to be an enum * move fs.File.Writer to Io +* add non-blocking flag to network operations, handle EAGAIN * finish moving std.fs to Io * finish moving all of std.posix into Threaded diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 8d6be76173..749e2b3ae8 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -672,12 +672,12 @@ pub const VTable = struct { now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, - netListenIp: *const fn (?*anyopaque, address: net.IpAddress, options: net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, + netListenIp: *const fn (?*anyopaque, address: net.IpAddress, net.IpAddress.ListenOptions) net.IpAddress.ListenError!net.Server, netAccept: *const fn (?*anyopaque, server: net.Socket.Handle) net.Server.AcceptError!net.Stream, netBindIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket, netConnectIp: *const fn (?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream, - netListenUnix: *const fn (?*anyopaque, net.UnixAddress) net.UnixAddress.ListenError!net.Socket.Handle, - netConnectUnix: *const fn (?*anyopaque, net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle, + netListenUnix: *const fn (?*anyopaque, *const net.UnixAddress, net.UnixAddress.ListenOptions) net.UnixAddress.ListenError!net.Socket.Handle, + netConnectUnix: *const fn (?*anyopaque, *const net.UnixAddress) net.UnixAddress.ConnectError!net.Socket.Handle, netSend: *const fn (?*anyopaque, net.Socket.Handle, []net.OutgoingMessage, net.SendFlags) struct { ?net.Socket.SendError, usize }, netReceive: *const fn (?*anyopaque, net.Socket.Handle, message_buffer: []net.IncomingMessage, data_buffer: []u8, net.ReceiveFlags, Timeout) struct { ?net.Socket.ReceiveTimeoutError, usize }, /// Returns 0 on end of stream. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a200632764..6051c99555 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1756,11 +1756,80 @@ fn netListenIpPosix( }; } -fn netListenUnix(userdata: ?*anyopaque, address: Io.net.UnixAddress) Io.net.UnixAddress.ListenError!Io.net.Socket.Handle { +fn netListenUnix( + userdata: ?*anyopaque, + address: *const Io.net.UnixAddress, + options: Io.net.UnixAddress.ListenOptions, +) Io.net.UnixAddress.ListenError!Io.net.Socket.Handle { + if (!Io.net.has_unix_sockets) return error.AddressFamilyUnsupported; const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; - _ = address; - @panic("TODO"); + const protocol: u32 = 0; + const socket_fd = while (true) { + try pool.checkCancel(); + const flags: u32 = posix.SOCK.STREAM | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; + const socket_rc = posix.system.socket(posix.AF.UNIX, flags, protocol); + switch (posix.errno(socket_rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(socket_rc); + errdefer posix.close(fd); + if (socket_flags_unsupported) while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + break fd; + }, + .INTR => continue, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(socket_fd); + + var storage: UnixAddress = undefined; + const addr_len = addressUnixToPosix(address, &storage); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.bind(socket_fd, &storage.any, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .ADDRINUSE => return error.AddressInUse, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .NOMEM => return error.SystemResources, + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .BADF => |err| return errnoBug(err), // always a race condition if this error is returned + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NAMETOOLONG => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } + + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { + .SUCCESS => break, + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } + + return socket_fd; } fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { @@ -1791,7 +1860,7 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka .ADDRINUSE => return error.AddressInUse, .ADDRNOTAVAIL => return error.AddressUnavailable, .AFNOSUPPORT => return error.AddressFamilyUnsupported, - .AGAIN, .INPROGRESS => |err| return errnoBug(err), + .AGAIN, .INPROGRESS => return error.WouldBlock, .ALREADY => return error.ConnectionPending, .BADF => |err| return errnoBug(err), .CONNREFUSED => return error.ConnectionRefused, @@ -1804,8 +1873,8 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka .PROTOTYPE => |err| return errnoBug(err), .TIMEDOUT => return error.ConnectionTimedOut, .CONNABORTED => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, // UNIX socket error codes: - .ACCES => |err| return errnoBug(err), .PERM => |err| return errnoBug(err), .NOENT => |err| return errnoBug(err), else => |err| return posix.unexpectedErrno(err), @@ -1867,7 +1936,10 @@ fn netConnectIpPosix( } }; } -fn netConnectUnix(userdata: ?*anyopaque, address: Io.net.UnixAddress) Io.net.UnixAddress.ConnectError!Io.net.Socket.Handle { +fn netConnectUnix( + userdata: ?*anyopaque, + address: *const Io.net.UnixAddress, +) Io.net.UnixAddress.ConnectError!Io.net.Socket.Handle { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; _ = address; @@ -2503,6 +2575,11 @@ const PosixAddress = extern union { in6: posix.sockaddr.in6, }; +const UnixAddress = extern union { + any: posix.sockaddr, + un: posix.sockaddr.un, +}; + fn posixAddressFamily(a: *const Io.net.IpAddress) posix.sa_family_t { return switch (a.*) { .ip4 => posix.AF.INET, @@ -2531,6 +2608,13 @@ fn addressToPosix(a: *const Io.net.IpAddress, storage: *PosixAddress) posix.sock }; } +fn addressUnixToPosix(a: *const Io.net.UnixAddress, storage: *UnixAddress) posix.socklen_t { + @memcpy(storage.un.path[0..a.path.len], a.path); + storage.un.family = posix.AF.UNIX; + storage.un.path[a.path.len] = 0; + return @sizeOf(posix.sockaddr.un); +} + fn address4FromPosix(in: *posix.sockaddr.in) Io.net.Ip4Address { return .{ .port = std.mem.bigToNative(u16, in.port), diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 4c0c9405a7..c555a1d502 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -51,6 +51,8 @@ pub const has_unix_sockets = switch (native_os) { else => true, }; +pub const default_kernel_backlog = 128; + pub const IpAddress = union(enum) { ip4: Ip4Address, ip6: Ip6Address, @@ -210,7 +212,7 @@ pub const IpAddress = union(enum) { /// How many connections the kernel will accept on the application's behalf. /// If more than this many connections pool in the kernel, clients will start /// seeing "Connection refused". - kernel_backlog: u31 = 128, + kernel_backlog: u31 = default_kernel_backlog, /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. reuse_address: bool = false, @@ -288,6 +290,11 @@ pub const IpAddress = union(enum) { ProtocolUnsupportedBySystem, ProtocolUnsupportedByAddressFamily, SocketModeUnsupported, + /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled or + /// the connection request failed because of a local firewall rule. + AccessDenied, + /// Non-blocking was requested and the operation cannot return immediately. + WouldBlock, } || Io.Timeout.Error || Io.UnexpectedError || Io.Cancelable; pub const ConnectOptions = struct { @@ -804,19 +811,40 @@ pub const UnixAddress = struct { return .{ .path = p }; } - pub const ListenError = error{}; + pub const ListenError = error{ + AddressFamilyUnsupported, + AddressInUse, + NetworkDown, + SystemResources, + SymLinkLoop, + FileNotFound, + NotDir, + ReadOnlyFileSystem, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + AccessDenied, + PermissionDenied, + AddressUnavailable, + } || Io.Cancelable || Io.UnexpectedError; - pub fn listen(ua: UnixAddress, io: Io) ListenError!Server { + pub const ListenOptions = struct { + /// How many connections the kernel will accept on the application's behalf. + /// If more than this many connections pool in the kernel, clients will start + /// seeing "Connection refused". + kernel_backlog: u31 = default_kernel_backlog, + }; + + pub fn listen(ua: *const UnixAddress, io: Io, options: ListenOptions) ListenError!Server { assert(ua.path.len <= max_len); return .{ .socket = .{ - .handle = try io.vtable.netListenUnix(io.userdata, ua), + .handle = try io.vtable.netListenUnix(io.userdata, ua, options), .address = .{ .ip4 = .loopback(0) }, } }; } - pub const ConnectError = error{}; + pub const ConnectError = error{} || Io.Cancelable || Io.UnexpectedError; - pub fn connect(ua: UnixAddress, io: Io) ConnectError!Stream { + pub fn connect(ua: *const UnixAddress, io: Io) ConnectError!Stream { assert(ua.path.len <= max_len); return .{ .socket = .{ .handle = try io.vtable.netConnectUnix(io.userdata, ua), diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 9bd4618429..edac076a6a 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -273,7 +273,7 @@ test "listen on a unix socket, send bytes, receive bytes" { const socket_addr = try net.UnixAddress.init(socket_path); defer std.fs.cwd().deleteFile(socket_path) catch {}; - var server = try socket_addr.listen(io); + var server = try socket_addr.listen(io, .{}); defer server.socket.close(io); const S = struct { From 0732ff22638a0e4b53bd2c42565165679674b3e1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 22:05:18 -0700 Subject: [PATCH 102/244] std.Io.Threaded: implement connecting to unix sockets --- BRANCH_TODO | 3 +- lib/std/Io/Threaded.zig | 184 +++++++++++++++++++--------------------- lib/std/Io/net.zig | 31 ++++++- 3 files changed, 119 insertions(+), 99 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index afb955968d..65b7580f68 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -5,11 +5,12 @@ * fix Group.wait not handling cancelation (need to move impl of ResetEvent to Threaded) * implement cancelRequest for non-linux posix * finish converting all Threaded into directly calling system functions and handling EINTR +* audit the TODOs * move max_iovecs_len to std.Io * address the cancelation race condition (signal received between checkCancel and syscall) * update signal values to be an enum * move fs.File.Writer to Io -* add non-blocking flag to network operations, handle EAGAIN +* add non-blocking flag to net and fs operations, handle EAGAIN * finish moving std.fs to Io * finish moving all of std.posix into Threaded diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 6051c99555..a22a80c82e 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1697,34 +1697,10 @@ fn netListenIpPosix( ) Io.net.IpAddress.ListenError!Io.net.Server { const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(&address); - const protocol: u32 = posix.IPPROTO.TCP; - const socket_fd = while (true) { - try pool.checkCancel(); - const flags: u32 = posix.SOCK.STREAM | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; - const socket_rc = posix.system.socket(family, flags, protocol); - switch (posix.errno(socket_rc)) { - .SUCCESS => { - const fd: posix.fd_t = @intCast(socket_rc); - errdefer posix.close(fd); - if (socket_flags_unsupported) while (true) { - try pool.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { - .SUCCESS => break, - .INTR => continue, - else => |err| return posix.unexpectedErrno(err), - } - }; - break fd; - }, - .INTR => continue, - .AFNOSUPPORT => return error.AddressFamilyUnsupported, - .MFILE => return error.ProcessFdQuotaExceeded, - .NFILE => return error.SystemFdQuotaExceeded, - .NOBUFS => return error.SystemResources, - .NOMEM => return error.SystemResources, - else => |err| return posix.unexpectedErrno(err), - } - }; + const socket_fd = try openSocketPosix(pool, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); errdefer posix.close(socket_fd); if (options.reuse_address) { @@ -1763,61 +1739,17 @@ fn netListenUnix( ) Io.net.UnixAddress.ListenError!Io.net.Socket.Handle { if (!Io.net.has_unix_sockets) return error.AddressFamilyUnsupported; const pool: *Pool = @ptrCast(@alignCast(userdata)); - const protocol: u32 = 0; - const socket_fd = while (true) { - try pool.checkCancel(); - const flags: u32 = posix.SOCK.STREAM | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; - const socket_rc = posix.system.socket(posix.AF.UNIX, flags, protocol); - switch (posix.errno(socket_rc)) { - .SUCCESS => { - const fd: posix.fd_t = @intCast(socket_rc); - errdefer posix.close(fd); - if (socket_flags_unsupported) while (true) { - try pool.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { - .SUCCESS => break, - .INTR => continue, - else => |err| return posix.unexpectedErrno(err), - } - }; - break fd; - }, - .INTR => continue, - .AFNOSUPPORT => return error.AddressFamilyUnsupported, - .MFILE => return error.ProcessFdQuotaExceeded, - .NFILE => return error.SystemFdQuotaExceeded, - .NOBUFS => return error.SystemResources, - .NOMEM => return error.SystemResources, - else => |err| return posix.unexpectedErrno(err), - } + const socket_fd = openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported, + error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported, + error.SocketModeUnsupported => return error.AddressFamilyUnsupported, + else => |e| return e, }; errdefer posix.close(socket_fd); var storage: UnixAddress = undefined; const addr_len = addressUnixToPosix(address, &storage); - while (true) { - try pool.checkCancel(); - switch (posix.errno(posix.system.bind(socket_fd, &storage.any, addr_len))) { - .SUCCESS => break, - .INTR => continue, - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .ADDRINUSE => return error.AddressInUse, - .AFNOSUPPORT => return error.AddressFamilyUnsupported, - .ADDRNOTAVAIL => return error.AddressUnavailable, - .NOMEM => return error.SystemResources, - .LOOP => return error.SymLinkLoop, - .NOENT => return error.FileNotFound, - .NOTDIR => return error.NotDir, - .ROFS => return error.ReadOnlyFileSystem, - .BADF => |err| return errnoBug(err), // always a race condition if this error is returned - .INVAL => |err| return errnoBug(err), // invalid parameters - .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` - .FAULT => |err| return errnoBug(err), // invalid `addr` pointer - .NAMETOOLONG => |err| return errnoBug(err), - else => |err| return posix.unexpectedErrno(err), - } - } + try posixBindUnix(pool, socket_fd, &storage.any, addr_len); while (true) { try pool.checkCancel(); @@ -1832,6 +1764,34 @@ fn netListenUnix( return socket_fd; } +fn posixBindUnix(pool: *Pool, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.bind(fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .ACCES => return error.AccessDenied, + .ADDRINUSE => return error.AddressInUse, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .NOMEM => return error.SystemResources, + + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .PERM => return error.PermissionDenied, + + .BADF => |err| return errnoBug(err), // always a race condition if this error is returned + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NAMETOOLONG => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { try pool.checkCancel(); @@ -1857,7 +1817,6 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { .SUCCESS => return, .INTR => continue, - .ADDRINUSE => return error.AddressInUse, .ADDRNOTAVAIL => return error.AddressUnavailable, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, @@ -1866,7 +1825,7 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka .CONNREFUSED => return error.ConnectionRefused, .CONNRESET => return error.ConnectionResetByPeer, .FAULT => |err| return errnoBug(err), - .ISCONN => return error.AlreadyConnected, + .ISCONN => |err| return errnoBug(err), .HOSTUNREACH => return error.HostUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => |err| return errnoBug(err), @@ -1874,7 +1833,6 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka .TIMEDOUT => return error.ConnectionTimedOut, .CONNABORTED => |err| return errnoBug(err), .ACCES => return error.AccessDenied, - // UNIX socket error codes: .PERM => |err| return errnoBug(err), .NOENT => |err| return errnoBug(err), else => |err| return posix.unexpectedErrno(err), @@ -1882,6 +1840,35 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka } } +fn posixConnectUnix(pool: *Pool, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.connect(fd, addr, addr_len))) { + .SUCCESS => return, + .INTR => continue, + + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .AGAIN => return error.WouldBlock, + .INPROGRESS => return error.WouldBlock, + .ACCES => return error.AccessDenied, + + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .PERM => return error.PermissionDenied, + + .BADF => |err| return errnoBug(err), + .CONNABORTED => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .NOTSOCK => |err| return errnoBug(err), + .PROTOTYPE => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn posixGetSockName(pool: *Pool, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { while (true) { try pool.checkCancel(); @@ -1926,6 +1913,7 @@ fn netConnectIpPosix( .mode = options.mode, .protocol = options.protocol, }); + errdefer posix.close(socket_fd); var storage: PosixAddress = undefined; var addr_len = addressToPosix(address, &storage); try posixConnect(pool, socket_fd, &storage.any, addr_len); @@ -1940,10 +1928,14 @@ fn netConnectUnix( userdata: ?*anyopaque, address: *const Io.net.UnixAddress, ) Io.net.UnixAddress.ConnectError!Io.net.Socket.Handle { + if (!Io.net.has_unix_sockets) return error.AddressFamilyUnsupported; const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; - _ = address; - @panic("TODO"); + const socket_fd = try openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .stream }); + errdefer posix.close(socket_fd); + var storage: UnixAddress = undefined; + const addr_len = addressUnixToPosix(address, &storage); + try posixConnectUnix(pool, socket_fd, &storage.any, addr_len); + return socket_fd; } fn netBindIpPosix( @@ -2497,18 +2489,16 @@ fn netInterfaceNameResolve( name: *const Io.net.Interface.Name, ) Io.net.Interface.Name.ResolveError!Io.net.Interface { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); if (native_os == .linux) { - const rc = posix.system.socket(posix.AF.UNIX, posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, 0); - const sock_fd: posix.fd_t = switch (posix.errno(rc)) { - .SUCCESS => @intCast(rc), - .ACCES => return error.AccessDenied, - .MFILE => return error.SystemResources, - .NFILE => return error.SystemResources, - .NOBUFS => return error.SystemResources, - .NOMEM => return error.SystemResources, - else => |err| return posix.unexpectedErrno(err), + const sock_fd = openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .dgram }) catch |err| switch (err) { + error.ProcessFdQuotaExceeded => return error.SystemResources, + error.SystemFdQuotaExceeded => return error.SystemResources, + error.AddressFamilyUnsupported => return error.Unexpected, + error.ProtocolUnsupportedBySystem => return error.Unexpected, + error.ProtocolUnsupportedByAddressFamily => return error.Unexpected, + error.SocketModeUnsupported => return error.Unexpected, + else => |e| return e, }; defer posix.close(sock_fd); @@ -2521,12 +2511,12 @@ fn netInterfaceNameResolve( try pool.checkCancel(); switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) }, + .INTR => continue, .INVAL => |err| return errnoBug(err), // Bad parameters. .NOTTY => |err| return errnoBug(err), .NXIO => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), // Always a race condition. .FAULT => |err| return errnoBug(err), // Bad pointer parameter. - .INTR => continue, .IO => |err| return errnoBug(err), // sock_fd is not a file descriptor .NODEV => return error.InterfaceNotFound, else => |err| return posix.unexpectedErrno(err), @@ -2535,12 +2525,14 @@ fn netInterfaceNameResolve( } if (native_os == .windows) { + try pool.checkCancel(); const index = std.os.windows.ws2_32.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; return .{ .index = index }; } if (builtin.link_libc) { + try pool.checkCancel(); const index = std.c.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; return .{ .index = @bitCast(index) }; @@ -2591,7 +2583,7 @@ fn addressFromPosix(posix_address: *PosixAddress) Io.net.IpAddress { return switch (posix_address.any.family) { posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, - else => unreachable, + else => .{ .ip4 = .loopback(0) }, }; } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index c555a1d502..e8aadde38c 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -206,6 +206,9 @@ pub const IpAddress = union(enum) { SystemFdQuotaExceeded, /// The requested address family (IPv4 or IPv6) is not supported by the operating system. AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, } || Io.UnexpectedError || Io.Cancelable; pub const ListenOptions = struct { @@ -216,6 +219,16 @@ pub const IpAddress = union(enum) { /// Sets SO_REUSEADDR and SO_REUSEPORT on POSIX. /// Sets SO_REUSEADDR on Windows, which is roughly equivalent. reuse_address: bool = false, + /// Only connection-oriented modes may be used here, which includes: + /// * `Socket.Mode.stream` + /// * `Socket.Mode.seqpacket` + mode: Socket.Mode = .stream, + /// Only connection-oriented protocols may be used here, which includes: + /// * `Protocol.tcp` + /// * `Protocol.tp` + /// * `Protocol.dccp` + /// * `Protocol.sctp` + protocol: Protocol = .tcp, }; /// Waits for a TCP connection. When using this API, `bind` does not need @@ -276,7 +289,6 @@ pub const IpAddress = union(enum) { ConnectionPending, ConnectionRefused, ConnectionResetByPeer, - AlreadyConnected, HostUnreachable, NetworkUnreachable, ConnectionTimedOut, @@ -842,7 +854,22 @@ pub const UnixAddress = struct { } }; } - pub const ConnectError = error{} || Io.Cancelable || Io.UnexpectedError; + pub const ConnectError = error{ + SystemResources, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + AccessDenied, + PermissionDenied, + SymLinkLoop, + FileNotFound, + NotDir, + ReadOnlyFileSystem, + WouldBlock, + } || Io.Cancelable || Io.UnexpectedError; pub fn connect(ua: *const UnixAddress, io: Io) ConnectError!Stream { assert(ua.path.len <= max_len); From 79a59c516531650664511ac539ee6d7c6402169f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 22:10:29 -0700 Subject: [PATCH 103/244] coff linker: don't check the time compiler toolchains have no business knowing what time it is --- src/link/Coff.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/link/Coff.zig b/src/link/Coff.zig index ac78f75af5..32889684dc 100644 --- a/src/link/Coff.zig +++ b/src/link/Coff.zig @@ -610,7 +610,7 @@ fn create( .Obj => false, }; const machine = target.toCoffMachine(); - const timestamp: u32 = if (options.repro) 0 else @truncate(@as(u64, @bitCast(std.time.timestamp()))); + const timestamp: u32 = 0; const major_subsystem_version = options.major_subsystem_version orelse 6; const minor_subsystem_version = options.minor_subsystem_version orelse 0; const magic: std.coff.OptionalHeader.Magic = switch (target.ptrBitWidth()) { From c2d1a339da65da0c93fdd66df90955500417b3c9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 23:16:02 -0700 Subject: [PATCH 104/244] std.fs.File: begrudgingly add back deprecated APIs I'm not ready to rework MachO linker file access at the moment. --- lib/std/fs/File.zig | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 8253fe61e7..1299a2c2c9 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -655,6 +655,17 @@ pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize { return posix.pread(self.handle, buffer, offset); } +/// Deprecated in favor of `Reader`. +pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize { + var index: usize = 0; + while (index != buffer.len) { + const amt = try self.pread(buffer[index..], offset + index); + if (amt == 0) break; + index += amt; + } + return index; +} + /// See https://github.com/ziglang/zig/issues/7699 pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize { if (is_windows) { @@ -697,6 +708,14 @@ pub fn writeAll(self: File, bytes: []const u8) WriteError!void { } } +/// Deprecated in favor of `Writer`. +pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.pwrite(bytes[index..], offset + index); + } +} + /// On Windows, this function currently does alter the file pointer. /// https://github.com/ziglang/zig/issues/12783 pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize { @@ -732,6 +751,31 @@ pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError return posix.pwritev(self.handle, iovecs, offset); } +/// Deprecated in favor of `Writer`. +pub const CopyRangeError = posix.CopyFileRangeError; + +/// Deprecated in favor of `Writer`. +pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { + const adjusted_len = math.cast(usize, len) orelse maxInt(usize); + const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0); + return result; +} + +/// Deprecated in favor of `Writer`. +pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 { + var total_bytes_copied: u64 = 0; + var in_off = in_offset; + var out_off = out_offset; + while (total_bytes_copied < len) { + const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied); + if (amt_copied == 0) return total_bytes_copied; + total_bytes_copied += amt_copied; + in_off += amt_copied; + out_off += amt_copied; + } + return total_bytes_copied; +} + /// Deprecated in favor of `Io.File.Reader`. pub const Reader = Io.File.Reader; From 2bcdde29850c4b7b769ac3e0ffc636825fd7b5e5 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 13 Oct 2025 23:36:44 -0700 Subject: [PATCH 105/244] compiler: update for introduction of std.Io only thing remaining is using libc dns resolution when linking libc --- BRANCH_TODO | 1 + lib/compiler/aro/aro/Compilation.zig | 55 ++++++++++++++------------ lib/std/Io.zig | 4 ++ lib/std/Io/Threaded.zig | 7 ++-- lib/std/Uri.zig | 2 +- lib/std/c.zig | 9 +++++ lib/std/http/Client.zig | 6 +-- lib/std/posix.zig | 3 +- lib/std/tar/Writer.zig | 9 +++++ lib/std/zig/ErrorBundle.zig | 1 - src/Builtin.zig | 2 +- src/Compilation.zig | 59 +++++++++++++++++----------- src/IncrementalDebugServer.zig | 23 ++++++----- src/Package/Fetch.zig | 42 +++++++++++++------- src/Zcu.zig | 32 +++++++++------ src/Zcu/PerThread.zig | 17 ++++---- src/codegen/llvm.zig | 16 ++++---- src/fmt.zig | 16 ++++++-- src/libs/freebsd.zig | 5 ++- src/libs/glibc.zig | 5 ++- src/libs/libcxx.zig | 6 ++- src/libs/libtsan.zig | 3 +- src/libs/libunwind.zig | 3 +- src/libs/mingw.zig | 4 +- src/libs/musl.zig | 3 +- src/libs/netbsd.zig | 5 ++- src/link/Lld.zig | 14 +++---- src/link/MachO.zig | 16 ++++---- src/link/MappedFile.zig | 14 ++++--- src/link/Wasm.zig | 20 +++++++--- src/link/Wasm/Flush.zig | 11 ++++-- src/main.zig | 28 ++++++------- 32 files changed, 267 insertions(+), 174 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index 65b7580f68..c270307737 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -10,6 +10,7 @@ * move max_iovecs_len to std.Io * address the cancelation race condition (signal received between checkCancel and syscall) * update signal values to be an enum +* delete the deprecated fs.File functions * move fs.File.Writer to Io * add non-blocking flag to net and fs operations, handle EAGAIN * finish moving std.fs to Io diff --git a/lib/compiler/aro/aro/Compilation.zig b/lib/compiler/aro/aro/Compilation.zig index fea2ba2a62..46f783097b 100644 --- a/lib/compiler/aro/aro/Compilation.zig +++ b/lib/compiler/aro/aro/Compilation.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const EpochSeconds = std.time.epoch.EpochSeconds; const mem = std.mem; @@ -124,6 +125,7 @@ const Compilation = @This(); gpa: Allocator, /// Allocations in this arena live all the way until `Compilation.deinit`. arena: Allocator, +io: Io, diagnostics: *Diagnostics, code_gen_options: CodeGenOptions = .default, @@ -157,10 +159,11 @@ type_store: TypeStore = .{}, ms_cwd_source_id: ?Source.Id = null, cwd: std.fs.Dir, -pub fn init(gpa: Allocator, arena: Allocator, diagnostics: *Diagnostics, cwd: std.fs.Dir) Compilation { +pub fn init(gpa: Allocator, arena: Allocator, io: Io, diagnostics: *Diagnostics, cwd: std.fs.Dir) Compilation { return .{ .gpa = gpa, .arena = arena, + .io = io, .diagnostics = diagnostics, .cwd = cwd, }; @@ -222,14 +225,14 @@ pub const SystemDefinesMode = enum { include_system_defines, }; -fn generateSystemDefines(comp: *Compilation, w: *std.Io.Writer) !void { +fn generateSystemDefines(comp: *Compilation, w: *Io.Writer) !void { const define = struct { - fn define(_w: *std.Io.Writer, name: []const u8) !void { + fn define(_w: *Io.Writer, name: []const u8) !void { try _w.print("#define {s} 1\n", .{name}); } }.define; const defineStd = struct { - fn defineStd(_w: *std.Io.Writer, name: []const u8, is_gnu: bool) !void { + fn defineStd(_w: *Io.Writer, name: []const u8, is_gnu: bool) !void { if (is_gnu) { try _w.print("#define {s} 1\n", .{name}); } @@ -957,7 +960,7 @@ fn generateSystemDefines(comp: *Compilation, w: *std.Io.Writer) !void { pub fn generateBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode) AddSourceError!Source { try comp.type_store.initNamedTypes(comp); - var allocating: std.Io.Writer.Allocating = try .initCapacity(comp.gpa, 2 << 13); + var allocating: Io.Writer.Allocating = try .initCapacity(comp.gpa, 2 << 13); defer allocating.deinit(); comp.writeBuiltinMacros(system_defines_mode, &allocating.writer) catch |err| switch (err) { @@ -971,7 +974,7 @@ pub fn generateBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefi return comp.addSourceFromOwnedBuffer("", contents, .user); } -fn writeBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode, w: *std.Io.Writer) !void { +fn writeBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode, w: *Io.Writer) !void { if (system_defines_mode == .include_system_defines) { try w.writeAll( \\#define __VERSION__ "Aro @@ -1026,7 +1029,7 @@ fn writeBuiltinMacros(comp: *Compilation, system_defines_mode: SystemDefinesMode } } -fn generateFloatMacros(w: *std.Io.Writer, prefix: []const u8, semantics: target_util.FPSemantics, ext: []const u8) !void { +fn generateFloatMacros(w: *Io.Writer, prefix: []const u8, semantics: target_util.FPSemantics, ext: []const u8) !void { const denormMin = semantics.chooseValue( []const u8, .{ @@ -1101,7 +1104,7 @@ fn generateFloatMacros(w: *std.Io.Writer, prefix: []const u8, semantics: target_ try w.print("#define __{s}_MIN__ {s}{s}\n", .{ prefix, min, ext }); } -fn generateTypeMacro(comp: *const Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateTypeMacro(comp: *const Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try w.print("#define {s} ", .{name}); try qt.print(comp, w); try w.writeByte('\n'); @@ -1136,7 +1139,7 @@ fn generateFastOrLeastType( bits: usize, kind: enum { least, fast }, signedness: std.builtin.Signedness, - w: *std.Io.Writer, + w: *Io.Writer, ) !void { const ty = comp.intLeastN(bits, signedness); // defining the fast types as the least types is permitted @@ -1166,7 +1169,7 @@ fn generateFastOrLeastType( try comp.generateFmt(prefix, w, ty); } -fn generateFastAndLeastWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { +fn generateFastAndLeastWidthTypes(comp: *Compilation, w: *Io.Writer) !void { const sizes = [_]usize{ 8, 16, 32, 64 }; for (sizes) |size| { try comp.generateFastOrLeastType(size, .least, .signed, w); @@ -1176,7 +1179,7 @@ fn generateFastAndLeastWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { } } -fn generateExactWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { +fn generateExactWidthTypes(comp: *Compilation, w: *Io.Writer) !void { try comp.generateExactWidthType(w, .schar); if (QualType.short.sizeof(comp) > QualType.char.sizeof(comp)) { @@ -1224,7 +1227,7 @@ fn generateExactWidthTypes(comp: *Compilation, w: *std.Io.Writer) !void { } } -fn generateFmt(comp: *const Compilation, prefix: []const u8, w: *std.Io.Writer, qt: QualType) !void { +fn generateFmt(comp: *const Compilation, prefix: []const u8, w: *Io.Writer, qt: QualType) !void { const unsigned = qt.signedness(comp) == .unsigned; const modifier = qt.formatModifier(comp); const formats = if (unsigned) "ouxX" else "di"; @@ -1233,7 +1236,7 @@ fn generateFmt(comp: *const Compilation, prefix: []const u8, w: *std.Io.Writer, } } -fn generateSuffixMacro(comp: *const Compilation, prefix: []const u8, w: *std.Io.Writer, qt: QualType) !void { +fn generateSuffixMacro(comp: *const Compilation, prefix: []const u8, w: *Io.Writer, qt: QualType) !void { return w.print("#define {s}_C_SUFFIX__ {s}\n", .{ prefix, qt.intValueSuffix(comp) }); } @@ -1241,7 +1244,7 @@ fn generateSuffixMacro(comp: *const Compilation, prefix: []const u8, w: *std.Io. /// Name macro (e.g. #define __UINT32_TYPE__ unsigned int) /// Format strings (e.g. #define __UINT32_FMTu__ "u") /// Suffix macro (e.g. #define __UINT32_C_SUFFIX__ U) -fn generateExactWidthType(comp: *Compilation, w: *std.Io.Writer, original_qt: QualType) !void { +fn generateExactWidthType(comp: *Compilation, w: *Io.Writer, original_qt: QualType) !void { var qt = original_qt; const width = qt.sizeof(comp) * 8; const unsigned = qt.signedness(comp) == .unsigned; @@ -1274,7 +1277,7 @@ pub fn hasHalfPrecisionFloatABI(comp: *const Compilation) bool { return comp.langopts.allow_half_args_and_returns or target_util.hasHalfPrecisionFloatABI(comp.target); } -fn generateIntMax(comp: *const Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateIntMax(comp: *const Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { const unsigned = qt.signedness(comp) == .unsigned; const max: u128 = switch (qt.bitSizeof(comp)) { 8 => if (unsigned) std.math.maxInt(u8) else std.math.maxInt(i8), @@ -1298,7 +1301,7 @@ pub fn wcharMax(comp: *const Compilation) u32 { }; } -fn generateExactWidthIntMax(comp: *Compilation, w: *std.Io.Writer, original_qt: QualType) !void { +fn generateExactWidthIntMax(comp: *Compilation, w: *Io.Writer, original_qt: QualType) !void { var qt = original_qt; const bit_count: u8 = @intCast(qt.sizeof(comp) * 8); const unsigned = qt.signedness(comp) == .unsigned; @@ -1315,16 +1318,16 @@ fn generateExactWidthIntMax(comp: *Compilation, w: *std.Io.Writer, original_qt: return comp.generateIntMax(w, name, qt); } -fn generateIntWidth(comp: *Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateIntWidth(comp: *Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try w.print("#define __{s}_WIDTH__ {d}\n", .{ name, qt.sizeof(comp) * 8 }); } -fn generateIntMaxAndWidth(comp: *Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateIntMaxAndWidth(comp: *Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try comp.generateIntMax(w, name, qt); try comp.generateIntWidth(w, name, qt); } -fn generateSizeofType(comp: *Compilation, w: *std.Io.Writer, name: []const u8, qt: QualType) !void { +fn generateSizeofType(comp: *Compilation, w: *Io.Writer, name: []const u8, qt: QualType) !void { try w.print("#define {s} {d}\n", .{ name, qt.sizeof(comp) }); } @@ -1805,7 +1808,7 @@ pub const IncludeType = enum { angle_brackets, }; -fn getPathContents(comp: *Compilation, path: []const u8, limit: std.Io.Limit) ![]u8 { +fn getPathContents(comp: *Compilation, path: []const u8, limit: Io.Limit) ![]u8 { if (mem.indexOfScalar(u8, path, 0) != null) { return error.FileNotFound; } @@ -1815,11 +1818,12 @@ fn getPathContents(comp: *Compilation, path: []const u8, limit: std.Io.Limit) ![ return comp.getFileContents(file, limit); } -fn getFileContents(comp: *Compilation, file: std.fs.File, limit: std.Io.Limit) ![]u8 { +fn getFileContents(comp: *Compilation, file: std.fs.File, limit: Io.Limit) ![]u8 { + const io = comp.io; var file_buf: [4096]u8 = undefined; - var file_reader = file.reader(&file_buf); + var file_reader = file.reader(io, &file_buf); - var allocating: std.Io.Writer.Allocating = .init(comp.gpa); + var allocating: Io.Writer.Allocating = .init(comp.gpa); defer allocating.deinit(); if (file_reader.getSize()) |size| { const limited_size = limit.minInt64(size); @@ -1846,7 +1850,7 @@ pub fn findEmbed( includer_token_source: Source.Id, /// angle bracket vs quotes include_type: IncludeType, - limit: std.Io.Limit, + limit: Io.Limit, opt_dep_file: ?*DepFile, ) !?[]u8 { if (std.fs.path.isAbsolute(filename)) { @@ -2010,8 +2014,7 @@ pub fn locSlice(comp: *const Compilation, loc: Source.Location) []const u8 { pub fn getSourceMTimeUncached(comp: *const Compilation, source_id: Source.Id) ?u64 { const source = comp.getSource(source_id); if (comp.cwd.statFile(source.path)) |stat| { - const mtime = @divTrunc(stat.mtime, std.time.ns_per_s); - return std.math.cast(u64, mtime); + return std.math.cast(u64, stat.mtime.toSeconds()); } else |_| { return null; } diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 749e2b3ae8..4a8f65060e 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -878,6 +878,10 @@ pub const Timestamp = struct { return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s)); } + pub fn toNanoseconds(t: Timestamp) i96 { + return t.nanoseconds; + } + pub fn formatNumber(t: Timestamp, w: *std.Io.Writer, n: std.fmt.Number) std.Io.Writer.Error!void { return w.printInt(t.nanoseconds, n.mode.base() orelse 10, n.case, .{ .precision = n.precision, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a22a80c82e..9261c72f45 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1142,7 +1142,7 @@ fn dirOpenFile( } const fd: posix.fd_t = while (true) { try pool.checkCancel(); - const rc = openat_sym(dir.handle, sub_path_posix, os_flags, 0); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, @as(posix.mode_t, 0)); switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), .INTR => continue, @@ -2259,10 +2259,11 @@ fn netSendMany( const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), flags); switch (posix.errno(rc)) { .SUCCESS => { - for (clamped_messages[0..rc], clamped_msgs[0..rc]) |*message, *msg| { + const n: usize = @intCast(rc); + for (clamped_messages[0..n], clamped_msgs[0..n]) |*message, *msg| { message.data_len = msg.len; } - return rc; + return n; }, .AGAIN => |err| return errnoBug(err), .ALREADY => return error.FastOpenAlreadyInProgress, diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 1e2b4fb921..bf183cb09f 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -39,7 +39,7 @@ pub const GetHostAllocError = GetHostError || error{OutOfMemory}; /// /// See also: /// * `getHost` -pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError![]const u8 { +pub fn getHostAlloc(uri: Uri, arena: Allocator) GetHostAllocError!HostName { const component = uri.host orelse return error.UriMissingHost; const bytes = try component.toRawMaybeAlloc(arena); return .{ .bytes = bytes }; diff --git a/lib/std/c.zig b/lib/std/c.zig index d4fd066233..24ad58e242 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -4149,6 +4149,14 @@ const posix_msghdr_const = extern struct { flags: u32, }; +pub const mmsghdr = switch (native_os) { + .linux => linux.mmsghdr, + else => extern struct { + hdr: msghdr, + len: u32, + }, +}; + pub const cmsghdr = switch (native_os) { .linux => if (@bitSizeOf(usize) > @bitSizeOf(i32) and builtin.abi.isMusl()) posix_cmsghdr else linux.cmsghdr, // https://github.com/emscripten-core/emscripten/blob/96371ed7888fc78c040179f4d4faa82a6a07a116/system/lib/libc/musl/include/sys/socket.h#L44 @@ -10665,6 +10673,7 @@ pub extern "c" fn sendto( addrlen: socklen_t, ) isize; pub extern "c" fn sendmsg(sockfd: fd_t, msg: *const msghdr_const, flags: u32) isize; +pub extern "c" fn sendmmsg(sockfd: fd_t, msgvec: [*]mmsghdr, n: c_uint, flags: u32) c_int; pub extern "c" fn recv( sockfd: fd_t, diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index 2660d22be9..f05d6ff5b1 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -377,17 +377,17 @@ pub const Connection = struct { } }; - pub const ReadError = std.crypto.tls.Client.ReadError || Io.net.Stream.ReadError; + pub const ReadError = std.crypto.tls.Client.ReadError || Io.net.Stream.Reader.Error; pub fn getReadError(c: *const Connection) ?ReadError { return switch (c.protocol) { .tls => { if (disable_tls) unreachable; const tls: *const Tls = @alignCast(@fieldParentPtr("connection", c)); - return tls.client.read_err orelse c.stream_reader.getError(); + return tls.client.read_err orelse c.stream_reader.err.?; }, .plain => { - return c.stream_reader.getError(); + return c.stream_reader.err.?; }, }; } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 47ca1e080c..bd5f04232d 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -5532,6 +5532,8 @@ pub const RealPathError = error{ /// On Windows, the volume does not contain a recognized file system. File /// system drivers might not be loaded, or the volume may be corrupt. UnrecognizedVolume, + + Canceled, } || UnexpectedError; /// Return the canonicalized absolute pathname. @@ -5596,7 +5598,6 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[max_path_bytes]u8) RealP error.FileLocksNotSupported => unreachable, error.WouldBlock => unreachable, error.FileBusy => unreachable, // not asking for write permissions - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; defer close(fd); diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig index 129f1e7ede..583bc8cfc1 100644 --- a/lib/std/tar/Writer.zig +++ b/lib/std/tar/Writer.zig @@ -39,6 +39,15 @@ pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void { pub const WriteFileError = Io.Writer.FileError || Error || Io.File.Reader.SizeError; +pub fn writeFileTimestamp( + w: *Writer, + sub_path: []const u8, + file_reader: *Io.File.Reader, + mtime: Io.Timestamp, +) WriteFileError!void { + return writeFile(w, sub_path, file_reader, @intCast(mtime.toSeconds())); +} + pub fn writeFile( w: *Writer, sub_path: []const u8, diff --git a/lib/std/zig/ErrorBundle.zig b/lib/std/zig/ErrorBundle.zig index bbc758f5e8..2b2ad396de 100644 --- a/lib/std/zig/ErrorBundle.zig +++ b/lib/std/zig/ErrorBundle.zig @@ -321,7 +321,6 @@ fn writeMsg(eb: ErrorBundle, err_msg: ErrorMessage, w: *Writer, indent: usize) ! pub const Wip = struct { gpa: Allocator, - io: Io, string_bytes: std.ArrayListUnmanaged(u8), /// The first thing in this array is a ErrorMessageList. extra: std.ArrayListUnmanaged(u32), diff --git a/src/Builtin.zig b/src/Builtin.zig index 7680d495d7..b0077f2276 100644 --- a/src/Builtin.zig +++ b/src/Builtin.zig @@ -360,7 +360,7 @@ pub fn updateFileOnDisk(file: *File, comp: *Compilation) !void { file.stat = .{ .size = file.source.?.len, .inode = 0, // dummy value - .mtime = 0, // dummy value + .mtime = .zero, // dummy value }; } diff --git a/src/Compilation.zig b/src/Compilation.zig index 0692c4ce80..3670bc51b5 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -55,6 +55,7 @@ gpa: Allocator, /// Not thread-safe - lock `mutex` if potentially accessing from multiple /// threads at once. arena: Allocator, +io: Io, /// Not every Compilation compiles .zig code! For example you could do `zig build-exe foo.o`. zcu: ?*Zcu, /// Contains different state depending on the `CacheMode` used by this `Compilation`. @@ -1077,26 +1078,26 @@ pub const CObject = struct { diag.* = undefined; } - pub fn count(diag: Diag) u32 { + pub fn count(diag: *const Diag) u32 { var total: u32 = 1; for (diag.sub_diags) |sub_diag| total += sub_diag.count(); return total; } - pub fn addToErrorBundle(diag: Diag, eb: *ErrorBundle.Wip, bundle: Bundle, note: *u32) !void { - const err_msg = try eb.addErrorMessage(try diag.toErrorMessage(eb, bundle, 0)); + pub fn addToErrorBundle(diag: *const Diag, io: Io, eb: *ErrorBundle.Wip, bundle: Bundle, note: *u32) !void { + const err_msg = try eb.addErrorMessage(try diag.toErrorMessage(io, eb, bundle, 0)); eb.extra.items[note.*] = @intFromEnum(err_msg); note.* += 1; - for (diag.sub_diags) |sub_diag| try sub_diag.addToErrorBundle(eb, bundle, note); + for (diag.sub_diags) |sub_diag| try sub_diag.addToErrorBundle(io, eb, bundle, note); } pub fn toErrorMessage( - diag: Diag, + diag: *const Diag, + io: Io, eb: *ErrorBundle.Wip, bundle: Bundle, notes_len: u32, ) !ErrorBundle.ErrorMessage { - const io = eb.io; var start = diag.src_loc.offset; var end = diag.src_loc.offset; for (diag.src_ranges) |src_range| { @@ -1307,14 +1308,14 @@ pub const CObject = struct { return bundle; } - pub fn addToErrorBundle(bundle: Bundle, eb: *ErrorBundle.Wip) !void { + pub fn addToErrorBundle(bundle: Bundle, io: Io, eb: *ErrorBundle.Wip) !void { for (bundle.diags) |diag| { const notes_len = diag.count() - 1; - try eb.addRootErrorMessage(try diag.toErrorMessage(eb, bundle, notes_len)); + try eb.addRootErrorMessage(try diag.toErrorMessage(io, eb, bundle, notes_len)); if (notes_len > 0) { var note = try eb.reserveNotes(notes_len); for (diag.sub_diags) |sub_diag| - try sub_diag.addToErrorBundle(eb, bundle, ¬e); + try sub_diag.addToErrorBundle(io, eb, bundle, ¬e); } } } @@ -1906,7 +1907,7 @@ pub const CreateDiagnostic = union(enum) { return error.CreateFail; } }; -pub fn create(gpa: Allocator, arena: Allocator, diag: *CreateDiagnostic, options: CreateOptions) error{ +pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, options: CreateOptions) error{ OutOfMemory, Unexpected, CurrentWorkingDirectoryUnlinked, @@ -2114,6 +2115,7 @@ pub fn create(gpa: Allocator, arena: Allocator, diag: *CreateDiagnostic, options const cache = try arena.create(Cache); cache.* = .{ .gpa = gpa, + .io = io, .manifest_dir = options.dirs.local_cache.handle.makeOpenPath("h", .{}) catch |err| { return diag.fail(.{ .create_cache_path = .{ .which = .local, .sub = "h", .err = err } }); }, @@ -2232,6 +2234,7 @@ pub fn create(gpa: Allocator, arena: Allocator, diag: *CreateDiagnostic, options comp.* = .{ .gpa = gpa, .arena = arena, + .io = io, .zcu = opt_zcu, .cache_use = undefined, // populated below .bin_file = null, // populated below if necessary @@ -3919,13 +3922,14 @@ fn addBuf(list: *std.array_list.Managed([]const u8), buf: []const u8) void { /// This function is temporally single-threaded. pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle { const gpa = comp.gpa; + const io = comp.io; var bundle: ErrorBundle.Wip = undefined; try bundle.init(gpa); defer bundle.deinit(); for (comp.failed_c_objects.values()) |diag_bundle| { - try diag_bundle.addToErrorBundle(&bundle); + try diag_bundle.addToErrorBundle(io, &bundle); } for (comp.failed_win32_resources.values()) |error_bundle| { @@ -5310,6 +5314,7 @@ fn docsCopyModule( name: []const u8, tar_file_writer: *fs.File.Writer, ) !void { + const io = comp.io; const root = module.root; var mod_dir = d: { const root_dir, const sub_path = root.openInfo(comp.dirs); @@ -5343,9 +5348,9 @@ fn docsCopyModule( }; defer file.close(); const stat = try file.stat(); - var file_reader: fs.File.Reader = .initSize(file, &buffer, stat.size); + var file_reader: fs.File.Reader = .initSize(file.adaptToNewApi(), io, &buffer, stat.size); - archiver.writeFile(entry.path, &file_reader, stat.mtime) catch |err| { + archiver.writeFileTimestamp(entry.path, &file_reader, stat.mtime) catch |err| { return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive {f}{s}: {t}", .{ root.fmt(comp), entry.path, err, }); @@ -5365,6 +5370,7 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubUpdateError!void { const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -5373,7 +5379,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU const optimize_mode = std.builtin.OptimizeMode.ReleaseSmall; const output_mode = std.builtin.OutputMode.Exe; const resolved_target: Package.Module.ResolvedTarget = .{ - .result = std.zig.system.resolveTargetQuery(.{ + .result = std.zig.system.resolveTargetQuery(io, .{ .cpu_arch = .wasm32, .os_tag = .freestanding, .cpu_features_add = std.Target.wasm.featureSet(&.{ @@ -5449,7 +5455,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU try root_mod.deps.put(arena, "Walk", walk_mod); var sub_create_diag: CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(gpa, arena, io, &sub_create_diag, .{ .dirs = dirs, .self_exe_path = comp.self_exe_path, .config = config, @@ -5667,6 +5673,8 @@ pub fn translateC( ) !CImportResult { dev.check(.translate_c_command); + const gpa = comp.gpa; + const io = comp.io; const tmp_basename = std.fmt.hex(std.crypto.random.int(u64)); const tmp_sub_path = "tmp" ++ fs.path.sep_str ++ tmp_basename; const cache_dir = comp.dirs.local_cache.handle; @@ -5706,9 +5714,9 @@ pub fn translateC( const mcpu = mcpu: { var buf: std.ArrayListUnmanaged(u8) = .empty; - defer buf.deinit(comp.gpa); + defer buf.deinit(gpa); - try buf.print(comp.gpa, "-mcpu={s}", .{target.cpu.model.name}); + try buf.print(gpa, "-mcpu={s}", .{target.cpu.model.name}); // TODO better serialization https://github.com/ziglang/zig/issues/4584 const all_features_list = target.cpu.arch.allFeaturesList(); @@ -5718,7 +5726,7 @@ pub fn translateC( const is_enabled = target.cpu.features.isEnabled(index); const plus_or_minus = "-+"[@intFromBool(is_enabled)]; - try buf.print(comp.gpa, "{c}{s}", .{ plus_or_minus, feature.name }); + try buf.print(gpa, "{c}{s}", .{ plus_or_minus, feature.name }); } break :mcpu try buf.toOwnedSlice(arena); }; @@ -5731,7 +5739,7 @@ pub fn translateC( } var stdout: []u8 = undefined; - try @import("main.zig").translateC(comp.gpa, arena, argv.items, prog_node, &stdout); + try @import("main.zig").translateC(gpa, arena, io, argv.items, prog_node, &stdout); if (out_dep_path) |dep_file_path| add_deps: { if (comp.verbose_cimport) log.info("processing dep file at {s}", .{dep_file_path}); @@ -5767,7 +5775,7 @@ pub fn translateC( fatal("unable to read {}-byte translate-c message body: {s}", .{ header.bytes_len, @errorName(err) }); switch (header.tag) { .error_bundle => { - const error_bundle = try std.zig.Server.allocErrorBundle(comp.gpa, body); + const error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); return .{ .digest = undefined, .cache_hit = false, @@ -6154,6 +6162,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr log.debug("updating C object: {s}", .{c_object.src.src_path}); const gpa = comp.gpa; + const io = comp.io; if (c_object.clearStatus(gpa)) { // There was previous failure. @@ -6353,7 +6362,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr try child.spawn(); - var stderr_reader = child.stderr.?.readerStreaming(&.{}); + var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); const stderr = try stderr_reader.interface.allocRemaining(arena, .limited(std.math.maxInt(u32))); const term = child.wait() catch |err| { @@ -6362,7 +6371,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr switch (term) { .Exited => |code| if (code != 0) if (out_diag_path) |diag_file_path| { - const bundle = CObject.Diag.Bundle.parse(gpa, diag_file_path) catch |err| { + const bundle = CObject.Diag.Bundle.parse(gpa, io, diag_file_path) catch |err| { log.err("{}: failed to parse clang diagnostics: {s}", .{ err, stderr }); return comp.failCObj(c_object, "clang exited with code {d}", .{code}); }; @@ -7807,6 +7816,7 @@ fn buildOutputFromZig( defer tracy_trace.end(); const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -7880,7 +7890,7 @@ fn buildOutputFromZig( }; var sub_create_diag: CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .cache_mode = .whole, .parent_whole_cache = parent_whole_cache, @@ -7948,6 +7958,7 @@ pub fn build_crt_file( defer tracy_trace.end(); const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -8016,7 +8027,7 @@ pub fn build_crt_file( } var sub_create_diag: CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, diff --git a/src/IncrementalDebugServer.zig b/src/IncrementalDebugServer.zig index 358b1a4327..0ac1b360b8 100644 --- a/src/IncrementalDebugServer.zig +++ b/src/IncrementalDebugServer.zig @@ -44,22 +44,24 @@ pub fn spawn(ids: *IncrementalDebugServer) void { } fn runThread(ids: *IncrementalDebugServer) void { const gpa = ids.zcu.gpa; + const io = ids.zcu.comp.io; var cmd_buf: [1024]u8 = undefined; var text_out: std.ArrayListUnmanaged(u8) = .empty; defer text_out.deinit(gpa); - const addr = std.net.Address.parseIp6("::", port) catch unreachable; - var server = addr.listen(.{}) catch @panic("IncrementalDebugServer: failed to listen"); - defer server.deinit(); - const conn = server.accept() catch @panic("IncrementalDebugServer: failed to accept"); - defer conn.stream.close(); + const addr: std.Io.net.IpAddress = .{ .ip6 = .loopback(port) }; + var server = addr.listen(io, .{}) catch @panic("IncrementalDebugServer: failed to listen"); + defer server.deinit(io); + var stream = server.accept(io) catch @panic("IncrementalDebugServer: failed to accept"); + defer stream.close(io); - var stream_reader = conn.stream.reader(&cmd_buf); + var stream_reader = stream.reader(io, &cmd_buf); + var stream_writer = stream.writer(io, &.{}); while (ids.running.load(.monotonic)) { - conn.stream.writeAll("zig> ") catch @panic("IncrementalDebugServer: failed to write"); - const untrimmed = stream_reader.interface().takeSentinel('\n') catch |err| switch (err) { + stream_writer.interface.writeAll("zig> ") catch @panic("IncrementalDebugServer: failed to write"); + const untrimmed = stream_reader.interface.takeSentinel('\n') catch |err| switch (err) { error.EndOfStream => break, else => @panic("IncrementalDebugServer: failed to read command"), }; @@ -72,7 +74,7 @@ fn runThread(ids: *IncrementalDebugServer) void { text_out.clearRetainingCapacity(); { if (!ids.mutex.tryLock()) { - conn.stream.writeAll("waiting for in-progress update to finish...\n") catch @panic("IncrementalDebugServer: failed to write"); + stream_writer.interface.writeAll("waiting for in-progress update to finish...\n") catch @panic("IncrementalDebugServer: failed to write"); ids.mutex.lock(); } defer ids.mutex.unlock(); @@ -81,7 +83,7 @@ fn runThread(ids: *IncrementalDebugServer) void { handleCommand(ids.zcu, &allocating.writer, cmd, arg) catch @panic("IncrementalDebugServer: out of memory"); } text_out.append(gpa, '\n') catch @panic("IncrementalDebugServer: out of memory"); - conn.stream.writeAll(text_out.items) catch @panic("IncrementalDebugServer: failed to write"); + stream_writer.interface.writeAll(text_out.items) catch @panic("IncrementalDebugServer: failed to write"); } std.debug.print("closing incremental debug server\n", .{}); } @@ -373,6 +375,7 @@ fn printType(ty: Type, zcu: *const Zcu, w: anytype) !void { } const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Compilation = @import("Compilation.zig"); diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index 9b1d1f18cf..08a49da30a 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -26,9 +26,13 @@ //! //! All of this must be done with only referring to the state inside this struct //! because this work will be done in a dedicated thread. +const Fetch = @This(); const builtin = @import("builtin"); +const native_os = builtin.os.tag; + const std = @import("std"); +const Io = std.Io; const fs = std.fs; const assert = std.debug.assert; const ascii = std.ascii; @@ -36,14 +40,13 @@ const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; const ThreadPool = std.Thread.Pool; const WaitGroup = std.Thread.WaitGroup; -const Fetch = @This(); const git = @import("Fetch/git.zig"); const Package = @import("../Package.zig"); const Manifest = Package.Manifest; const ErrorBundle = std.zig.ErrorBundle; -const native_os = builtin.os.tag; arena: std.heap.ArenaAllocator, +io: Io, location: Location, location_tok: std.zig.Ast.TokenIndex, hash_tok: std.zig.Ast.OptionalTokenIndex, @@ -323,6 +326,7 @@ pub const RunError = error{ }; pub fn run(f: *Fetch) RunError!void { + const io = f.io; const eb = &f.error_bundle; const arena = f.arena.allocator(); const gpa = f.arena.child_allocator; @@ -389,7 +393,7 @@ pub fn run(f: *Fetch) RunError!void { const file_err = if (dir_err == error.NotDir) e: { if (fs.cwd().openFile(path_or_url, .{})) |file| { - var resource: Resource = .{ .file = file.reader(&server_header_buffer) }; + var resource: Resource = .{ .file = file.reader(io, &server_header_buffer) }; return f.runResource(path_or_url, &resource, null); } else |err| break :e err; } else dir_err; @@ -484,7 +488,8 @@ fn runResource( resource: *Resource, remote_hash: ?Package.Hash, ) RunError!void { - defer resource.deinit(); + const io = f.io; + defer resource.deinit(io); const arena = f.arena.allocator(); const eb = &f.error_bundle; const s = fs.path.sep_str; @@ -697,6 +702,7 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void { } fn queueJobsForDeps(f: *Fetch) RunError!void { + const io = f.io; assert(f.job_queue.recursive); // If the package does not have a build.zig.zon file then there are no dependencies. @@ -786,6 +792,7 @@ fn queueJobsForDeps(f: *Fetch) RunError!void { f.job_queue.all_fetches.appendAssumeCapacity(new_fetch); } new_fetch.* = .{ + .io = io, .arena = std.heap.ArenaAllocator.init(gpa), .location = location, .location_tok = dep.location_tok, @@ -897,9 +904,9 @@ const Resource = union(enum) { decompress_buffer: []u8, }; - fn deinit(resource: *Resource) void { + fn deinit(resource: *Resource, io: Io) void { switch (resource.*) { - .file => |*file_reader| file_reader.file.close(), + .file => |*file_reader| file_reader.file.close(io), .http_request => |*http_request| http_request.request.deinit(), .git => |*git_resource| { git_resource.fetch_stream.deinit(); @@ -909,7 +916,7 @@ const Resource = union(enum) { resource.* = undefined; } - fn reader(resource: *Resource) *std.Io.Reader { + fn reader(resource: *Resource) *Io.Reader { return switch (resource.*) { .file => |*file_reader| return &file_reader.interface, .http_request => |*http_request| return http_request.response.readerDecompressing( @@ -985,6 +992,7 @@ const FileType = enum { const init_resource_buffer_size = git.Packet.max_data_length; fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u8) RunError!void { + const io = f.io; const arena = f.arena.allocator(); const eb = &f.error_bundle; @@ -995,7 +1003,7 @@ fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u f.parent_package_root, path, err, })); }; - resource.* = .{ .file = file.reader(reader_buffer) }; + resource.* = .{ .file = file.reader(io, reader_buffer) }; return; } @@ -1242,7 +1250,7 @@ fn unpackResource( } } -fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!UnpackResult { +fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *Io.Reader) RunError!UnpackResult { const eb = &f.error_bundle; const arena = f.arena.allocator(); @@ -1273,11 +1281,12 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!Un return res; } -fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, OutOfMemory, FetchFailed }!UnpackResult { +fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *Io.Reader) error{ ReadFailed, OutOfMemory, FetchFailed }!UnpackResult { // We write the entire contents to a file first because zip files // must be processed back to front and they could be too large to // load into memory. + const io = f.io; const cache_root = f.job_queue.global_cache; const prefix = "tmp/"; const suffix = ".zip"; @@ -1319,7 +1328,7 @@ fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, f.location_tok, try eb.printString("failed writing temporary zip file: {t}", .{err}), ); - break :b zip_file_writer.moveToReader(); + break :b zip_file_writer.moveToReader(io); }; var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() }; @@ -1339,7 +1348,10 @@ fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, } fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult { + const io = f.io; const arena = f.arena.allocator(); + // TODO don't try to get a gpa from an arena. expose this dependency higher up + // because the backing of arena could be page allocator const gpa = f.arena.child_allocator; const object_format: git.Oid.Format = resource.want_oid; @@ -1358,7 +1370,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!U const fetch_reader = &resource.fetch_stream.reader; _ = try fetch_reader.streamRemaining(&pack_file_writer.interface); try pack_file_writer.interface.flush(); - break :b pack_file_writer.moveToReader(); + break :b pack_file_writer.moveToReader(io); }; var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true }); @@ -1372,7 +1384,7 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!U } { - var index_file_reader = index_file.reader(&index_file_buffer); + var index_file_reader = index_file.reader(io, &index_file_buffer); const checkout_prog_node = f.prog_node.start("Checkout", 0); defer checkout_prog_node.end(); var repository: git.Repository = undefined; @@ -2029,7 +2041,7 @@ const UnpackResult = struct { // output errors to string var errors = try fetch.error_bundle.toOwnedBundle(""); defer errors.deinit(gpa); - var aw: std.Io.Writer.Allocating = .init(gpa); + var aw: Io.Writer.Allocating = .init(gpa); defer aw.deinit(); try errors.renderToWriter(.{ .ttyconf = .no_color }, &aw.writer); try std.testing.expectEqualStrings( @@ -2338,7 +2350,7 @@ const TestFetchBuilder = struct { if (notes_len > 0) { try std.testing.expectEqual(notes_len, em.notes_len); } - var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); + var aw: Io.Writer.Allocating = .init(std.testing.allocator); defer aw.deinit(); try errors.renderToWriter(.{ .ttyconf = .no_color }, &aw.writer); try std.testing.expectEqualStrings(msg, aw.written()); diff --git a/src/Zcu.zig b/src/Zcu.zig index 5af60dfa6c..ed1ae0eece 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -4,9 +4,12 @@ //! //! Each `Compilation` has exactly one or zero `Zcu`, depending on whether //! there is or is not any zig source code, respectively. +const Zcu = @This(); +const builtin = @import("builtin"); const std = @import("std"); -const builtin = @import("builtin"); +const Io = std.Io; +const Writer = std.Io.Writer; const mem = std.mem; const Allocator = std.mem.Allocator; const assert = std.debug.assert; @@ -15,9 +18,7 @@ const BigIntConst = std.math.big.int.Const; const BigIntMutable = std.math.big.int.Mutable; const Target = std.Target; const Ast = std.zig.Ast; -const Writer = std.Io.Writer; -const Zcu = @This(); const Compilation = @import("Compilation.zig"); const Cache = std.Build.Cache; pub const Value = @import("Value.zig"); @@ -1037,10 +1038,15 @@ pub const File = struct { stat: Cache.File.Stat, }; - pub const GetSourceError = error{ OutOfMemory, FileTooBig } || std.fs.File.OpenError || std.fs.File.ReadError; + pub const GetSourceError = error{ + OutOfMemory, + FileTooBig, + Streaming, + } || std.fs.File.OpenError || std.fs.File.ReadError; pub fn getSource(file: *File, zcu: *const Zcu) GetSourceError!Source { const gpa = zcu.gpa; + const io = zcu.comp.io; if (file.source) |source| return .{ .bytes = source, @@ -1061,7 +1067,7 @@ pub const File = struct { const source = try gpa.allocSentinel(u8, @intCast(stat.size), 0); errdefer gpa.free(source); - var file_reader = f.reader(&.{}); + var file_reader = f.reader(io, &.{}); file_reader.size = stat.size; file_reader.interface.readSliceAll(source) catch return file_reader.err.?; @@ -2859,9 +2865,9 @@ comptime { } } -pub fn loadZirCache(gpa: Allocator, cache_file: std.fs.File) !Zir { +pub fn loadZirCache(gpa: Allocator, io: Io, cache_file: std.fs.File) !Zir { var buffer: [2000]u8 = undefined; - var file_reader = cache_file.reader(&buffer); + var file_reader = cache_file.reader(io, &buffer); return result: { const header = file_reader.interface.takeStructPointer(Zir.Header) catch |err| break :result err; break :result loadZirCacheBody(gpa, header.*, &file_reader.interface); @@ -2871,7 +2877,7 @@ pub fn loadZirCache(gpa: Allocator, cache_file: std.fs.File) !Zir { }; } -pub fn loadZirCacheBody(gpa: Allocator, header: Zir.Header, cache_br: *std.Io.Reader) !Zir { +pub fn loadZirCacheBody(gpa: Allocator, header: Zir.Header, cache_br: *Io.Reader) !Zir { var instructions: std.MultiArrayList(Zir.Inst) = .{}; errdefer instructions.deinit(gpa); @@ -2940,7 +2946,7 @@ pub fn saveZirCache(gpa: Allocator, cache_file: std.fs.File, stat: std.fs.File.S .stat_size = stat.size, .stat_inode = stat.inode, - .stat_mtime = stat.mtime, + .stat_mtime = stat.mtime.toNanoseconds(), }; var vecs = [_][]const u8{ @ptrCast((&header)[0..1]), @@ -2969,7 +2975,7 @@ pub fn saveZoirCache(cache_file: std.fs.File, stat: std.fs.File.Stat, zoir: Zoir .stat_size = stat.size, .stat_inode = stat.inode, - .stat_mtime = stat.mtime, + .stat_mtime = stat.mtime.toNanoseconds(), }; var vecs = [_][]const u8{ @ptrCast((&header)[0..1]), @@ -2988,7 +2994,7 @@ pub fn saveZoirCache(cache_file: std.fs.File, stat: std.fs.File.Stat, zoir: Zoir }; } -pub fn loadZoirCacheBody(gpa: Allocator, header: Zoir.Header, cache_br: *std.Io.Reader) !Zoir { +pub fn loadZoirCacheBody(gpa: Allocator, header: Zoir.Header, cache_br: *Io.Reader) !Zoir { var zoir: Zoir = .{ .nodes = .empty, .extra = &.{}, @@ -4283,7 +4289,7 @@ const FormatAnalUnit = struct { zcu: *Zcu, }; -fn formatAnalUnit(data: FormatAnalUnit, writer: *std.Io.Writer) std.Io.Writer.Error!void { +fn formatAnalUnit(data: FormatAnalUnit, writer: *Io.Writer) Io.Writer.Error!void { const zcu = data.zcu; const ip = &zcu.intern_pool; switch (data.unit.unwrap()) { @@ -4309,7 +4315,7 @@ fn formatAnalUnit(data: FormatAnalUnit, writer: *std.Io.Writer) std.Io.Writer.Er const FormatDependee = struct { dependee: InternPool.Dependee, zcu: *Zcu }; -fn formatDependee(data: FormatDependee, writer: *std.Io.Writer) std.Io.Writer.Error!void { +fn formatDependee(data: FormatDependee, writer: *Io.Writer) Io.Writer.Error!void { const zcu = data.zcu; const ip = &zcu.intern_pool; switch (data.dependee) { diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index 8f2656e78a..474ccc710d 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -87,6 +87,7 @@ pub fn updateFile( const zcu = pt.zcu; const comp = zcu.comp; const gpa = zcu.gpa; + const io = comp.io; // In any case we need to examine the stat of the file to determine the course of action. var source_file = f: { @@ -127,7 +128,7 @@ pub fn updateFile( .astgen_failure, .success => lock: { const unchanged_metadata = stat.size == file.stat.size and - stat.mtime == file.stat.mtime and + stat.mtime.nanoseconds == file.stat.mtime.nanoseconds and stat.inode == file.stat.inode; if (unchanged_metadata) { @@ -173,8 +174,6 @@ pub fn updateFile( .lock = lock, }) catch |err| switch (err) { error.NotDir => unreachable, // no dir components - error.InvalidUtf8 => unreachable, // it's a hex encoded name - error.InvalidWtf8 => unreachable, // it's a hex encoded name error.BadPathName => unreachable, // it's a hex encoded name error.NameTooLong => unreachable, // it's a fixed size name error.PipeBusy => unreachable, // it's not a pipe @@ -255,7 +254,7 @@ pub fn updateFile( const source = try gpa.allocSentinel(u8, @intCast(stat.size), 0); defer if (file.source == null) gpa.free(source); - var source_fr = source_file.reader(&.{}); + var source_fr = source_file.reader(io, &.{}); source_fr.size = stat.size; source_fr.interface.readSliceAll(source) catch |err| switch (err) { error.ReadFailed => return source_fr.err.?, @@ -353,6 +352,7 @@ fn loadZirZoirCache( assert(file.getMode() == mode); const gpa = zcu.gpa; + const io = zcu.comp.io; const Header = switch (mode) { .zig => Zir.Header, @@ -360,7 +360,7 @@ fn loadZirZoirCache( }; var buffer: [2000]u8 = undefined; - var cache_fr = cache_file.reader(&buffer); + var cache_fr = cache_file.reader(io, &buffer); cache_fr.size = stat.size; const cache_br = &cache_fr.interface; @@ -375,7 +375,7 @@ fn loadZirZoirCache( const unchanged_metadata = stat.size == header.stat_size and - stat.mtime == header.stat_mtime and + stat.mtime.nanoseconds == header.stat_mtime and stat.inode == header.stat_inode; if (!unchanged_metadata) { @@ -2436,6 +2436,7 @@ fn updateEmbedFileInner( const tid = pt.tid; const zcu = pt.zcu; const gpa = zcu.gpa; + const io = zcu.comp.io; const ip = &zcu.intern_pool; var file = f: { @@ -2450,7 +2451,7 @@ fn updateEmbedFileInner( const old_stat = ef.stat; const unchanged_metadata = stat.size == old_stat.size and - stat.mtime == old_stat.mtime and + stat.mtime.nanoseconds == old_stat.mtime.nanoseconds and stat.inode == old_stat.inode; if (unchanged_metadata) return; } @@ -2464,7 +2465,7 @@ fn updateEmbedFileInner( const old_len = string_bytes.mutate.len; errdefer string_bytes.shrinkRetainingCapacity(old_len); const bytes = (try string_bytes.addManyAsSlice(size_plus_one))[0]; - var fr = file.reader(&.{}); + var fr = file.reader(io, &.{}); fr.size = stat.size; fr.interface.readSliceAll(bytes[0..size]) catch |err| switch (err) { error.ReadFailed => return fr.err.?, diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 8242958073..752c0a6004 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -794,10 +794,10 @@ pub const Object = struct { pub const EmitOptions = struct { pre_ir_path: ?[]const u8, pre_bc_path: ?[]const u8, - bin_path: ?[*:0]const u8, - asm_path: ?[*:0]const u8, - post_ir_path: ?[*:0]const u8, - post_bc_path: ?[*:0]const u8, + bin_path: ?[:0]const u8, + asm_path: ?[:0]const u8, + post_ir_path: ?[:0]const u8, + post_bc_path: ?[]const u8, is_debug: bool, is_small: bool, @@ -1001,7 +1001,7 @@ pub const Object = struct { options.post_ir_path == null and options.post_bc_path == null) return; if (options.post_bc_path) |path| { - var file = std.fs.cwd().createFileZ(path, .{}) catch |err| + var file = std.fs.cwd().createFile(path, .{}) catch |err| return diags.fail("failed to create '{s}': {s}", .{ path, @errorName(err) }); defer file.close(); @@ -1110,8 +1110,8 @@ pub const Object = struct { // though it's clearly not ready and produces multiple miscompilations in our std tests. .allow_machine_outliner = !comp.root_mod.resolved_target.result.cpu.arch.isRISCV(), .asm_filename = null, - .bin_filename = options.bin_path, - .llvm_ir_filename = options.post_ir_path, + .bin_filename = if (options.bin_path) |x| x.ptr else null, + .llvm_ir_filename = if (options.post_ir_path) |x| x.ptr else null, .bitcode_filename = null, // `.coverage` value is only used when `.sancov` is enabled. @@ -1158,7 +1158,7 @@ pub const Object = struct { lowered_options.time_report_out = &time_report_c_str; } - lowered_options.asm_filename = options.asm_path; + lowered_options.asm_filename = if (options.asm_path) |x| x.ptr else null; if (target_machine.emitToFile(module, &error_message, &lowered_options)) { defer llvm.disposeMessage(error_message); return diags.fail("LLVM failed to emit asm={s} bin={s} ir={s} bc={s}: {s}", .{ diff --git a/src/fmt.zig b/src/fmt.zig index 4a3c321877..344a89d6ed 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const mem = std.mem; const fs = std.fs; const process = std.process; @@ -34,13 +35,14 @@ const Fmt = struct { color: Color, gpa: Allocator, arena: Allocator, + io: Io, out_buffer: std.Io.Writer.Allocating, stdout_writer: *fs.File.Writer, const SeenMap = std.AutoHashMap(fs.File.INode, void); }; -pub fn run(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { +pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void { var color: Color = .auto; var stdin_flag = false; var check_flag = false; @@ -99,7 +101,7 @@ pub fn run(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { const stdin: fs.File = .stdin(); var stdio_buffer: [1024]u8 = undefined; - var file_reader: fs.File.Reader = stdin.reader(&stdio_buffer); + var file_reader: fs.File.Reader = stdin.reader(io, &stdio_buffer); const source_code = std.zig.readSourceFileToEndAlloc(gpa, &file_reader) catch |err| { fatal("unable to read stdin: {}", .{err}); }; @@ -165,6 +167,7 @@ pub fn run(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { var fmt: Fmt = .{ .gpa = gpa, .arena = arena, + .io = io, .seen = .init(gpa), .any_error = false, .check_ast = check_ast_flag, @@ -255,6 +258,8 @@ fn fmtPathFile( dir: fs.Dir, sub_path: []const u8, ) !void { + const io = fmt.io; + const source_file = try dir.openFile(sub_path, .{}); var file_closed = false; errdefer if (!file_closed) source_file.close(); @@ -265,7 +270,7 @@ fn fmtPathFile( return error.IsDir; var read_buffer: [1024]u8 = undefined; - var file_reader: fs.File.Reader = source_file.reader(&read_buffer); + var file_reader: fs.File.Reader = source_file.reader(io, &read_buffer); file_reader.size = stat.size; const gpa = fmt.gpa; @@ -363,5 +368,8 @@ pub fn main() !void { var arena_instance = std.heap.ArenaAllocator.init(gpa); const arena = arena_instance.allocator(); const args = try process.argsAlloc(arena); - return run(gpa, arena, args[1..]); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + return run(gpa, arena, io, args[1..]); } diff --git a/src/libs/freebsd.zig b/src/libs/freebsd.zig index 2315964a50..ff32b4649a 100644 --- a/src/libs/freebsd.zig +++ b/src/libs/freebsd.zig @@ -426,6 +426,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye } const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -438,6 +439,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = fs.cwd() }); @@ -1017,6 +1019,7 @@ fn buildSharedLib( const tracy = trace(@src()); defer tracy.end(); + const io = comp.io; const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover }); const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 }; const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?); @@ -1071,7 +1074,7 @@ fn buildSharedLib( const misc_task: Compilation.MiscTask = .@"freebsd libc shared object"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/libs/glibc.zig b/src/libs/glibc.zig index 8281bee303..76048239e0 100644 --- a/src/libs/glibc.zig +++ b/src/libs/glibc.zig @@ -666,6 +666,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye } const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -677,6 +678,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = fs.cwd() }); @@ -1175,6 +1177,7 @@ fn buildSharedLib( const tracy = trace(@src()); defer tracy.end(); + const io = comp.io; const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover }); const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 }; const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?); @@ -1229,7 +1232,7 @@ fn buildSharedLib( const misc_task: Compilation.MiscTask = .@"glibc shared object"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/libs/libcxx.zig b/src/libs/libcxx.zig index 4e39d1e03a..555d700a3d 100644 --- a/src/libs/libcxx.zig +++ b/src/libs/libcxx.zig @@ -123,6 +123,7 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError! defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const root_name = "c++"; const output_mode = .Lib; const link_mode = .static; @@ -263,7 +264,7 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError! const misc_task: Compilation.MiscTask = .libcxx; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, @@ -318,6 +319,7 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const root_name = "c++abi"; const output_mode = .Lib; const link_mode = .static; @@ -455,7 +457,7 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr const misc_task: Compilation.MiscTask = .libcxxabi; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, diff --git a/src/libs/libtsan.zig b/src/libs/libtsan.zig index 7a72ae2ac3..a10e63cf11 100644 --- a/src/libs/libtsan.zig +++ b/src/libs/libtsan.zig @@ -25,6 +25,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const target = comp.getTarget(); const root_name = switch (target.os.tag) { // On Apple platforms, we use the same name as LLVM because the @@ -277,7 +278,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo const misc_task: Compilation.MiscTask = .libtsan; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/libs/libunwind.zig b/src/libs/libunwind.zig index 765058087f..a79043b42f 100644 --- a/src/libs/libunwind.zig +++ b/src/libs/libunwind.zig @@ -26,6 +26,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; const output_mode = .Lib; const target = &comp.root_mod.resolved_target.result; const unwind_tables: std.builtin.UnwindTables = @@ -143,7 +144,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr const misc_task: Compilation.MiscTask = .libunwind; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .config = config, diff --git a/src/libs/mingw.zig b/src/libs/mingw.zig index ce78809892..1773c321e1 100644 --- a/src/libs/mingw.zig +++ b/src/libs/mingw.zig @@ -235,6 +235,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { dev.check(.build_import_lib); const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -255,6 +256,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() }); @@ -302,7 +304,7 @@ pub fn buildImportLib(comp: *Compilation, lib_name: []const u8) !void { .output = .{ .to_list = .{ .arena = .init(gpa) } }, }; defer diagnostics.deinit(); - var aro_comp = aro.Compilation.init(gpa, arena, &diagnostics, std.fs.cwd()); + var aro_comp = aro.Compilation.init(gpa, arena, io, &diagnostics, std.fs.cwd()); defer aro_comp.deinit(); aro_comp.target = target.*; diff --git a/src/libs/musl.zig b/src/libs/musl.zig index ae91425470..69bd892b3b 100644 --- a/src/libs/musl.zig +++ b/src/libs/musl.zig @@ -26,6 +26,7 @@ pub fn buildCrtFile(comp: *Compilation, in_crt_file: CrtFile, prog_node: std.Pro var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const io = comp.io; switch (in_crt_file) { .crt1_o => { @@ -246,7 +247,7 @@ pub fn buildCrtFile(comp: *Compilation, in_crt_file: CrtFile, prog_node: std.Pro const misc_task: Compilation.MiscTask = .@"musl libc.so"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .self_exe_path = comp.self_exe_path, .cache_mode = .whole, diff --git a/src/libs/netbsd.zig b/src/libs/netbsd.zig index e47bdce3af..07a7da7f6f 100644 --- a/src/libs/netbsd.zig +++ b/src/libs/netbsd.zig @@ -372,6 +372,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye } const gpa = comp.gpa; + const io = comp.io; var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -383,6 +384,7 @@ pub fn buildSharedObjects(comp: *Compilation, prog_node: std.Progress.Node) anye // Use the global cache directory. var cache: Cache = .{ .gpa = gpa, + .io = io, .manifest_dir = try comp.dirs.global_cache.handle.makeOpenPath("h", .{}), }; cache.addPrefix(.{ .path = null, .handle = fs.cwd() }); @@ -680,6 +682,7 @@ fn buildSharedLib( const tracy = trace(@src()); defer tracy.end(); + const io = comp.io; const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover }); const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 }; const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?); @@ -733,7 +736,7 @@ fn buildSharedLib( const misc_task: Compilation.MiscTask = .@"netbsd libc shared object"; var sub_create_diag: Compilation.CreateDiagnostic = undefined; - const sub_compilation = Compilation.create(comp.gpa, arena, &sub_create_diag, .{ + const sub_compilation = Compilation.create(comp.gpa, arena, io, &sub_create_diag, .{ .dirs = comp.dirs.withoutLocalCache(), .thread_pool = comp.thread_pool, .self_exe_path = comp.self_exe_path, diff --git a/src/link/Lld.zig b/src/link/Lld.zig index 2a3310384b..0d36f90a3e 100644 --- a/src/link/Lld.zig +++ b/src/link/Lld.zig @@ -1614,11 +1614,9 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void { } } -fn spawnLld( - comp: *Compilation, - arena: Allocator, - argv: []const []const u8, -) !void { +fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !void { + const io = comp.io; + if (comp.verbose_link) { // Skip over our own name so that the LLD linker name is the first argv item. Compilation.dump_argv(argv[1..]); @@ -1650,7 +1648,7 @@ fn spawnLld( child.stderr_behavior = .Pipe; child.spawn() catch |err| break :term err; - var stderr_reader = child.stderr.?.readerStreaming(&.{}); + var stderr_reader = child.stderr.?.readerStreaming(io, &.{}); stderr = try stderr_reader.interface.allocRemaining(comp.gpa, .unlimited); break :term child.wait(); }) catch |first_err| term: { @@ -1660,7 +1658,7 @@ fn spawnLld( const rand_int = std.crypto.random.int(u64); const rsp_path = "tmp" ++ s ++ std.fmt.hex(rand_int) ++ ".rsp"; - const rsp_file = try comp.dirs.local_cache.handle.createFileZ(rsp_path, .{}); + const rsp_file = try comp.dirs.local_cache.handle.createFile(rsp_path, .{}); defer comp.dirs.local_cache.handle.deleteFileZ(rsp_path) catch |err| log.warn("failed to delete response file {s}: {s}", .{ rsp_path, @errorName(err) }); { @@ -1700,7 +1698,7 @@ fn spawnLld( rsp_child.stderr_behavior = .Pipe; rsp_child.spawn() catch |err| break :err err; - var stderr_reader = rsp_child.stderr.?.readerStreaming(&.{}); + var stderr_reader = rsp_child.stderr.?.readerStreaming(io, &.{}); stderr = try stderr_reader.interface.allocRemaining(comp.gpa, .unlimited); break :term rsp_child.wait() catch |err| break :err err; } diff --git a/src/link/MachO.zig b/src/link/MachO.zig index 11127495ab..ef5c837fd1 100644 --- a/src/link/MachO.zig +++ b/src/link/MachO.zig @@ -915,7 +915,7 @@ pub fn readArMagic(file: std.fs.File, offset: usize, buffer: *[Archive.SARMAG]u8 return buffer[0..Archive.SARMAG]; } -fn addObject(self: *MachO, path: Path, handle: File.HandleIndex, offset: u64) !void { +fn addObject(self: *MachO, path: Path, handle_index: File.HandleIndex, offset: u64) !void { const tracy = trace(@src()); defer tracy.end(); @@ -929,17 +929,15 @@ fn addObject(self: *MachO, path: Path, handle: File.HandleIndex, offset: u64) !v }); errdefer gpa.free(abs_path); - const mtime: u64 = mtime: { - const file = self.getFileHandle(handle); - const stat = file.stat() catch break :mtime 0; - break :mtime @as(u64, @intCast(@divFloor(stat.mtime, 1_000_000_000))); - }; - const index = @as(File.Index, @intCast(try self.files.addOne(gpa))); + const file = self.getFileHandle(handle_index); + const stat = try file.stat(); + const mtime = stat.mtime.toSeconds(); + const index: File.Index = @intCast(try self.files.addOne(gpa)); self.files.set(index, .{ .object = .{ .offset = offset, .path = abs_path, - .file_handle = handle, - .mtime = mtime, + .file_handle = handle_index, + .mtime = @intCast(mtime), .index = index, } }); try self.objects.append(gpa, index); diff --git a/src/link/MappedFile.zig b/src/link/MappedFile.zig index 97c250f758..a9d874c23f 100644 --- a/src/link/MappedFile.zig +++ b/src/link/MappedFile.zig @@ -16,11 +16,13 @@ writers: std.SinglyLinkedList, pub const growth_factor = 4; -pub const Error = std.posix.MMapError || - std.posix.MRemapError || - std.fs.File.SetEndPosError || - std.fs.File.CopyRangeError || - error{NotFile}; +pub const Error = std.posix.MMapError || std.posix.MRemapError || std.fs.File.SetEndPosError || error{ + NotFile, + SystemResources, + IsDir, + Unseekable, + NoSpaceLeft, +}; pub fn init(file: std.fs.File, gpa: std.mem.Allocator) !MappedFile { var mf: MappedFile = .{ @@ -402,7 +404,7 @@ pub const Node = extern struct { const w: *Writer = @fieldParentPtr("interface", interface); const copy_size: usize = @intCast(w.mf.copyFileRange( - file_reader.file, + .adaptFromNewApi(file_reader.file), file_reader.pos, w.ni.fileLocation(w.mf, true).offset + interface.end, limit.minInt(interface.unusedCapacityLen()), diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index c0aeb01fc0..f47f7fbe2a 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -3029,18 +3029,22 @@ fn openParseObjectReportingFailure(wasm: *Wasm, path: Path) void { fn parseObject(wasm: *Wasm, obj: link.Input.Object) !void { log.debug("parseObject {f}", .{obj.path}); const gpa = wasm.base.comp.gpa; + const io = wasm.base.comp.io; const gc_sections = wasm.base.gc_sections; defer obj.file.close(); + var file_reader = obj.file.reader(io, &.{}); + try wasm.objects.ensureUnusedCapacity(gpa, 1); - const stat = try obj.file.stat(); - const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig; + const size = std.math.cast(usize, try file_reader.getSize()) orelse return error.FileTooBig; const file_contents = try gpa.alloc(u8, size); defer gpa.free(file_contents); - const n = try obj.file.preadAll(file_contents, 0); + const n = file_reader.interface.readSliceShort(file_contents) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; if (n != file_contents.len) return error.UnexpectedEndOfFile; var ss: Object.ScratchSpace = .{}; @@ -3053,17 +3057,21 @@ fn parseObject(wasm: *Wasm, obj: link.Input.Object) !void { fn parseArchive(wasm: *Wasm, obj: link.Input.Object) !void { log.debug("parseArchive {f}", .{obj.path}); const gpa = wasm.base.comp.gpa; + const io = wasm.base.comp.io; const gc_sections = wasm.base.gc_sections; defer obj.file.close(); - const stat = try obj.file.stat(); - const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig; + var file_reader = obj.file.reader(io, &.{}); + + const size = std.math.cast(usize, try file_reader.getSize()) orelse return error.FileTooBig; const file_contents = try gpa.alloc(u8, size); defer gpa.free(file_contents); - const n = try obj.file.preadAll(file_contents, 0); + const n = file_reader.interface.readSliceShort(file_contents) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + }; if (n != file_contents.len) return error.UnexpectedEndOfFile; var archive = try Archive.parse(gpa, file_contents); diff --git a/src/link/Wasm/Flush.zig b/src/link/Wasm/Flush.zig index b5d93259e2..6bc1887855 100644 --- a/src/link/Wasm/Flush.zig +++ b/src/link/Wasm/Flush.zig @@ -1064,9 +1064,14 @@ pub fn finish(f: *Flush, wasm: *Wasm) !void { } // Finally, write the entire binary into the file. - const file = wasm.base.file.?; - try file.pwriteAll(binary_bytes.items, 0); - try file.setEndPos(binary_bytes.items.len); + var file_writer = wasm.base.file.?.writer(&.{}); + file_writer.interface.writeAll(binary_bytes.items) catch |err| switch (err) { + error.WriteFailed => return file_writer.err.?, + }; + file_writer.end() catch |err| switch (err) { + error.WriteFailed => return file_writer.err.?, + else => |e| return e, + }; } const VirtualAddrs = struct { diff --git a/src/main.zig b/src/main.zig index 6ab768aac7..bd086c67f4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -312,7 +312,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { }); } else if (mem.eql(u8, cmd, "fmt")) { dev.check(.fmt_command); - return @import("fmt.zig").run(gpa, arena, cmd_args); + return @import("fmt.zig").run(gpa, arena, io, cmd_args); } else if (mem.eql(u8, cmd, "objcopy")) { return jitCmd(gpa, arena, io, cmd_args, .{ .cmd_name = "objcopy", @@ -376,7 +376,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "changelist")) { return cmdChangelist(arena, io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "dump-zir")) { - return cmdDumpZir(arena, cmd_args); + return cmdDumpZir(arena, io, cmd_args); } else if (build_options.enable_debug_extensions and mem.eql(u8, cmd, "llvm-ints")) { return cmdDumpLlvmInts(gpa, arena, cmd_args); } else { @@ -3376,7 +3376,7 @@ fn buildOutputType( try create_module.rpath_list.appendSlice(arena, rpath_dedup.keys()); var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, &create_diag, .{ + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ .dirs = dirs, .thread_pool = &thread_pool, .self_exe_path = switch (native_os) { @@ -3554,7 +3554,6 @@ fn buildOutputType( var stdin_reader = fs.File.stdin().reader(io, &stdin_buffer); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); try serve( - io, comp, &stdin_reader.interface, &stdout_writer.interface, @@ -3581,7 +3580,6 @@ fn buildOutputType( var output = stream.writer(io, &stdout_buffer); try serve( - io, comp, &input.interface, &output.interface, @@ -4051,7 +4049,6 @@ fn saveState(comp: *Compilation, incremental: bool) void { } fn serve( - io: Io, comp: *Compilation, in: *Io.Reader, out: *Io.Writer, @@ -4104,7 +4101,7 @@ fn serve( defer arena_instance.deinit(); const arena = arena_instance.allocator(); var output: Compilation.CImportResult = undefined; - try cmdTranslateC(io, comp, arena, &output, file_system_inputs, main_progress_node); + try cmdTranslateC(comp, arena, &output, file_system_inputs, main_progress_node); defer output.deinit(gpa); if (file_system_inputs.items.len != 0) { @@ -4537,6 +4534,8 @@ fn cmdTranslateC( ) !void { dev.check(.translate_c_command); + const io = comp.io; + assert(comp.c_source_files.len == 1); const c_source_file = comp.c_source_files[0]; @@ -4600,7 +4599,7 @@ fn cmdTranslateC( }; defer zig_file.close(); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); - var file_reader = zig_file.reader(&.{}); + var file_reader = zig_file.reader(io, &.{}); _ = try stdout_writer.interface.sendFileAll(&file_reader, .unlimited); try stdout_writer.interface.flush(); return cleanExit(); @@ -5156,6 +5155,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) var fetch: Package.Fetch = .{ .arena = std.heap.ArenaAllocator.init(gpa), + .io = io, .location = .{ .relative_path = phantom_package_root }, .location_tok = 0, .hash_tok = .none, @@ -5278,7 +5278,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) try root_mod.deps.put(arena, "@build", build_mod); var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, &create_diag, .{ + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ .libc_installation = libc_installation, .dirs = dirs, .root_name = "build", @@ -5522,7 +5522,7 @@ fn jitCmd( } var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, &create_diag, .{ + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ .dirs = dirs, .root_name = options.cmd_name, .config = config, @@ -6400,10 +6400,7 @@ fn cmdDumpLlvmInts( } /// This is only enabled for debug builds. -fn cmdDumpZir( - arena: Allocator, - args: []const []const u8, -) !void { +fn cmdDumpZir(arena: Allocator, io: Io, args: []const []const u8) !void { dev.check(.dump_zir_command); const Zir = std.zig.Zir; @@ -6415,7 +6412,7 @@ fn cmdDumpZir( }; defer f.close(); - const zir = try Zcu.loadZirCache(arena, f); + const zir = try Zcu.loadZirCache(arena, io, f); var stdout_writer = fs.File.stdout().writerStreaming(&stdout_buffer); const stdout_bw = &stdout_writer.interface; { @@ -6914,6 +6911,7 @@ fn cmdFetch( var fetch: Package.Fetch = .{ .arena = std.heap.ArenaAllocator.init(gpa), + .io = io, .location = .{ .path_or_url = path_or_url }, .location_tok = 0, .hash_tok = .none, From 1382e4122603fd2b57e4feb0eff76ba2d73a913a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 14 Oct 2025 10:50:00 -0700 Subject: [PATCH 106/244] std.Io.Threaded: import std.Io.net --- lib/std/Io/Threaded.zig | 101 ++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 9261c72f45..4ecc6e7b31 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -6,10 +6,11 @@ const is_windows = native_os == .windows; const windows = std.os.windows; const std = @import("../std.zig"); +const Io = std.Io; +const net = std.Io.net; const Allocator = std.mem.Allocator; const assert = std.debug.assert; const posix = std.posix; -const Io = std.Io; const ResetEvent = std.Thread.ResetEvent; /// Thread-safe. @@ -1692,9 +1693,9 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { fn netListenIpPosix( userdata: ?*anyopaque, - address: Io.net.IpAddress, - options: Io.net.IpAddress.ListenOptions, -) Io.net.IpAddress.ListenError!Io.net.Server { + address: net.IpAddress, + options: net.IpAddress.ListenOptions, +) net.IpAddress.ListenError!net.Server { const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(&address); const socket_fd = try openSocketPosix(pool, family, .{ @@ -1734,10 +1735,10 @@ fn netListenIpPosix( fn netListenUnix( userdata: ?*anyopaque, - address: *const Io.net.UnixAddress, - options: Io.net.UnixAddress.ListenOptions, -) Io.net.UnixAddress.ListenError!Io.net.Socket.Handle { - if (!Io.net.has_unix_sockets) return error.AddressFamilyUnsupported; + address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; const pool: *Pool = @ptrCast(@alignCast(userdata)); const socket_fd = openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported, @@ -1903,9 +1904,9 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio fn netConnectIpPosix( userdata: ?*anyopaque, - address: *const Io.net.IpAddress, - options: Io.net.IpAddress.ConnectOptions, -) Io.net.IpAddress.ConnectError!Io.net.Stream { + address: *const net.IpAddress, + options: net.IpAddress.ConnectOptions, +) net.IpAddress.ConnectError!net.Stream { if (options.timeout != .none) @panic("TODO"); const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); @@ -1926,9 +1927,9 @@ fn netConnectIpPosix( fn netConnectUnix( userdata: ?*anyopaque, - address: *const Io.net.UnixAddress, -) Io.net.UnixAddress.ConnectError!Io.net.Socket.Handle { - if (!Io.net.has_unix_sockets) return error.AddressFamilyUnsupported; + address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; const pool: *Pool = @ptrCast(@alignCast(userdata)); const socket_fd = try openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .stream }); errdefer posix.close(socket_fd); @@ -1940,9 +1941,9 @@ fn netConnectUnix( fn netBindIpPosix( userdata: ?*anyopaque, - address: *const Io.net.IpAddress, - options: Io.net.IpAddress.BindOptions, -) Io.net.IpAddress.BindError!Io.net.Socket { + address: *const net.IpAddress, + options: net.IpAddress.BindOptions, +) net.IpAddress.BindError!net.Socket { const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); const socket_fd = try openSocketPosix(pool, family, options); @@ -1957,7 +1958,7 @@ fn netBindIpPosix( }; } -fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: Io.net.IpAddress.BindOptions) !posix.socket_t { +fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: net.IpAddress.BindOptions) !posix.socket_t { const mode = posixSocketMode(options.mode); const protocol = posixProtocol(options.protocol); const socket_fd = while (true) { @@ -2003,7 +2004,7 @@ fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: Io.net.IpAdd const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 const have_accept4 = !socket_flags_unsupported; -fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: Io.net.Socket.Handle) Io.net.Server.AcceptError!Io.net.Stream { +fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream { const pool: *Pool = @ptrCast(@alignCast(userdata)); var storage: PosixAddress = undefined; var addr_len: posix.socklen_t = @sizeOf(PosixAddress); @@ -2050,7 +2051,7 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: Io.net.Socket.Handle) Io.net } }; } -fn netReadPosix(userdata: ?*anyopaque, fd: Io.net.Socket.Handle, data: [][]u8) Io.net.Stream.Reader.Error!usize { +fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; @@ -2113,10 +2114,10 @@ const have_sendmmsg = builtin.os.tag == .linux; fn netSend( userdata: ?*anyopaque, - handle: Io.net.Socket.Handle, - messages: []Io.net.OutgoingMessage, - flags: Io.net.SendFlags, -) struct { ?Io.net.Socket.SendError, usize } { + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { const pool: *Pool = @ptrCast(@alignCast(userdata)); const posix_flags: u32 = @@ -2141,10 +2142,10 @@ fn netSend( fn netSendOne( pool: *Pool, - handle: Io.net.Socket.Handle, - message: *Io.net.OutgoingMessage, + handle: net.Socket.Handle, + message: *net.OutgoingMessage, flags: u32, -) Io.net.Socket.SendError!void { +) net.Socket.SendError!void { var addr: PosixAddress = undefined; var iovec: posix.iovec = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; const msg: posix.msghdr = .{ @@ -2225,10 +2226,10 @@ fn netSendOne( fn netSendMany( pool: *Pool, - handle: Io.net.Socket.Handle, - messages: []Io.net.OutgoingMessage, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, flags: u32, -) Io.net.Socket.SendError!usize { +) net.Socket.SendError!usize { var msg_buffer: [64]std.os.linux.mmsghdr = undefined; var addr_buffer: [msg_buffer.len]PosixAddress = undefined; var iovecs_buffer: [msg_buffer.len]posix.iovec = undefined; @@ -2292,12 +2293,12 @@ fn netSendMany( fn netReceive( userdata: ?*anyopaque, - handle: Io.net.Socket.Handle, - message_buffer: []Io.net.IncomingMessage, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, data_buffer: []u8, - flags: Io.net.ReceiveFlags, + flags: net.ReceiveFlags, timeout: Io.Timeout, -) struct { ?Io.net.Socket.ReceiveTimeoutError, usize } { +) struct { ?net.Socket.ReceiveTimeoutError, usize } { const pool: *Pool = @ptrCast(@alignCast(userdata)); // recvmmsg is useless, here's why: @@ -2418,11 +2419,11 @@ fn netReceive( fn netWritePosix( userdata: ?*anyopaque, - fd: Io.net.Socket.Handle, + fd: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize, -) Io.net.Stream.Writer.Error!usize { +) net.Stream.Writer.Error!usize { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); @@ -2476,7 +2477,7 @@ fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), i.* += 1; } -fn netClose(userdata: ?*anyopaque, handle: Io.net.Socket.Handle) void { +fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { const pool: *Pool = @ptrCast(@alignCast(userdata)); _ = pool; switch (native_os) { @@ -2487,8 +2488,8 @@ fn netClose(userdata: ?*anyopaque, handle: Io.net.Socket.Handle) void { fn netInterfaceNameResolve( userdata: ?*anyopaque, - name: *const Io.net.Interface.Name, -) Io.net.Interface.Name.ResolveError!Io.net.Interface { + name: *const net.Interface.Name, +) net.Interface.Name.ResolveError!net.Interface { const pool: *Pool = @ptrCast(@alignCast(userdata)); if (native_os == .linux) { @@ -2542,7 +2543,7 @@ fn netInterfaceNameResolve( @panic("unimplemented"); } -fn netInterfaceName(userdata: ?*anyopaque, interface: Io.net.Interface) Io.net.Interface.NameError!Io.net.Interface.Name { +fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { const pool: *Pool = @ptrCast(@alignCast(userdata)); try pool.checkCancel(); @@ -2573,14 +2574,14 @@ const UnixAddress = extern union { un: posix.sockaddr.un, }; -fn posixAddressFamily(a: *const Io.net.IpAddress) posix.sa_family_t { +fn posixAddressFamily(a: *const net.IpAddress) posix.sa_family_t { return switch (a.*) { .ip4 => posix.AF.INET, .ip6 => posix.AF.INET6, }; } -fn addressFromPosix(posix_address: *PosixAddress) Io.net.IpAddress { +fn addressFromPosix(posix_address: *PosixAddress) net.IpAddress { return switch (posix_address.any.family) { posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, @@ -2588,7 +2589,7 @@ fn addressFromPosix(posix_address: *PosixAddress) Io.net.IpAddress { }; } -fn addressToPosix(a: *const Io.net.IpAddress, storage: *PosixAddress) posix.socklen_t { +fn addressToPosix(a: *const net.IpAddress, storage: *PosixAddress) posix.socklen_t { return switch (a.*) { .ip4 => |ip4| { storage.in = address4ToPosix(ip4); @@ -2601,21 +2602,21 @@ fn addressToPosix(a: *const Io.net.IpAddress, storage: *PosixAddress) posix.sock }; } -fn addressUnixToPosix(a: *const Io.net.UnixAddress, storage: *UnixAddress) posix.socklen_t { +fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.socklen_t { @memcpy(storage.un.path[0..a.path.len], a.path); storage.un.family = posix.AF.UNIX; storage.un.path[a.path.len] = 0; return @sizeOf(posix.sockaddr.un); } -fn address4FromPosix(in: *posix.sockaddr.in) Io.net.Ip4Address { +fn address4FromPosix(in: *posix.sockaddr.in) net.Ip4Address { return .{ .port = std.mem.bigToNative(u16, in.port), .bytes = @bitCast(in.addr), }; } -fn address6FromPosix(in6: *posix.sockaddr.in6) Io.net.Ip6Address { +fn address6FromPosix(in6: *posix.sockaddr.in6) net.Ip6Address { return .{ .port = std.mem.bigToNative(u16, in6.port), .bytes = in6.addr, @@ -2624,14 +2625,14 @@ fn address6FromPosix(in6: *posix.sockaddr.in6) Io.net.Ip6Address { }; } -fn address4ToPosix(a: Io.net.Ip4Address) posix.sockaddr.in { +fn address4ToPosix(a: net.Ip4Address) posix.sockaddr.in { return .{ .port = std.mem.nativeToBig(u16, a.port), .addr = @bitCast(a.bytes), }; } -fn address6ToPosix(a: *const Io.net.Ip6Address) posix.sockaddr.in6 { +fn address6ToPosix(a: *const net.Ip6Address) posix.sockaddr.in6 { return .{ .port = std.mem.nativeToBig(u16, a.port), .flowinfo = a.flow, @@ -2647,7 +2648,7 @@ fn errnoBug(err: posix.E) Io.UnexpectedError { } } -fn posixSocketMode(mode: Io.net.Socket.Mode) u32 { +fn posixSocketMode(mode: net.Socket.Mode) u32 { return switch (mode) { .stream => posix.SOCK.STREAM, .dgram => posix.SOCK.DGRAM, @@ -2657,7 +2658,7 @@ fn posixSocketMode(mode: Io.net.Socket.Mode) u32 { }; } -fn posixProtocol(protocol: ?Io.net.Protocol) u32 { +fn posixProtocol(protocol: ?net.Protocol) u32 { return @intFromEnum(protocol orelse return 0); } From 35ce907c06d5758adab276927ad8dbe730d6130d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 14 Oct 2025 20:59:16 -0700 Subject: [PATCH 107/244] std.Io.net.HostName: move lookup to the interface Unfortunately this can't be implemented "above the vtable" because various operating systems don't provide low level DNS resolution primitives such as just putting the list of nameservers in a file. Without libc on Linux it works great though! Anyway this also changes the API to be based on Io.Queue. By using a large enough buffer, reusable code can be written that does not require concurrent, yet takes advantage of responding to DNS queries as they come in. I sketched out a new implementation of `HostName.connect` to demonstrate this, but it will require an additional API (`Io.Select`) to be implemented in a future commit. This commit also introduces "uncancelable" variants for mutex locking, waiting on a condition, and putting items into a queue. --- BRANCH_TODO | 3 + lib/std/Io.zig | 109 +++--- lib/std/Io/EventLoop.zig | 2 +- lib/std/Io/File.zig | 2 +- lib/std/Io/Threaded.zig | 692 +++++++++++++++++++++++++++++++++--- lib/std/Io/net.zig | 5 +- lib/std/Io/net/HostName.zig | 550 +++------------------------- lib/std/posix.zig | 30 +- lib/std/zig/system.zig | 2 +- 9 files changed, 778 insertions(+), 617 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index c270307737..53b157f41e 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -1,3 +1,4 @@ +* Threaded: rename Pool to Threaded * Threaded: finish linux impl (all tests passing) * Threaded: finish macos impl * Threaded: finish windows impl @@ -14,4 +15,6 @@ * move fs.File.Writer to Io * add non-blocking flag to net and fs operations, handle EAGAIN * finish moving std.fs to Io +* migrate child process into std.Io +* eliminate std.Io.poll (it should be replaced by "select" functionality) * finish moving all of std.posix into Threaded diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 4a8f65060e..41aebcb712 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -649,9 +649,11 @@ pub const VTable = struct { select: *const fn (?*anyopaque, futures: []const *AnyFuture) usize, mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void, + mutexLockUncancelable: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, mutexUnlock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, + conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, @@ -686,6 +688,7 @@ pub const VTable = struct { netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, + netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) void, }; pub const Cancelable = error{ @@ -1030,7 +1033,7 @@ pub const Group = struct { } }; -pub const Mutex = if (true) struct { +pub const Mutex = struct { state: State, pub const State = enum(usize) { @@ -1073,54 +1076,32 @@ pub const Mutex = if (true) struct { return io.vtable.mutexLock(io.userdata, prev_state, mutex); } + /// Same as `lock` but cannot be canceled. + pub fn lockUncancelable(mutex: *Mutex, io: std.Io) void { + const prev_state: State = @enumFromInt(@atomicRmw( + usize, + @as(*usize, @ptrCast(&mutex.state)), + .And, + ~@intFromEnum(State.unlocked), + .acquire, + )); + if (prev_state.isUnlocked()) { + @branchHint(.likely); + return; + } + return io.vtable.mutexLockUncancelable(io.userdata, prev_state, mutex); + } + pub fn unlock(mutex: *Mutex, io: std.Io) void { const prev_state = @cmpxchgWeak(State, &mutex.state, .locked_once, .unlocked, .release, .acquire) orelse { @branchHint(.likely); return; }; - std.debug.assert(prev_state != .unlocked); // mutex not locked + assert(prev_state != .unlocked); // mutex not locked return io.vtable.mutexUnlock(io.userdata, prev_state, mutex); } -} else struct { - state: std.atomic.Value(u32), - - pub const State = void; - - pub const init: Mutex = .{ .state = .init(unlocked) }; - - pub const unlocked: u32 = 0b00; - pub const locked: u32 = 0b01; - pub const contended: u32 = 0b11; // must contain the `locked` bit for x86 optimization below - - pub fn tryLock(m: *Mutex) bool { - // On x86, use `lock bts` instead of `lock cmpxchg` as: - // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048 - // - `lock bts` is smaller instruction-wise which makes it better for inlining - if (builtin.target.cpu.arch.isX86()) { - const locked_bit = @ctz(locked); - return m.state.bitSet(locked_bit, .acquire) == 0; - } - - // Acquire barrier ensures grabbing the lock happens before the critical section - // and that the previous lock holder's critical section happens before we grab the lock. - return m.state.cmpxchgWeak(unlocked, locked, .acquire, .monotonic) == null; - } - - /// Avoids the vtable for uncontended locks. - pub fn lock(m: *Mutex, io: Io) Cancelable!void { - if (!m.tryLock()) { - @branchHint(.unlikely); - try io.vtable.mutexLock(io.userdata, {}, m); - } - } - - pub fn unlock(m: *Mutex, io: Io) void { - io.vtable.mutexUnlock(io.userdata, {}, m); - } }; -/// Supports exactly 1 waiter. More than 1 simultaneous wait on the same -/// condition is illegal. pub const Condition = struct { state: u64 = 0, @@ -1128,6 +1109,10 @@ pub const Condition = struct { return io.vtable.conditionWait(io.userdata, cond, mutex); } + pub fn waitUncancelable(cond: *Condition, io: Io, mutex: *Mutex) void { + return io.vtable.conditionWaitUncancelable(io.userdata, cond, mutex); + } + pub fn signal(cond: *Condition, io: Io) void { io.vtable.conditionWake(io.userdata, cond, .one); } @@ -1137,9 +1122,9 @@ pub const Condition = struct { } pub const Wake = enum { - /// wake up only one thread + /// Wake up only one thread. one, - /// wake up all thread + /// Wake up all threads. all, }; }; @@ -1180,10 +1165,24 @@ pub const TypeErasedQueue = struct { pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) Cancelable!usize { assert(elements.len >= min); - + if (elements.len == 0) return 0; try q.mutex.lock(io); defer q.mutex.unlock(io); + return putLocked(q, io, elements, min, false); + } + /// Same as `put` but cannot be canceled. + pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize { + assert(elements.len >= min); + if (elements.len == 0) return 0; + q.mutex.lockUncancelable(io); + defer q.mutex.unlock(io); + return putLocked(q, io, elements, min, true) catch |err| switch (err) { + error.Canceled => unreachable, + }; + } + + fn putLocked(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize, uncancelable: bool) Cancelable!usize { // Getters have first priority on the data, and only when the getters // queue is empty do we start populating the buffer. @@ -1226,7 +1225,10 @@ pub const TypeErasedQueue = struct { var pending: Put = .{ .remaining = remaining, .condition = .{}, .node = .{} }; q.putters.append(&pending.node); - try pending.condition.wait(io, &q.mutex); + if (uncancelable) + pending.condition.waitUncancelable(io, &q.mutex) + else + try pending.condition.wait(io, &q.mutex); remaining = pending.remaining; } } @@ -1347,6 +1349,16 @@ pub fn Queue(Elem: type) type { return @divExact(try q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); } + /// Same as `put` but blocks until all elements have been added to the queue. + pub fn putAll(q: *@This(), io: Io, elements: []const Elem) Cancelable!void { + assert(try q.put(io, elements, elements.len) == elements.len); + } + + /// Same as `put` but cannot be interrupted. + pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize { + return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); + } + /// Receives elements from the beginning of the queue. The function /// returns when at least `min` elements have been populated inside /// `buffer`. @@ -1362,11 +1374,20 @@ pub fn Queue(Elem: type) type { assert(try q.put(io, &.{item}, 1) == 1); } + pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void { + assert(q.putUncancelable(io, &.{item}, 1) == 1); + } + pub fn getOne(q: *@This(), io: Io) Cancelable!Elem { var buf: [1]Elem = undefined; assert(try q.get(io, &buf, 1) == 1); return buf[0]; } + + /// Returns buffer length in `Elem` units. + pub fn capacity(q: *const @This()) usize { + return @divExact(q.type_erased.buffer.len, @sizeOf(Elem)); + } }; } diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/EventLoop.zig index 4cb5745e31..84ea0035ae 100644 --- a/lib/std/Io/EventLoop.zig +++ b/lib/std/Io/EventLoop.zig @@ -1410,7 +1410,7 @@ fn pread(userdata: ?*anyopaque, file: Io.File, buffer: []u8, offset: std.posix.o .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index c2ceaa95b7..7bd3669e2e 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -153,7 +153,7 @@ pub const ReadStreamingError = error{ IsDir, BrokenPipe, ConnectionResetByPeer, - ConnectionTimedOut, + Timeout, NotOpenForReading, SocketUnconnected, /// This error occurs when no global event loop is configured, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 4ecc6e7b31..f0d7f3ea4b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -8,6 +8,8 @@ const windows = std.os.windows; const std = @import("../std.zig"); const Io = std.Io; const net = std.Io.net; +const HostName = std.Io.net.HostName; +const IpAddress = std.Io.net.IpAddress; const Allocator = std.mem.Allocator; const assert = std.debug.assert; const posix = std.posix; @@ -156,9 +158,11 @@ pub fn io(pool: *Pool) Io { .groupCancel = groupCancel, .mutexLock = mutexLock, + .mutexLockUncancelable = mutexLockUncancelable, .mutexUnlock = mutexUnlock, .conditionWait = conditionWait, + .conditionWaitUncancelable = conditionWaitUncancelable, .conditionWake = conditionWake, .dirMake = switch (builtin.os.tag) { @@ -235,6 +239,7 @@ pub fn io(pool: *Pool) Io { .netReceive = netReceive, .netInterfaceNameResolve = netInterfaceNameResolve, .netInterfaceName = netInterfaceName, + .netLookup = netLookup, }, }; } @@ -653,26 +658,63 @@ fn checkCancel(pool: *Pool) error{Canceled}!void { if (cancelRequested(pool)) return error.Canceled; } -fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) error{Canceled}!void { - _ = userdata; +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); if (prev_state == .contended) { - std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + try pool.checkCancel(); + futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } - while (@atomicRmw( - Io.Mutex.State, - &mutex.state, - .Xchg, - .contended, - .acquire, - ) != .unlocked) { - std.Thread.Futex.wait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { + try pool.checkCancel(); + futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } } + +fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + _ = userdata; + if (prev_state == .contended) { + futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } + while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { + futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + } +} + fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { _ = userdata; _ = prev_state; if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { - std.Thread.Futex.wake(@ptrCast(&mutex.state), 1); + futexWake(@ptrCast(&mutex.state), 1); + } +} + +fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const pool_io = pool.io(); + comptime assert(@TypeOf(cond.state) == u64); + const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); + const cond_state = &ints[0]; + const cond_epoch = &ints[1]; + const one_waiter = 1; + const waiter_mask = 0xffff; + const one_signal = 1 << 16; + const signal_mask = 0xffff << 16; + var epoch = cond_epoch.load(.acquire); + var state = cond_state.fetchAdd(one_waiter, .monotonic); + assert(state & waiter_mask != waiter_mask); + state += one_waiter; + + mutex.unlock(pool_io); + defer mutex.lockUncancelable(pool_io); + + while (true) { + futexWait(cond_epoch, epoch); + epoch = cond_epoch.load(.acquire); + state = cond_state.load(.monotonic); + while (state & signal_mask != 0) { + const new_state = state - one_waiter - one_signal; + state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; + } } } @@ -702,20 +744,18 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I state += one_waiter; mutex.unlock(pool.io()); - defer mutex.lock(pool.io()) catch @panic("TODO"); - - var futex_deadline = std.Thread.Futex.Deadline.init(null); + defer mutex.lockUncancelable(pool.io()); while (true) { - futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { - error.Timeout => unreachable, - }; + try pool.checkCancel(); + futexWait(cond_epoch, epoch); epoch = cond_epoch.load(.acquire); state = cond_state.load(.monotonic); - // Try to wake up by consuming a signal and decremented the waiter we added previously. - // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. + // Try to wake up by consuming a signal and decremented the waiter we + // added previously. Acquire barrier ensures code before the wake() + // which added the signal happens before we decrement it and return. while (state & signal_mask != 0) { const new_state = state - one_waiter - one_signal; state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; @@ -740,8 +780,10 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. const signals = (state & signal_mask) / one_signal; // Reserves which waiters to wake up by incrementing the signals count. - // Therefore, the signals count is always less than or equal to the waiters count. - // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters. + // Therefore, the signals count is always less than or equal to the + // waiters count. We don't need to Futex.wake if there's nothing to + // wake up or if other wake() threads have reserved to wake up the + // current waiters. const wakeable = waiters - signals; if (wakeable == 0) { return; @@ -752,16 +794,23 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. .all => wakeable, }; - // Reserve the amount of waiters to wake by incrementing the signals count. - // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. + // Reserve the amount of waiters to wake by incrementing the signals + // count. Release barrier ensures code before the wake() happens before + // the signal it posted and consumed by the wait() threads. const new_state = state + (one_signal * to_wake); state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { // Wake up the waiting threads we reserved above by changing the epoch value. - // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it. - // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption. // - // Release barrier ensures the signal being added to the state happens before the epoch is changed. - // If not, the waiting thread could potentially deadlock from missing both the state and epoch change: + // A waiting thread could miss a wake up if *exactly* ((1<<32)-1) + // wake()s happen between it observing the epoch and sleeping on + // it. This is very unlikely due to how many precise amount of + // Futex.wake() calls that would be between the waiting thread's + // potential preemption. + // + // Release barrier ensures the signal being added to the state + // happens before the epoch is changed. If not, the waiting thread + // could potentially deadlock from missing both the state and epoch + // change: // // - T2: UPDATE(&epoch, 1) (reordered before the state change) // - T1: e = LOAD(&epoch) @@ -769,7 +818,7 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) _ = cond_epoch.fetchAdd(1, .release); - std.Thread.Futex.wake(cond_epoch, to_wake); + futexWake(cond_epoch, to_wake); return; }; } @@ -1298,7 +1347,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return posix.unexpectedErrno(err), } @@ -1321,7 +1370,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, else => |err| return posix.unexpectedErrno(err), } } @@ -1420,7 +1469,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1446,7 +1495,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1693,9 +1742,9 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { fn netListenIpPosix( userdata: ?*anyopaque, - address: net.IpAddress, - options: net.IpAddress.ListenOptions, -) net.IpAddress.ListenError!net.Server { + address: IpAddress, + options: IpAddress.ListenOptions, +) IpAddress.ListenError!net.Server { const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(&address); const socket_fd = try openSocketPosix(pool, family, .{ @@ -1831,7 +1880,7 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => |err| return errnoBug(err), .PROTOTYPE => |err| return errnoBug(err), - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .CONNABORTED => |err| return errnoBug(err), .ACCES => return error.AccessDenied, .PERM => |err| return errnoBug(err), @@ -1904,9 +1953,9 @@ fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, optio fn netConnectIpPosix( userdata: ?*anyopaque, - address: *const net.IpAddress, - options: net.IpAddress.ConnectOptions, -) net.IpAddress.ConnectError!net.Stream { + address: *const IpAddress, + options: IpAddress.ConnectOptions, +) IpAddress.ConnectError!net.Stream { if (options.timeout != .none) @panic("TODO"); const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); @@ -1941,9 +1990,9 @@ fn netConnectUnix( fn netBindIpPosix( userdata: ?*anyopaque, - address: *const net.IpAddress, - options: net.IpAddress.BindOptions, -) net.IpAddress.BindError!net.Socket { + address: *const IpAddress, + options: IpAddress.BindOptions, +) IpAddress.BindError!net.Socket { const pool: *Pool = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); const socket_fd = try openSocketPosix(pool, family, options); @@ -1958,7 +2007,7 @@ fn netBindIpPosix( }; } -fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: net.IpAddress.BindOptions) !posix.socket_t { +fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: IpAddress.BindOptions) !posix.socket_t { const mode = posixSocketMode(options.mode); const protocol = posixProtocol(options.protocol); const socket_fd = while (true) { @@ -2081,7 +2130,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return posix.unexpectedErrno(err), } @@ -2102,7 +2151,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .PIPE => return error.BrokenPipe, .NETDOWN => return error.NetworkDown, else => |err| return posix.unexpectedErrno(err), @@ -2563,6 +2612,118 @@ fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interfa @panic("unimplemented"); } +fn netLookup( + userdata: ?*anyopaque, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) void { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + const pool_io = pool.io(); + resolved.putOneUncancelable(pool_io, .{ .end = netLookupFallible(pool, host_name, resolved, options) }); +} + +fn netLookupFallible( + pool: *Pool, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) !void { + const pool_io = pool.io(); + const name = host_name.bytes; + assert(name.len <= HostName.max_len); + + if (is_windows) { + // TODO use GetAddrInfoExW / GetAddrInfoExCancel + @compileError("TODO"); + } + + // On Linux, glibc provides getaddrinfo_a which is capable of supporting our semantics. + // However, musl's POSIX-compliant getaddrinfo is not, so we bypass it. + + if (builtin.target.isGnuLibC()) { + // TODO use getaddrinfo_a / gai_cancel + } + + if (native_os == .linux) { + if (options.family != .ip4) { + if (IpAddress.parseIp6(name, options.port)) |addr| { + try resolved.putAll(pool_io, &.{ + .{ .address = addr }, + .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, + }); + return; + } else |_| {} + } + + if (options.family != .ip6) { + if (IpAddress.parseIp4(name, options.port)) |addr| { + try resolved.putAll(pool_io, &.{ + .{ .address = addr }, + .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, + }); + } else |_| {} + } + + lookupHosts(pool, host_name, resolved, options) catch |err| switch (err) { + error.UnknownHostName => {}, + else => |e| return e, + }; + + // RFC 6761 Section 6.3.3 + // Name resolution APIs and libraries SHOULD recognize + // localhost names as special and SHOULD always return the IP + // loopback address for address queries and negative responses + // for all other query types. + + // Check for equal to "localhost(.)" or ends in ".localhost(.)" + const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; + if (std.mem.endsWith(u8, name, localhost) and + (name.len == localhost.len or name[name.len - localhost.len] == '.')) + { + var results_buffer: [3]HostName.LookupResult = undefined; + var results_index: usize = 0; + if (options.family != .ip4) { + results_buffer[results_index] = .{ .address = .{ .ip6 = .loopback(options.port) } }; + results_index += 1; + } + if (options.family != .ip6) { + results_buffer[results_index] = .{ .address = .{ .ip4 = .loopback(options.port) } }; + results_index += 1; + } + const canon_name = "localhost"; + const canon_name_dest = options.canonical_name_buffer[0..canon_name.len]; + canon_name_dest.* = canon_name.*; + results_buffer[results_index] = .{ .canonical_name = .{ .bytes = canon_name_dest } }; + results_index += 1; + try resolved.putAll(pool_io, results_buffer[0..results_index]); + return; + } + + return lookupDnsSearch(pool, host_name, resolved, options); + } + + if (native_os == .openbsd) { + // TODO use getaddrinfo_async / asr_abort + } + + if (native_os == .freebsd) { + // TODO use dnsres_getaddrinfo + } + + if (native_os.isDarwin()) { + // TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution + } + + if (builtin.link_libc) { + // This operating system lacks a way to resolve asynchronously. We are + // stuck with getaddrinfo. + @compileError("TODO"); + } + + return error.OptionUnsupported; +} + const PosixAddress = extern union { any: posix.sockaddr, in: posix.sockaddr.in, @@ -2574,14 +2735,14 @@ const UnixAddress = extern union { un: posix.sockaddr.un, }; -fn posixAddressFamily(a: *const net.IpAddress) posix.sa_family_t { +fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t { return switch (a.*) { .ip4 => posix.AF.INET, .ip6 => posix.AF.INET6, }; } -fn addressFromPosix(posix_address: *PosixAddress) net.IpAddress { +fn addressFromPosix(posix_address: *PosixAddress) IpAddress { return switch (posix_address.any.family) { posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, @@ -2589,7 +2750,7 @@ fn addressFromPosix(posix_address: *PosixAddress) net.IpAddress { }; } -fn addressToPosix(a: *const net.IpAddress, storage: *PosixAddress) posix.socklen_t { +fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t { return switch (a.*) { .ip4 => |ip4| { storage.in = address4ToPosix(ip4); @@ -2789,3 +2950,436 @@ fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNa buffer[file_path.len] = 0; return buffer[0..file_path.len :0]; } + +fn lookupDnsSearch( + pool: *Pool, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) HostName.LookupError!void { + const pool_io = pool.io(); + const rc = HostName.ResolvConf.init(pool_io) catch return error.ResolvConfParseFailed; + + // Count dots, suppress search when >=ndots or name ends in + // a dot, which is an explicit request for global scope. + const dots = std.mem.countScalar(u8, host_name.bytes, '.'); + const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len; + const search = rc.search_buffer[0..search_len]; + + var canon_name = host_name.bytes; + + // Strip final dot for canon, fail if multiple trailing dots. + if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; + if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; + + // Name with search domain appended is set up in `canon_name`. This + // both provides the desired default canonical name (if the requested + // name is not a CNAME record) and serves as a buffer for passing the + // full requested name to `lookupDns`. + @memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name); + options.canonical_name_buffer[canon_name.len] = '.'; + var it = std.mem.tokenizeAny(u8, search, " \t"); + while (it.next()) |token| { + @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token); + const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len]; + if (lookupDns(pool, lookup_canon_name, &rc, resolved, options)) |result| { + return result; + } else |err| switch (err) { + error.UnknownHostName => continue, + else => |e| return e, + } + } + + const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len]; + return lookupDns(pool, lookup_canon_name, &rc, resolved, options); +} + +fn lookupDns( + pool: *Pool, + lookup_canon_name: []const u8, + rc: *const HostName.ResolvConf, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) HostName.LookupError!void { + const pool_io = pool.io(); + const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ + .{ .af = .ip6, .rr = std.posix.RR.A }, + .{ .af = .ip4, .rr = std.posix.RR.AAAA }, + }; + var query_buffers: [2][280]u8 = undefined; + var answer_buffer: [2 * 512]u8 = undefined; + var queries_buffer: [2][]const u8 = undefined; + var answers_buffer: [2][]const u8 = undefined; + var nq: usize = 0; + var answer_buffer_i: usize = 0; + + for (family_records) |fr| { + if (options.family != fr.af) { + const entropy = std.crypto.random.array(u8, 2); + const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy); + queries_buffer[nq] = query_buffers[nq][0..len]; + nq += 1; + } + } + + var ip4_mapped: [HostName.ResolvConf.max_nameservers]IpAddress = undefined; + var any_ip6 = false; + for (rc.nameservers(), &ip4_mapped) |*ns, *m| { + m.* = .{ .ip6 = .fromAny(ns.*) }; + any_ip6 = any_ip6 or ns.* == .ip6; + } + var socket = s: { + if (any_ip6) ip6: { + const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; + const socket = ip6_addr.bind(pool_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { + error.AddressFamilyUnsupported => break :ip6, + else => |e| return e, + }; + break :s socket; + } + any_ip6 = false; + const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; + const socket = try ip4_addr.bind(pool_io, .{ .mode = .dgram }); + break :s socket; + }; + defer socket.close(pool_io); + + const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); + const queries = queries_buffer[0..nq]; + const answers = answers_buffer[0..queries.len]; + var answers_remaining = answers.len; + for (answers) |*answer| answer.len = 0; + + // boot clock is chosen because time the computer is suspended should count + // against time spent waiting for external messages to arrive. + const clock: Io.Clock = .boot; + var now_ts = try clock.now(pool_io); + const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); + const attempt_duration: Io.Duration = .{ + .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, + }; + + send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(pool_io)) { + const max_messages = queries_buffer.len * HostName.ResolvConf.max_nameservers; + { + var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; + var message_i: usize = 0; + for (queries, answers) |query, *answer| { + if (answer.len != 0) continue; + for (mapped_nameservers) |*ns| { + message_buffer[message_i] = .{ + .address = ns, + .data_ptr = query.ptr, + .data_len = query.len, + }; + message_i += 1; + } + } + _ = netSend(pool, socket.handle, message_buffer[0..message_i], .{}); + } + + const timeout: Io.Timeout = .{ .deadline = .{ + .raw = now_ts.addDuration(attempt_duration), + .clock = clock, + } }; + + while (true) { + var message_buffer: [max_messages]Io.net.IncomingMessage = undefined; + const buf = answer_buffer[answer_buffer_i..]; + const recv_err, const recv_n = socket.receiveManyTimeout(pool_io, &message_buffer, buf, .{}, timeout); + for (message_buffer[0..recv_n]) |*received_message| { + const reply = received_message.data; + // Ignore non-identifiable packets. + if (reply.len < 4) continue; + + // Ignore replies from addresses we didn't send to. + const ns = for (mapped_nameservers) |*ns| { + if (received_message.from.eql(ns)) break ns; + } else { + continue; + }; + + // Find which query this answer goes with, if any. + const query, const answer = for (queries, answers) |query, *answer| { + if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer }; + } else { + continue; + }; + if (answer.len != 0) continue; + + // Only accept positive or negative responses; retry immediately on + // server failure, and ignore all other codes such as refusal. + switch (reply[3] & 15) { + 0, 3 => { + answer.* = reply; + answer_buffer_i += reply.len; + answers_remaining -= 1; + if (answer_buffer.len - answer_buffer_i == 0) break :send; + if (answers_remaining == 0) break :send; + }, + 2 => { + var retry_message: Io.net.OutgoingMessage = .{ + .address = ns, + .data_ptr = query.ptr, + .data_len = query.len, + }; + _ = netSend(pool, socket.handle, (&retry_message)[0..1], .{}); + continue; + }, + else => continue, + } + } + if (recv_err) |err| switch (err) { + error.Canceled => return error.Canceled, + error.Timeout => continue :send, + else => continue, + }; + } + } else { + return error.NameServerFailure; + } + + var addresses_len: usize = 0; + var canonical_name: ?HostName = null; + + for (answers) |answer| { + var it = HostName.DnsResponse.init(answer) catch { + // TODO accept a diagnostics struct and append warnings + continue; + }; + while (it.next() catch { + // TODO accept a diagnostics struct and append warnings + continue; + }) |record| switch (record.rr) { + std.posix.RR.A => { + const data = record.packet[record.data_off..][0..record.data_len]; + if (data.len != 4) return error.InvalidDnsARecord; + try resolved.putOne(pool_io, .{ .address = .{ .ip4 = .{ + .bytes = data[0..4].*, + .port = options.port, + } } }); + addresses_len += 1; + }, + std.posix.RR.AAAA => { + const data = record.packet[record.data_off..][0..record.data_len]; + if (data.len != 16) return error.InvalidDnsAAAARecord; + try resolved.putOne(pool_io, .{ .address = .{ .ip6 = .{ + .bytes = data[0..16].*, + .port = options.port, + } } }); + addresses_len += 1; + }, + std.posix.RR.CNAME => { + _, canonical_name = HostName.expand(record.packet, record.data_off, options.canonical_name_buffer) catch + return error.InvalidDnsCnameRecord; + }, + else => continue, + }; + } + + try resolved.putOne(pool_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } }); + if (addresses_len == 0) return error.NameServerFailure; +} + +fn lookupHosts( + pool: *Pool, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) !void { + const pool_io = pool.io(); + const file = Io.File.openAbsolute(pool_io, "/etc/hosts", .{}) catch |err| switch (err) { + error.FileNotFound, + error.NotDir, + error.AccessDenied, + => return error.UnknownHostName, + + error.Canceled => |e| return e, + + else => { + // TODO populate optional diagnostic struct + return error.DetectingNetworkConfigurationFailed; + }, + }; + defer file.close(pool_io); + + var line_buf: [512]u8 = undefined; + var file_reader = file.reader(pool_io, &line_buf); + return lookupHostsReader(pool, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) { + error.ReadFailed => switch (file_reader.err.?) { + error.Canceled => |e| return e, + else => { + // TODO populate optional diagnostic struct + return error.DetectingNetworkConfigurationFailed; + }, + }, + error.Canceled => |e| return e, + error.UnknownHostName => |e| return e, + }; +} + +fn lookupHostsReader( + pool: *Pool, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, + reader: *Io.Reader, +) error{ ReadFailed, Canceled, UnknownHostName }!void { + const pool_io = pool.io(); + var addresses_len: usize = 0; + var canonical_name: ?HostName = null; + while (true) { + const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { + error.StreamTooLong => { + // Skip lines that are too long. + _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) { + error.EndOfStream => break, + error.ReadFailed => return error.ReadFailed, + }; + continue; + }, + error.ReadFailed => return error.ReadFailed, + error.EndOfStream => break, + }; + reader.toss(1); + var split_it = std.mem.splitScalar(u8, line, '#'); + const no_comment_line = split_it.first(); + + var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t"); + const ip_text = line_it.next() orelse continue; + var first_name_text: ?[]const u8 = null; + while (line_it.next()) |name_text| { + if (std.mem.eql(u8, name_text, host_name.bytes)) { + if (first_name_text == null) first_name_text = name_text; + break; + } + } else continue; + + if (canonical_name == null) { + if (HostName.init(first_name_text.?)) |name_text| { + if (name_text.bytes.len <= options.canonical_name_buffer.len) { + const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len]; + @memcpy(canonical_name_dest, name_text.bytes); + canonical_name = .{ .bytes = canonical_name_dest }; + } + } else |_| {} + } + + if (options.family != .ip6) { + if (IpAddress.parseIp4(ip_text, options.port)) |addr| { + try resolved.putOne(pool_io, .{ .address = addr }); + addresses_len += 1; + } else |_| {} + } + if (options.family != .ip4) { + if (IpAddress.parseIp6(ip_text, options.port)) |addr| { + try resolved.putOne(pool_io, .{ .address = addr }); + addresses_len += 1; + } else |_| {} + } + } + + if (canonical_name) |canon_name| try resolved.putOne(pool_io, .{ .canonical_name = canon_name }); + if (addresses_len == 0) return error.UnknownHostName; +} + +/// Writes DNS resolution query packet data to `w`; at most 280 bytes. +fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize { + // This implementation is ported from musl libc. + // A more idiomatic "ziggy" implementation would be welcome. + var name = dname; + if (std.mem.endsWith(u8, name, ".")) name.len -= 1; + assert(name.len <= 253); + const n = 17 + name.len + @intFromBool(name.len != 0); + + // Construct query template - ID will be filled later + q[0..2].* = entropy; + @memset(q[2..n], 0); + q[2] = @as(u8, op) * 8 + 1; + q[5] = 1; + @memcpy(q[13..][0..name.len], name); + var i: usize = 13; + var j: usize = undefined; + while (q[i] != 0) : (i = j + 1) { + j = i; + while (q[j] != 0 and q[j] != '.') : (j += 1) {} + // TODO determine the circumstances for this and whether or + // not this should be an error. + if (j - i - 1 > 62) unreachable; + q[i - 1] = @intCast(j - i); + } + q[i + 1] = ty; + q[i + 3] = class; + return n; +} + +fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) HostName { + const dest = canonical_name_buffer[0..name.len]; + @memcpy(dest, name); + return .{ .bytes = dest }; +} + +pub fn futexWait(ptr: *const std.atomic.Value(u32), expect: u32) void { + @branchHint(.cold); + + if (native_os == .linux) { + const linux = std.os.linux; + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); + if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .INVAL => {}, // possibly timeout overflow + .TIMEDOUT => unreachable, + .FAULT => unreachable, // ptr was invalid + else => unreachable, + }; + return; + } + + @compileError("TODO"); +} + +pub fn futexWaitDuration(ptr: *const std.atomic.Value(u32), expect: u32, timeout: Io.Duration) void { + @branchHint(.cold); + + if (native_os == .linux) { + const linux = std.os.linux; + var ts = timestampToPosix(timeout.toNanoseconds()); + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, &ts); + if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .TIMEDOUT => {}, + .INVAL => {}, // possibly timeout overflow + .FAULT => unreachable, // ptr was invalid + else => unreachable, + }; + return; + } + + @compileError("TODO"); +} + +pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { + @branchHint(.cold); + + if (native_os == .linux) { + const linux = std.os.linux; + const rc = linux.futex_3arg( + &ptr.raw, + .{ .cmd = .WAKE, .private = true }, + @min(max_waiters, std.math.maxInt(i32)), + ); + if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // successful wake up + .INVAL => {}, // invalid futex_wait() on ptr done elsewhere + .FAULT => {}, // pointer became invalid while doing the wake + else => unreachable, + }; + return; + } + + @compileError("TODO"); +} diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index e8aadde38c..53cfee60c5 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -281,7 +281,6 @@ pub const IpAddress = union(enum) { } pub const ConnectError = error{ - AddressInUse, AddressUnavailable, AddressFamilyUnsupported, /// Insufficient memory or other resource internal to the operating system. @@ -291,7 +290,7 @@ pub const IpAddress = union(enum) { ConnectionResetByPeer, HostUnreachable, NetworkUnreachable, - ConnectionTimedOut, + Timeout, /// One of the `ConnectOptions` is not supported by the Io /// implementation. OptionUnsupported, @@ -1165,7 +1164,7 @@ pub const Stream = struct { SystemResources, BrokenPipe, ConnectionResetByPeer, - ConnectionTimedOut, + Timeout, SocketUnconnected, /// The file descriptor does not hold the required rights to read /// from it. diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 4d0df744c4..788c40dd0c 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -63,8 +63,6 @@ pub fn eql(a: HostName, b: HostName) bool { pub const LookupOptions = struct { port: u16, - /// Must have at least length 2. - addresses_buffer: []IpAddress, canonical_name_buffer: *[max_len]u8, /// `null` means either. family: ?IpAddress.Family = null, @@ -81,487 +79,23 @@ pub const LookupError = error{ DetectingNetworkConfigurationFailed, } || Io.Clock.Error || IpAddress.BindError || Io.Cancelable; -pub const LookupResult = struct { - /// How many `LookupOptions.addresses_buffer` elements are populated. - addresses_len: usize, +pub const LookupResult = union(enum) { + address: IpAddress, canonical_name: HostName, - - pub const empty: LookupResult = .{ - .addresses_len = 0, - .canonical_name = undefined, - }; + end: LookupError!void, }; -pub fn lookup(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { - const name = host_name.bytes; - assert(name.len <= max_len); - assert(options.addresses_buffer.len >= 2); - - if (native_os == .windows) @compileError("TODO"); - if (builtin.link_libc) @compileError("TODO"); - if (native_os == .linux) { - if (options.family != .ip6) { - if (IpAddress.parseIp4(name, options.port)) |addr| { - options.addresses_buffer[0] = addr; - return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) }; - } else |_| {} - } - if (options.family != .ip4) { - if (IpAddress.parseIp6(name, options.port)) |addr| { - options.addresses_buffer[0] = addr; - return .{ .addresses_len = 1, .canonical_name = copyCanon(options.canonical_name_buffer, name) }; - } else |_| {} - } - { - const result = try lookupHosts(host_name, io, options); - if (result.addresses_len > 0) return sortLookupResults(options, result); - } - { - // RFC 6761 Section 6.3.3 - // Name resolution APIs and libraries SHOULD recognize - // localhost names as special and SHOULD always return the IP - // loopback address for address queries and negative responses - // for all other query types. - - // Check for equal to "localhost(.)" or ends in ".localhost(.)" - const localhost = if (name[name.len - 1] == '.') "localhost." else "localhost"; - if (std.mem.endsWith(u8, name, localhost) and - (name.len == localhost.len or name[name.len - localhost.len] == '.')) - { - var i: usize = 0; - if (options.family != .ip6) { - options.addresses_buffer[i] = .{ .ip4 = .loopback(options.port) }; - i += 1; - } - if (options.family != .ip4) { - options.addresses_buffer[i] = .{ .ip6 = .loopback(options.port) }; - i += 1; - } - const canon_name = "localhost"; - const canon_name_dest = options.canonical_name_buffer[0..canon_name.len]; - canon_name_dest.* = canon_name.*; - return sortLookupResults(options, .{ - .addresses_len = i, - .canonical_name = .{ .bytes = canon_name_dest }, - }); - } - } - { - const result = try lookupDnsSearch(host_name, io, options); - if (result.addresses_len > 0) return sortLookupResults(options, result); - } - return error.UnknownHostName; - } - @compileError("unimplemented"); -} - -fn sortLookupResults(options: LookupOptions, result: LookupResult) !LookupResult { - const addresses = options.addresses_buffer[0..result.addresses_len]; - // No further processing is needed if there are fewer than 2 results or - // if there are only IPv4 results. - if (addresses.len < 2) return result; - const all_ip4 = for (addresses) |a| switch (a) { - .ip4 => continue, - .ip6 => break false, - } else true; - if (all_ip4) return result; - - // RFC 3484/6724 describes how destination address selection is - // supposed to work. However, to implement it requires making a bunch - // of networking syscalls, which is unnecessarily high latency, - // especially if implemented serially. Furthermore, rules 3, 4, and 7 - // have excessive runtime and code size cost and dubious benefit. - // - // Therefore, this logic sorts only using values available without - // doing any syscalls, relying on the calling code to have a - // meta-strategy such as attempting connection to multiple results at - // once and keeping the fastest response while canceling the others. - - const S = struct { - pub fn lessThan(s: @This(), lhs: IpAddress, rhs: IpAddress) bool { - return sortKey(s, lhs) < sortKey(s, rhs); - } - - fn sortKey(s: @This(), a: IpAddress) i32 { - _ = s; - var da6: Ip6Address = .{ - .port = 65535, - .bytes = undefined, - }; - switch (a) { - .ip6 => |ip6| { - da6.bytes = ip6.bytes; - da6.interface = ip6.interface; - }, - .ip4 => |ip4| { - da6.bytes[0..12].* = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff".*; - da6.bytes[12..].* = ip4.bytes; - }, - } - const da6_scope: i32 = da6.scope(); - const da6_prec: i32 = da6.policy().prec; - var key: i32 = 0; - key |= da6_prec << 20; - key |= (15 - da6_scope) << 16; - return key; - } - }; - std.mem.sort(IpAddress, addresses, @as(S, .{}), S.lessThan); - return result; -} - -fn lookupDnsSearch(host_name: HostName, io: Io, options: LookupOptions) LookupError!LookupResult { - const rc = ResolvConf.init(io) catch return error.ResolvConfParseFailed; - - // Count dots, suppress search when >=ndots or name ends in - // a dot, which is an explicit request for global scope. - const dots = std.mem.countScalar(u8, host_name.bytes, '.'); - const search_len = if (dots >= rc.ndots or std.mem.endsWith(u8, host_name.bytes, ".")) 0 else rc.search_len; - const search = rc.search_buffer[0..search_len]; - - var canon_name = host_name.bytes; - - // Strip final dot for canon, fail if multiple trailing dots. - if (std.mem.endsWith(u8, canon_name, ".")) canon_name.len -= 1; - if (std.mem.endsWith(u8, canon_name, ".")) return error.UnknownHostName; - - // Name with search domain appended is set up in `canon_name`. This - // both provides the desired default canonical name (if the requested - // name is not a CNAME record) and serves as a buffer for passing the - // full requested name to `lookupDns`. - @memcpy(options.canonical_name_buffer[0..canon_name.len], canon_name); - options.canonical_name_buffer[canon_name.len] = '.'; - var it = std.mem.tokenizeAny(u8, search, " \t"); - while (it.next()) |token| { - @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token); - const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len]; - const result = try lookupDns(io, lookup_canon_name, &rc, options); - if (result.addresses_len > 0) return sortLookupResults(options, result); - } - - const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len]; - return lookupDns(io, lookup_canon_name, &rc, options); -} - -fn lookupDns(io: Io, lookup_canon_name: []const u8, rc: *const ResolvConf, options: LookupOptions) LookupError!LookupResult { - const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ - .{ .af = .ip6, .rr = std.posix.RR.A }, - .{ .af = .ip4, .rr = std.posix.RR.AAAA }, - }; - var query_buffers: [2][280]u8 = undefined; - var answer_buffer: [2 * 512]u8 = undefined; - var queries_buffer: [2][]const u8 = undefined; - var answers_buffer: [2][]const u8 = undefined; - var nq: usize = 0; - var answer_buffer_i: usize = 0; - - for (family_records) |fr| { - if (options.family != fr.af) { - const entropy = std.crypto.random.array(u8, 2); - const len = writeResolutionQuery(&query_buffers[nq], 0, lookup_canon_name, 1, fr.rr, entropy); - queries_buffer[nq] = query_buffers[nq][0..len]; - nq += 1; - } - } - - var ip4_mapped: [ResolvConf.max_nameservers]IpAddress = undefined; - var any_ip6 = false; - for (rc.nameservers(), &ip4_mapped) |*ns, *m| { - m.* = .{ .ip6 = .fromAny(ns.*) }; - any_ip6 = any_ip6 or ns.* == .ip6; - } - var socket = s: { - if (any_ip6) ip6: { - const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; - const socket = ip6_addr.bind(io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { - error.AddressFamilyUnsupported => break :ip6, - else => |e| return e, - }; - break :s socket; - } - any_ip6 = false; - const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; - const socket = try ip4_addr.bind(io, .{ .mode = .dgram }); - break :s socket; - }; - defer socket.close(io); - - const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); - const queries = queries_buffer[0..nq]; - const answers = answers_buffer[0..queries.len]; - var answers_remaining = answers.len; - for (answers) |*answer| answer.len = 0; - - // boot clock is chosen because time the computer is suspended should count - // against time spent waiting for external messages to arrive. - const clock: Io.Clock = .boot; - var now_ts = try clock.now(io); - const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); - const attempt_duration: Io.Duration = .{ - .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, - }; - - send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(io)) { - const max_messages = queries_buffer.len * ResolvConf.max_nameservers; - { - var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; - var message_i: usize = 0; - for (queries, answers) |query, *answer| { - if (answer.len != 0) continue; - for (mapped_nameservers) |*ns| { - message_buffer[message_i] = .{ - .address = ns, - .data_ptr = query.ptr, - .data_len = query.len, - }; - message_i += 1; - } - } - _ = io.vtable.netSend(io.userdata, socket.handle, message_buffer[0..message_i], .{}); - } - - const timeout: Io.Timeout = .{ .deadline = .{ - .raw = now_ts.addDuration(attempt_duration), - .clock = clock, - } }; - - while (true) { - var message_buffer: [max_messages]Io.net.IncomingMessage = undefined; - const buf = answer_buffer[answer_buffer_i..]; - const recv_err, const recv_n = socket.receiveManyTimeout(io, &message_buffer, buf, .{}, timeout); - for (message_buffer[0..recv_n]) |*received_message| { - const reply = received_message.data; - // Ignore non-identifiable packets. - if (reply.len < 4) continue; - - // Ignore replies from addresses we didn't send to. - const ns = for (mapped_nameservers) |*ns| { - if (received_message.from.eql(ns)) break ns; - } else { - continue; - }; - - // Find which query this answer goes with, if any. - const query, const answer = for (queries, answers) |query, *answer| { - if (reply[0] == query[0] and reply[1] == query[1]) break .{ query, answer }; - } else { - continue; - }; - if (answer.len != 0) continue; - - // Only accept positive or negative responses; retry immediately on - // server failure, and ignore all other codes such as refusal. - switch (reply[3] & 15) { - 0, 3 => { - answer.* = reply; - answer_buffer_i += reply.len; - answers_remaining -= 1; - if (answer_buffer.len - answer_buffer_i == 0) break :send; - if (answers_remaining == 0) break :send; - }, - 2 => { - var retry_message: Io.net.OutgoingMessage = .{ - .address = ns, - .data_ptr = query.ptr, - .data_len = query.len, - }; - _ = io.vtable.netSend(io.userdata, socket.handle, (&retry_message)[0..1], .{}); - continue; - }, - else => continue, - } - } - if (recv_err) |err| switch (err) { - error.Canceled => return error.Canceled, - error.Timeout => continue :send, - else => continue, - }; - } - } else { - return error.NameServerFailure; - } - - var addresses_len: usize = 0; - var canonical_name: ?HostName = null; - - for (answers) |answer| { - var it = DnsResponse.init(answer) catch { - // TODO accept a diagnostics struct and append warnings - continue; - }; - while (it.next() catch { - // TODO accept a diagnostics struct and append warnings - continue; - }) |record| switch (record.rr) { - std.posix.RR.A => { - const data = record.packet[record.data_off..][0..record.data_len]; - if (data.len != 4) return error.InvalidDnsARecord; - if (addresses_len < options.addresses_buffer.len) { - options.addresses_buffer[addresses_len] = .{ .ip4 = .{ - .bytes = data[0..4].*, - .port = options.port, - } }; - addresses_len += 1; - } - }, - std.posix.RR.AAAA => { - const data = record.packet[record.data_off..][0..record.data_len]; - if (data.len != 16) return error.InvalidDnsAAAARecord; - if (addresses_len < options.addresses_buffer.len) { - options.addresses_buffer[addresses_len] = .{ .ip6 = .{ - .bytes = data[0..16].*, - .port = options.port, - } }; - addresses_len += 1; - } - }, - std.posix.RR.CNAME => { - _, canonical_name = expand(record.packet, record.data_off, options.canonical_name_buffer) catch - return error.InvalidDnsCnameRecord; - }, - else => continue, - }; - } - - if (addresses_len != 0) return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name }, - }; - - return error.NameServerFailure; -} - -fn lookupHosts(host_name: HostName, io: Io, options: LookupOptions) !LookupResult { - const file = Io.File.openAbsolute(io, "/etc/hosts", .{}) catch |err| switch (err) { - error.FileNotFound, - error.NotDir, - error.AccessDenied, - => return .empty, - - error.Canceled => |e| return e, - - else => { - // TODO populate optional diagnostic struct - return error.DetectingNetworkConfigurationFailed; - }, - }; - defer file.close(io); - - var line_buf: [512]u8 = undefined; - var file_reader = file.reader(io, &line_buf); - return lookupHostsReader(host_name, options, &file_reader.interface) catch |err| switch (err) { - error.ReadFailed => switch (file_reader.err.?) { - error.Canceled => |e| return e, - else => { - // TODO populate optional diagnostic struct - return error.DetectingNetworkConfigurationFailed; - }, - }, - }; -} - -fn lookupHostsReader(host_name: HostName, options: LookupOptions, reader: *Io.Reader) error{ReadFailed}!LookupResult { - var addresses_len: usize = 0; - var canonical_name: ?HostName = null; - while (true) { - const line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) { - error.StreamTooLong => { - // Skip lines that are too long. - _ = reader.discardDelimiterInclusive('\n') catch |e| switch (e) { - error.EndOfStream => break, - error.ReadFailed => return error.ReadFailed, - }; - continue; - }, - error.ReadFailed => return error.ReadFailed, - error.EndOfStream => break, - }; - reader.toss(1); - var split_it = std.mem.splitScalar(u8, line, '#'); - const no_comment_line = split_it.first(); - - var line_it = std.mem.tokenizeAny(u8, no_comment_line, " \t"); - const ip_text = line_it.next() orelse continue; - var first_name_text: ?[]const u8 = null; - while (line_it.next()) |name_text| { - if (std.mem.eql(u8, name_text, host_name.bytes)) { - if (first_name_text == null) first_name_text = name_text; - break; - } - } else continue; - - if (canonical_name == null) { - if (HostName.init(first_name_text.?)) |name_text| { - if (name_text.bytes.len <= options.canonical_name_buffer.len) { - const canonical_name_dest = options.canonical_name_buffer[0..name_text.bytes.len]; - @memcpy(canonical_name_dest, name_text.bytes); - canonical_name = .{ .bytes = canonical_name_dest }; - } - } else |_| {} - } - - if (options.family != .ip6) { - if (IpAddress.parseIp4(ip_text, options.port)) |addr| { - options.addresses_buffer[addresses_len] = addr; - addresses_len += 1; - if (options.addresses_buffer.len - addresses_len == 0) return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text), - }; - } else |_| {} - } - if (options.family != .ip4) { - if (IpAddress.parseIp6(ip_text, options.port)) |addr| { - options.addresses_buffer[addresses_len] = addr; - addresses_len += 1; - if (options.addresses_buffer.len - addresses_len == 0) return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name orelse copyCanon(options.canonical_name_buffer, ip_text), - }; - } else |_| {} - } - } - if (canonical_name == null) assert(addresses_len == 0); - return .{ - .addresses_len = addresses_len, - .canonical_name = canonical_name orelse undefined, - }; -} - -fn copyCanon(canonical_name_buffer: *[max_len]u8, name: []const u8) HostName { - const dest = canonical_name_buffer[0..name.len]; - @memcpy(dest, name); - return .{ .bytes = dest }; -} - -/// Writes DNS resolution query packet data to `w`; at most 280 bytes. -fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize { - // This implementation is ported from musl libc. - // A more idiomatic "ziggy" implementation would be welcome. - var name = dname; - if (std.mem.endsWith(u8, name, ".")) name.len -= 1; - assert(name.len <= 253); - const n = 17 + name.len + @intFromBool(name.len != 0); - - // Construct query template - ID will be filled later - q[0..2].* = entropy; - @memset(q[2..n], 0); - q[2] = @as(u8, op) * 8 + 1; - q[5] = 1; - @memcpy(q[13..][0..name.len], name); - var i: usize = 13; - var j: usize = undefined; - while (q[i] != 0) : (i = j + 1) { - j = i; - while (q[j] != 0 and q[j] != '.') : (j += 1) {} - // TODO determine the circumstances for this and whether or - // not this should be an error. - if (j - i - 1 > 62) unreachable; - q[i - 1] = @intCast(j - i); - } - q[i + 1] = ty; - q[i + 3] = class; - return n; +/// Adds any number of `IpAddress` into resolved, exactly one canonical_name, +/// and then always finishes by adding one `LookupResult.end` entry. +/// +/// Guaranteed not to block if provided queue has capacity at least 8. +pub fn lookup( + host_name: HostName, + io: Io, + resolved: *Io.Queue(LookupResult), + options: LookupOptions, +) void { + return io.vtable.netLookup(io.userdata, host_name, resolved, options); } pub const ExpandError = error{InvalidDnsPacket} || ValidateError; @@ -672,33 +206,43 @@ pub fn connect( port: u16, options: IpAddress.ConnectOptions, ) ConnectError!Stream { - var addresses_buffer: [32]IpAddress = undefined; - var canonical_name_buffer: [HostName.max_len]u8 = undefined; + var canonical_name_buffer: [max_len]u8 = undefined; + var results_buffer: [32]HostName.LookupResult = undefined; + var results: Io.Queue(LookupResult) = .init(&results_buffer); - const results = try lookup(host_name, io, .{ + var lookup_task = io.async(HostName.lookup, .{ host_name, io, &results, .{ .port = port, - .addresses_buffer = &addresses_buffer, .canonical_name_buffer = &canonical_name_buffer, - }); - const addresses = addresses_buffer[0..results.addresses_len]; + } }); + defer lookup_task.cancel(io); - if (addresses.len == 0) return error.UnknownHostName; + var select: Io.Select(union(enum) { ip_connect: IpAddress.ConnectError!Stream }) = .init; + defer select.cancel(io); - // TODO instead of serially, use a Select API to send out - // the connections simultaneously and then keep the first - // successful one, canceling the rest. + while (results.getOne(io)) |result| switch (result) { + .address => |address| select.async(io, .ip_connect, IpAddress.connect, .{ address, io, options }), + .canonical_name => continue, + .end => |lookup_result| { + try lookup_result; + break; + }, + } else |err| return err; - // TODO On Linux this should additionally use an Io.Queue based - // DNS resolution API in order to send out a connection after - // each DNS response before waiting for the rest of them. + var aggregate_error: ConnectError = error.UnknownHostName; - for (addresses) |*addr| { - return addr.connect(io, options) catch |err| switch (err) { - error.ConnectionRefused => continue, - else => |e| return e, - }; - } - return error.ConnectionRefused; + while (select.remaining != 0) switch (select.wait(io)) { + .ip_connect => |ip_connect| if (ip_connect) |stream| return stream else |err| switch (err) { + error.SystemResources => |e| return e, + error.OptionUnsupported => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.Canceled => |e| return e, + error.WouldBlock => return error.Unexpected, + else => |e| aggregate_error = e, + }, + }; + + return aggregate_error; } pub const ResolvConf = struct { @@ -713,7 +257,7 @@ pub const ResolvConf = struct { pub const max_nameservers = 3; /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. - fn init(io: Io) !ResolvConf { + pub fn init(io: Io) !ResolvConf { var rc: ResolvConf = .{ .nameservers_buffer = undefined, .nameservers_len = 0, @@ -749,7 +293,7 @@ pub const ResolvConf = struct { const Directive = enum { options, nameserver, domain, search }; const Option = enum { ndots, attempts, timeout }; - fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void { + pub fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void { while (reader.takeSentinel('\n')) |line_with_comment| { const line = line: { var split = std.mem.splitScalar(u8, line_with_comment, '#'); @@ -799,7 +343,7 @@ pub const ResolvConf = struct { rc.nameservers_len += 1; } - fn nameservers(rc: *const ResolvConf) []const IpAddress { + pub fn nameservers(rc: *const ResolvConf) []const IpAddress { return rc.nameservers_buffer[0..rc.nameservers_len]; } }; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index bd5f04232d..8844a8249e 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -845,7 +845,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return unexpectedErrno(err), } @@ -874,7 +874,7 @@ pub fn read(fd: fd_t, buf: []u8) ReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, else => |err| return unexpectedErrno(err), } } @@ -914,7 +914,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOTCAPABLE => return error.AccessDenied, else => |err| return unexpectedErrno(err), } @@ -936,7 +936,7 @@ pub fn readv(fd: fd_t, iov: []const iovec) ReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, else => |err| return unexpectedErrno(err), } } @@ -983,7 +983,7 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1016,7 +1016,7 @@ pub fn pread(fd: fd_t, buf: []u8, offset: u64) PReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1134,7 +1134,7 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1160,7 +1160,7 @@ pub fn preadv(fd: fd_t, iov: []const iovec, offset: u64) PReadError!usize { .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NXIO => return error.Unseekable, .SPIPE => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -4205,7 +4205,7 @@ pub const ConnectError = error{ /// Timeout while attempting connection. The server may be too busy to accept new connections. Note /// that for IP sockets the timeout may be very long when syncookies are enabled on the server. - ConnectionTimedOut, + Timeout, /// This error occurs when no global event loop is configured, /// and connecting to the socket would block. @@ -4236,7 +4236,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, .WSAECONNREFUSED => return error.ConnectionRefused, .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAETIMEDOUT => return error.ConnectionTimedOut, + .WSAETIMEDOUT => return error.Timeout, .WSAEHOSTUNREACH, // TODO: should we return NetworkUnreachable in this case as well? .WSAENETUNREACH, => return error.NetworkUnreachable, @@ -4273,7 +4273,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol. - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .NOENT => return error.FileNotFound, // Returned when socket is AF.UNIX and the given path does not exist. .CONNABORTED => unreachable, // Tried to reuse socket that previously received error.ConnectionRefused. else => |err| return unexpectedErrno(err), @@ -4333,7 +4333,7 @@ pub fn getsockoptError(sockfd: fd_t) ConnectError!void { .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol. - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .CONNRESET => return error.ConnectionResetByPeer, else => |err| return unexpectedErrno(err), }, @@ -6465,7 +6465,7 @@ pub const RecvFromError = error{ SystemResources, ConnectionResetByPeer, - ConnectionTimedOut, + Timeout, /// The socket has not been bound. SocketNotBound, @@ -6508,7 +6508,7 @@ pub fn recvfrom( .WSAENETDOWN => return error.NetworkDown, .WSAENOTCONN => return error.SocketUnconnected, .WSAEWOULDBLOCK => return error.WouldBlock, - .WSAETIMEDOUT => return error.ConnectionTimedOut, + .WSAETIMEDOUT => return error.Timeout, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), } @@ -6528,7 +6528,7 @@ pub fn recvfrom( .NOMEM => return error.SystemResources, .CONNREFUSED => return error.ConnectionRefused, .CONNRESET => return error.ConnectionResetByPeer, - .TIMEDOUT => return error.ConnectionTimedOut, + .TIMEDOUT => return error.Timeout, .PIPE => return error.BrokenPipe, else => |err| return unexpectedErrno(err), } diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 7bf836b583..2b11e971ca 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -428,7 +428,7 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { error.WouldBlock => return error.Unexpected, error.BrokenPipe => return error.Unexpected, error.ConnectionResetByPeer => return error.Unexpected, - error.ConnectionTimedOut => return error.Unexpected, + error.Timeout => return error.Unexpected, error.NotOpenForReading => return error.Unexpected, error.SocketUnconnected => return error.Unexpected, From d3c4158a1053e5322d137f61bf4bc643650a4741 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 00:36:02 -0700 Subject: [PATCH 108/244] std.Io: implement Select and finish implementation of HostName.connect --- lib/std/Io.zig | 84 ++++++++++++++++++++++++++++++++++++- lib/std/Io/Threaded.zig | 13 +++--- lib/std/Io/net.zig | 4 +- lib/std/Io/net/HostName.zig | 14 ++++--- lib/std/Io/net/test.zig | 65 +++++++++++++++++++--------- 5 files changed, 144 insertions(+), 36 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 41aebcb712..6da8187cd1 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -639,7 +639,7 @@ pub const VTable = struct { /// Copied and then passed to `start`. context: []const u8, context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) void, + start: *const fn (*Group, context: *const anyopaque) void, ) void, groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, @@ -1005,7 +1005,8 @@ pub const Group = struct { pub fn async(g: *Group, io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function))) void { const Args = @TypeOf(args); const TypeErased = struct { - fn start(context: *const anyopaque) void { + fn start(group: *Group, context: *const anyopaque) void { + _ = group; const args_casted: *const Args = @ptrCast(@alignCast(context)); @call(.auto, function, args_casted.*); } @@ -1033,6 +1034,85 @@ pub const Group = struct { } }; +pub fn Select(comptime U: type) type { + return struct { + io: Io, + group: Group, + queue: Queue(U), + outstanding: usize, + + const S = @This(); + + pub const Union = U; + + pub const Field = std.meta.FieldEnum(U); + + pub fn init(io: Io, buffer: []U) S { + return .{ + .io = io, + .queue = .init(buffer), + .group = .init, + .outstanding = 0, + }; + } + + /// Calls `function` with `args` asynchronously. The resource spawned is + /// owned by the select. + /// + /// `function` must have return type matching the `field` field of `Union`. + /// + /// `function` *may* be called immediately, before `async` returns. + /// + /// After this is called, `wait` or `cancel` must be called before the + /// select is deinitialized. + /// + /// Threadsafe. + /// + /// Related: + /// * `Io.async` + /// * `Group.async` + pub fn async( + s: *S, + comptime field: Field, + function: anytype, + args: std.meta.ArgsTuple(@TypeOf(function)), + ) void { + const Args = @TypeOf(args); + const TypeErased = struct { + fn start(group: *Group, context: *const anyopaque) void { + const args_casted: *const Args = @ptrCast(@alignCast(context)); + const unerased_select: *S = @fieldParentPtr("group", group); + const elem = @unionInit(U, @tagName(field), @call(.auto, function, args_casted.*)); + unerased_select.queue.putOneUncancelable(unerased_select.io, elem); + } + }; + _ = @atomicRmw(usize, &s.outstanding, .Add, 1, .monotonic); + s.io.vtable.groupAsync(s.io.userdata, &s.group, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + } + + /// Blocks until another task of the select finishes. + /// + /// Asserts there is at least one more `outstanding` task. + /// + /// Not threadsafe. + pub fn wait(s: *S) Io.Cancelable!U { + s.outstanding -= 1; + return s.queue.getOne(s.io); + } + + /// Equivalent to `wait` but requests cancellation on all remaining + /// tasks owned by the select. + /// + /// It is illegal to call `wait` after this. + /// + /// Idempotent. Not threadsafe. + pub fn cancel(s: *S) void { + s.outstanding = 0; + s.group.cancel(s.io); + } + }; +} + pub const Mutex = struct { state: State, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f0d7f3ea4b..f654687684 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -458,7 +458,7 @@ const GroupClosure = struct { group: *Io.Group, /// Points to sibling `GroupClosure`. Used for walking the group to cancel all. node: std.SinglyLinkedList.Node, - func: *const fn (context: *anyopaque) void, + func: *const fn (*Io.Group, context: *anyopaque) void, context_alignment: std.mem.Alignment, context_len: usize, @@ -476,7 +476,7 @@ const GroupClosure = struct { return; } current_closure = closure; - gc.func(gc.contextPointer()); + gc.func(group, gc.contextPointer()); current_closure = null; // In case a cancel happens after successful task completion, prevents @@ -512,7 +512,7 @@ fn groupAsync( group: *Io.Group, context: []const u8, context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) void, + start: *const fn (*Io.Group, context: *const anyopaque) void, ) void { if (builtin.single_threaded) return start(context.ptr); const pool: *Pool = @ptrCast(@alignCast(userdata)); @@ -520,7 +520,7 @@ fn groupAsync( const gpa = pool.allocator; const n = GroupClosure.contextEnd(context_alignment, context.len); const gc: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { - return start(context.ptr); + return start(group, context.ptr); })); gc.* = .{ .closure = .{ @@ -548,7 +548,7 @@ fn groupAsync( pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { pool.mutex.unlock(); gc.free(gpa); - return start(context.ptr); + return start(group, context.ptr); }; pool.run_queue.prepend(&gc.closure.node); @@ -558,7 +558,7 @@ fn groupAsync( assert(pool.run_queue.popFirst() == &gc.closure.node); pool.mutex.unlock(); gc.free(gpa); - return start(context.ptr); + return start(group, context.ptr); }; pool.threads.appendAssumeCapacity(thread); } @@ -2662,6 +2662,7 @@ fn netLookupFallible( .{ .address = addr }, .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, }); + return; } else |_| {} } diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 53cfee60c5..7ffc6098a2 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -315,8 +315,8 @@ pub const IpAddress = union(enum) { }; /// Initiates a connection-oriented network stream. - pub fn connect(address: *const IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { - return io.vtable.netConnectIp(io.userdata, address, options); + pub fn connect(address: IpAddress, io: Io, options: ConnectOptions) ConnectError!Stream { + return io.vtable.netConnectIp(io.userdata, &address, options); } }; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 788c40dd0c..291b2745f4 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -88,7 +88,7 @@ pub const LookupResult = union(enum) { /// Adds any number of `IpAddress` into resolved, exactly one canonical_name, /// and then always finishes by adding one `LookupResult.end` entry. /// -/// Guaranteed not to block if provided queue has capacity at least 8. +/// Guaranteed not to block if provided queue has capacity at least 16. pub fn lookup( host_name: HostName, io: Io, @@ -216,11 +216,13 @@ pub fn connect( } }); defer lookup_task.cancel(io); - var select: Io.Select(union(enum) { ip_connect: IpAddress.ConnectError!Stream }) = .init; - defer select.cancel(io); + const Result = union(enum) { connect_result: IpAddress.ConnectError!Stream }; + var finished_task_buffer: [results_buffer.len]Result = undefined; + var select: Io.Select(Result) = .init(io, &finished_task_buffer); + defer select.cancel(); while (results.getOne(io)) |result| switch (result) { - .address => |address| select.async(io, .ip_connect, IpAddress.connect, .{ address, io, options }), + .address => |address| select.async(.connect_result, IpAddress.connect, .{ address, io, options }), .canonical_name => continue, .end => |lookup_result| { try lookup_result; @@ -230,8 +232,8 @@ pub fn connect( var aggregate_error: ConnectError = error.UnknownHostName; - while (select.remaining != 0) switch (select.wait(io)) { - .ip_connect => |ip_connect| if (ip_connect) |stream| return stream else |err| switch (err) { + while (select.outstanding != 0) switch (try select.wait()) { + .connect_result => |connect_result| if (connect_result) |stream| return stream else |err| switch (err) { error.SystemResources => |e| return e, error.OptionUnsupported => |e| return e, error.ProcessFdQuotaExceeded => |e| return e, diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index edac076a6a..1c8f8bd8c7 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -1,5 +1,7 @@ -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const net = std.Io.net; const mem = std.mem; const testing = std.testing; @@ -126,33 +128,56 @@ test "resolve DNS" { const localhost_v4 = try net.IpAddress.parse("127.0.0.1", 80); const localhost_v6 = try net.IpAddress.parse("::2", 80); - var addresses_buffer: [8]net.IpAddress = undefined; - var canon_name_buffer: [net.HostName.max_len]u8 = undefined; - const result = try net.HostName.lookup(try .init("localhost"), io, .{ + var canonical_name_buffer: [net.HostName.max_len]u8 = undefined; + var results_buffer: [32]net.HostName.LookupResult = undefined; + var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer); + + net.HostName.lookup(try .init("localhost"), io, &results, .{ .port = 80, - .addresses_buffer = &addresses_buffer, - .canonical_name_buffer = &canon_name_buffer, + .canonical_name_buffer = &canonical_name_buffer, }); - for (addresses_buffer[0..result.addresses_len]) |addr| { - if (addr.eql(&localhost_v4) or addr.eql(&localhost_v6)) break; - } else @panic("unexpected address for localhost"); + + var addresses_found: usize = 0; + + while (results.getOne(io)) |result| switch (result) { + .address => |address| { + if (address.eql(&localhost_v4) or address.eql(&localhost_v6)) + addresses_found += 1; + }, + .canonical_name => |canonical_name| try testing.expectEqualStrings("localhost", canonical_name.bytes), + .end => |end| { + try end; + break; + }, + } else |err| return err; + + try testing.expect(addresses_found != 0); } { // The tests are required to work even when there is no Internet connection, // so some of these errors we must accept and skip the test. - var addresses_buffer: [8]net.IpAddress = undefined; - var canon_name_buffer: [net.HostName.max_len]u8 = undefined; - const result = net.HostName.lookup(try .init("example.com"), io, .{ + var canonical_name_buffer: [net.HostName.max_len]u8 = undefined; + var results_buffer: [16]net.HostName.LookupResult = undefined; + var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer); + + net.HostName.lookup(try .init("example.com"), io, &results, .{ .port = 80, - .addresses_buffer = &addresses_buffer, - .canonical_name_buffer = &canon_name_buffer, - }) catch |err| switch (err) { - error.UnknownHostName => return error.SkipZigTest, - error.NameServerFailure => return error.SkipZigTest, - else => return err, - }; - _ = result; + .canonical_name_buffer = &canonical_name_buffer, + }); + + while (results.getOne(io)) |result| switch (result) { + .address => {}, + .canonical_name => {}, + .end => |end| { + end catch |err| switch (err) { + error.UnknownHostName => return error.SkipZigTest, + error.NameServerFailure => return error.SkipZigTest, + else => return err, + }; + break; + }, + } else |err| return err; } } From adaef433d24ce88230fe64ce20214d695cf83bc0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 10:20:04 -0700 Subject: [PATCH 109/244] std.net.HostName.connect: rework to avoid waiting for DNS The previous implementation would eagerly attempt TCP connection upon receiving a DNS reply, but it would still wait for all the DNS results before returning from the function. This implementation returns immediately upon first successful TCP connection, canceling not only in-flight TCP connection attempts but also unfinished DNS queries. --- lib/std/Io/net/HostName.zig | 84 +++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 291b2745f4..75e69a6a76 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -206,34 +206,16 @@ pub fn connect( port: u16, options: IpAddress.ConnectOptions, ) ConnectError!Stream { - var canonical_name_buffer: [max_len]u8 = undefined; - var results_buffer: [32]HostName.LookupResult = undefined; - var results: Io.Queue(LookupResult) = .init(&results_buffer); + var connect_many_buffer: [32]ConnectManyResult = undefined; + var connect_many_queue: Io.Queue(ConnectManyResult) = .init(&connect_many_buffer); - var lookup_task = io.async(HostName.lookup, .{ host_name, io, &results, .{ - .port = port, - .canonical_name_buffer = &canonical_name_buffer, - } }); - defer lookup_task.cancel(io); - - const Result = union(enum) { connect_result: IpAddress.ConnectError!Stream }; - var finished_task_buffer: [results_buffer.len]Result = undefined; - var select: Io.Select(Result) = .init(io, &finished_task_buffer); - defer select.cancel(); - - while (results.getOne(io)) |result| switch (result) { - .address => |address| select.async(.connect_result, IpAddress.connect, .{ address, io, options }), - .canonical_name => continue, - .end => |lookup_result| { - try lookup_result; - break; - }, - } else |err| return err; + var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options }); + defer connect_many.cancel(io); var aggregate_error: ConnectError = error.UnknownHostName; - while (select.outstanding != 0) switch (try select.wait()) { - .connect_result => |connect_result| if (connect_result) |stream| return stream else |err| switch (err) { + while (connect_many_queue.getOne(io)) |result| switch (result) { + .connection => |connection| if (connection) |stream| return stream else |err| switch (err) { error.SystemResources => |e| return e, error.OptionUnsupported => |e| return e, error.ProcessFdQuotaExceeded => |e| return e, @@ -242,9 +224,59 @@ pub fn connect( error.WouldBlock => return error.Unexpected, else => |e| aggregate_error = e, }, - }; + .end => |end| { + try end; + return aggregate_error; + }, + } else |err| return err; +} - return aggregate_error; +pub const ConnectManyResult = union(enum) { + connection: IpAddress.ConnectError!Stream, + end: ConnectError!void, +}; + +/// Asynchronously establishes a connection to all IP addresses associated with +/// a host name, adding them to a results queue upon completion. +pub fn connectMany( + host_name: HostName, + io: Io, + port: u16, + results: *Io.Queue(ConnectManyResult), + options: IpAddress.ConnectOptions, +) void { + var canonical_name_buffer: [max_len]u8 = undefined; + var lookup_buffer: [32]HostName.LookupResult = undefined; + var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer); + + host_name.lookup(io, &lookup_queue, .{ + .port = port, + .canonical_name_buffer = &canonical_name_buffer, + }); + + var group: Io.Group = .init; + defer group.cancel(io); + + while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) { + .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), + .canonical_name => continue, + .end => |lookup_result| { + group.wait(io); + results.putOneUncancelable(io, .{ .end = lookup_result }); + return; + }, + } else |err| switch (err) { + error.Canceled => |e| results.putOneUncancelable(io, .{ .end = e }), + } +} + +fn enqueueConnection( + address: IpAddress, + io: Io, + queue: *Io.Queue(ConnectManyResult), + options: IpAddress.ConnectOptions, +) void { + queue.putOneUncancelable(io, .{ .connection = address.connect(io, options) }); } pub const ResolvConf = struct { From 80069c1e69140ac240e5397270ea919ddbfce89b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 11:00:00 -0700 Subject: [PATCH 110/244] std.Io.Queue: add "uncancelable" variants to "get" useful for resource management --- lib/std/Io.zig | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 6da8187cd1..2e15c4b8cb 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -1315,10 +1315,23 @@ pub const TypeErasedQueue = struct { pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) Cancelable!usize { assert(buffer.len >= min); - + if (buffer.len == 0) return 0; try q.mutex.lock(io); defer q.mutex.unlock(io); + return getLocked(q, io, buffer, min, false); + } + pub fn getUncancelable(q: *@This(), io: Io, buffer: []u8, min: usize) usize { + assert(buffer.len >= min); + if (buffer.len == 0) return 0; + q.mutex.lockUncancelable(io); + defer q.mutex.unlock(io); + return getLocked(q, io, buffer, min, true) catch |err| switch (err) { + error.Canceled => unreachable, + }; + } + + pub fn getLocked(q: *@This(), io: Io, buffer: []u8, min: usize, uncancelable: bool) Cancelable!usize { // The ring buffer gets first priority, then data should come from any // queued putters, then finally the ring buffer should be filled with // data from putters so they can be resumed. @@ -1371,7 +1384,10 @@ pub const TypeErasedQueue = struct { var pending: Get = .{ .remaining = remaining, .condition = .{}, .node = .{} }; q.getters.append(&pending.node); - try pending.condition.wait(io, &q.mutex); + if (uncancelable) + pending.condition.waitUncancelable(io, &q.mutex) + else + try pending.condition.wait(io, &q.mutex); remaining = pending.remaining; } } @@ -1439,6 +1455,14 @@ pub fn Queue(Elem: type) type { return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); } + pub fn putOne(q: *@This(), io: Io, item: Elem) Cancelable!void { + assert(try q.put(io, &.{item}, 1) == 1); + } + + pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void { + assert(q.putUncancelable(io, &.{item}, 1) == 1); + } + /// Receives elements from the beginning of the queue. The function /// returns when at least `min` elements have been populated inside /// `buffer`. @@ -1450,12 +1474,8 @@ pub fn Queue(Elem: type) type { return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); } - pub fn putOne(q: *@This(), io: Io, item: Elem) Cancelable!void { - assert(try q.put(io, &.{item}, 1) == 1); - } - - pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void { - assert(q.putUncancelable(io, &.{item}, 1) == 1); + pub fn getUncancelable(q: *@This(), io: Io, buffer: []Elem, min: usize) usize { + return @divExact(q.type_erased.getUncancelable(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); } pub fn getOne(q: *@This(), io: Io) Cancelable!Elem { @@ -1464,6 +1484,12 @@ pub fn Queue(Elem: type) type { return buf[0]; } + pub fn getOneUncancelable(q: *@This(), io: Io) Elem { + var buf: [1]Elem = undefined; + assert(q.getUncancelable(io, &buf, 1) == 1); + return buf[0]; + } + /// Returns buffer length in `Elem` units. pub fn capacity(q: *const @This()) usize { return @divExact(q.type_erased.buffer.len, @sizeOf(Elem)); From 426a377c7b6a64092ab0b45710dc428da60557a1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 11:00:33 -0700 Subject: [PATCH 111/244] std.Io.net.Stream: add "const" variant to "close" useful for resource management --- lib/std/Io/net.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 7ffc6098a2..6b407eb504 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1154,6 +1154,11 @@ pub const Stream = struct { s.* = undefined; } + /// Same as `close` but doesn't try to set `Stream` to `undefined`. + pub fn closeConst(s: *const Stream, io: Io) void { + io.vtable.netClose(io.userdata, s.socket.handle); + } + pub const Reader = struct { io: Io, interface: Io.Reader, From 870a682cd84e49b2213dffd59c9848d7fd12b7a1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 11:01:03 -0700 Subject: [PATCH 112/244] std.Io.net.HostName.connect: fix resource leaks Must free other succeeded connections that lost the race. --- BRANCH_TODO | 1 + lib/std/Io/net/HostName.zig | 33 ++++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index 53b157f41e..37739e7d82 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -18,3 +18,4 @@ * migrate child process into std.Io * eliminate std.Io.poll (it should be replaced by "select" functionality) * finish moving all of std.posix into Threaded +* TCP fastopen - sends initial payload along with connection. can be done for idempotent http requests diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 75e69a6a76..6352d82dd0 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -210,25 +210,38 @@ pub fn connect( var connect_many_queue: Io.Queue(ConnectManyResult) = .init(&connect_many_buffer); var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options }); - defer connect_many.cancel(io); + var saw_end = false; + defer { + connect_many.cancel(io); + if (!saw_end) while (true) switch (connect_many_queue.getOneUncancelable(io)) { + .connection => |loser| if (loser) |s| s.closeConst(io) else |_| continue, + .end => break, + }; + } var aggregate_error: ConnectError = error.UnknownHostName; while (connect_many_queue.getOne(io)) |result| switch (result) { .connection => |connection| if (connection) |stream| return stream else |err| switch (err) { - error.SystemResources => |e| return e, - error.OptionUnsupported => |e| return e, - error.ProcessFdQuotaExceeded => |e| return e, - error.SystemFdQuotaExceeded => |e| return e, - error.Canceled => |e| return e, + error.SystemResources, + error.OptionUnsupported, + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.Canceled, + => |e| return e, + error.WouldBlock => return error.Unexpected, + else => |e| aggregate_error = e, }, .end => |end| { + saw_end = true; try end; return aggregate_error; }, - } else |err| return err; + } else |err| switch (err) { + error.Canceled => |e| return e, + } } pub const ConnectManyResult = union(enum) { @@ -255,7 +268,6 @@ pub fn connectMany( }); var group: Io.Group = .init; - defer group.cancel(io); while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) { .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), @@ -266,7 +278,10 @@ pub fn connectMany( return; }, } else |err| switch (err) { - error.Canceled => |e| results.putOneUncancelable(io, .{ .end = e }), + error.Canceled => |e| { + group.cancel(io); + results.putOneUncancelable(io, .{ .end = e }); + }, } } From 10bfbd7d60b721af9bed3f74fa7ab5171471ee46 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 12:51:34 -0700 Subject: [PATCH 113/244] std.Io.Threaded: rename from "pool" --- BRANCH_TODO | 2 +- lib/std/Io/Threaded.zig | 550 ++++++++++++++++++++-------------------- 2 files changed, 276 insertions(+), 276 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index 37739e7d82..d09d2f9273 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -1,7 +1,7 @@ -* Threaded: rename Pool to Threaded * Threaded: finish linux impl (all tests passing) * Threaded: finish macos impl * Threaded: finish windows impl +* Threaded: glibc impl of netLookup * fix Group.wait not handling cancelation (need to move impl of ResetEvent to Threaded) * implement cancelRequest for non-linux posix diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f654687684..76d1e49daf 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1,4 +1,4 @@ -const Pool = @This(); +const Threaded = @This(); const builtin = @import("builtin"); const native_os = builtin.os.tag; @@ -76,18 +76,18 @@ pub fn init( /// If these functions are avoided, then `Allocator.failing` may be passed /// here. gpa: Allocator, -) Pool { - var pool: Pool = .{ +) Threaded { + var t: Threaded = .{ .allocator = gpa, .threads = .empty, .stack_size = std.Thread.SpawnConfig.default_stack_size, .cpu_count = std.Thread.getCpuCount(), .concurrent_count = 0, }; - if (pool.cpu_count) |n| { - pool.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {}; + if (t.cpu_count) |n| { + t.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {}; } else |_| {} - return pool; + return t; } /// Statically initialize such that any call to the following functions will @@ -96,7 +96,7 @@ pub fn init( /// * `Io.VTable.concurrent` /// * `Io.VTable.groupAsync` /// When initialized this way, `deinit` is safe, but unnecessary to call. -pub const init_single_threaded: Pool = .{ +pub const init_single_threaded: Threaded = .{ .allocator = .failing, .threads = .empty, .stack_size = std.Thread.SpawnConfig.default_stack_size, @@ -104,48 +104,48 @@ pub const init_single_threaded: Pool = .{ .concurrent_count = 0, }; -pub fn deinit(pool: *Pool) void { - const gpa = pool.allocator; - pool.join(); - pool.threads.deinit(gpa); - pool.* = undefined; +pub fn deinit(t: *Threaded) void { + const gpa = t.allocator; + t.join(); + t.threads.deinit(gpa); + t.* = undefined; } -fn join(pool: *Pool) void { +fn join(t: *Threaded) void { if (builtin.single_threaded) return; { - pool.mutex.lock(); - defer pool.mutex.unlock(); - pool.join_requested = true; + t.mutex.lock(); + defer t.mutex.unlock(); + t.join_requested = true; } - pool.cond.broadcast(); - for (pool.threads.items) |thread| thread.join(); + t.cond.broadcast(); + for (t.threads.items) |thread| thread.join(); } -fn worker(pool: *Pool) void { - pool.mutex.lock(); - defer pool.mutex.unlock(); +fn worker(t: *Threaded) void { + t.mutex.lock(); + defer t.mutex.unlock(); while (true) { - while (pool.run_queue.popFirst()) |closure_node| { - pool.mutex.unlock(); + while (t.run_queue.popFirst()) |closure_node| { + t.mutex.unlock(); const closure: *Closure = @fieldParentPtr("node", closure_node); const is_concurrent = closure.is_concurrent; closure.start(closure); - pool.mutex.lock(); + t.mutex.lock(); if (is_concurrent) { // TODO also pop thread and join sometimes - pool.concurrent_count -= 1; + t.concurrent_count -= 1; } } - if (pool.join_requested) break; - pool.cond.wait(&pool.mutex); + if (t.join_requested) break; + t.cond.wait(&t.mutex); } } -pub fn io(pool: *Pool) Io { +pub fn io(t: *Threaded) Io { return .{ - .userdata = pool, + .userdata = t, .vtable = &.{ .async = async, .concurrent = concurrent, @@ -324,14 +324,14 @@ fn async( start(context.ptr, result.ptr); return null; } - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const cpu_count = pool.cpu_count catch { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const cpu_count = t.cpu_count catch { return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; }; }; - const gpa = pool.allocator; + const gpa = t.allocator; const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result.len; @@ -356,38 +356,38 @@ fn async( @memcpy(ac.contextPointer()[0..context.len], context); - pool.mutex.lock(); + t.mutex.lock(); - const thread_capacity = cpu_count - 1 + pool.concurrent_count; + const thread_capacity = cpu_count - 1 + t.concurrent_count; - pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { - pool.mutex.unlock(); + t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { + t.mutex.unlock(); ac.free(gpa, result.len); start(context.ptr, result.ptr); return null; }; - pool.run_queue.prepend(&ac.closure.node); + t.run_queue.prepend(&ac.closure.node); - if (pool.threads.items.len < thread_capacity) { - const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { - if (pool.threads.items.len == 0) { - assert(pool.run_queue.popFirst() == &ac.closure.node); - pool.mutex.unlock(); + if (t.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch { + if (t.threads.items.len == 0) { + assert(t.run_queue.popFirst() == &ac.closure.node); + t.mutex.unlock(); ac.free(gpa, result.len); start(context.ptr, result.ptr); return null; } // Rely on other workers to do it. - pool.mutex.unlock(); - pool.cond.signal(); + t.mutex.unlock(); + t.cond.signal(); return @ptrCast(ac); }; - pool.threads.appendAssumeCapacity(thread); + t.threads.appendAssumeCapacity(thread); } - pool.mutex.unlock(); - pool.cond.signal(); + t.mutex.unlock(); + t.cond.signal(); return @ptrCast(ac); } @@ -401,9 +401,9 @@ fn concurrent( ) error{OutOfMemory}!*Io.AnyFuture { if (builtin.single_threaded) unreachable; - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const cpu_count = pool.cpu_count catch 1; - const gpa = pool.allocator; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const cpu_count = t.cpu_count catch 1; + const gpa = t.allocator; const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result_len; @@ -424,37 +424,37 @@ fn concurrent( }; @memcpy(ac.contextPointer()[0..context.len], context); - pool.mutex.lock(); + t.mutex.lock(); - pool.concurrent_count += 1; - const thread_capacity = cpu_count - 1 + pool.concurrent_count; + t.concurrent_count += 1; + const thread_capacity = cpu_count - 1 + t.concurrent_count; - pool.threads.ensureTotalCapacity(gpa, thread_capacity) catch { - pool.mutex.unlock(); + t.threads.ensureTotalCapacity(gpa, thread_capacity) catch { + t.mutex.unlock(); ac.free(gpa, result_len); return error.OutOfMemory; }; - pool.run_queue.prepend(&ac.closure.node); + t.run_queue.prepend(&ac.closure.node); - if (pool.threads.items.len < thread_capacity) { - const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { - assert(pool.run_queue.popFirst() == &ac.closure.node); - pool.mutex.unlock(); + if (t.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch { + assert(t.run_queue.popFirst() == &ac.closure.node); + t.mutex.unlock(); ac.free(gpa, result_len); return error.OutOfMemory; }; - pool.threads.appendAssumeCapacity(thread); + t.threads.appendAssumeCapacity(thread); } - pool.mutex.unlock(); - pool.cond.signal(); + t.mutex.unlock(); + t.cond.signal(); return @ptrCast(ac); } const GroupClosure = struct { closure: Closure, - pool: *Pool, + t: *Threaded, group: *Io.Group, /// Points to sibling `GroupClosure`. Used for walking the group to cancel all. node: std.SinglyLinkedList.Node, @@ -515,9 +515,9 @@ fn groupAsync( start: *const fn (*Io.Group, context: *const anyopaque) void, ) void { if (builtin.single_threaded) return start(context.ptr); - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const cpu_count = pool.cpu_count catch 1; - const gpa = pool.allocator; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const cpu_count = t.cpu_count catch 1; + const gpa = t.allocator; const n = GroupClosure.contextEnd(context_alignment, context.len); const gc: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { return start(group, context.ptr); @@ -528,7 +528,7 @@ fn groupAsync( .start = GroupClosure.start, .is_concurrent = false, }, - .pool = pool, + .t = t, .group = group, .node = undefined, .func = start, @@ -537,30 +537,30 @@ fn groupAsync( }; @memcpy(gc.contextPointer()[0..context.len], context); - pool.mutex.lock(); + t.mutex.lock(); // Append to the group linked list inside the mutex to make `Io.Group.async` thread-safe. gc.node = .{ .next = @ptrCast(@alignCast(group.token)) }; group.token = &gc.node; - const thread_capacity = cpu_count - 1 + pool.concurrent_count; + const thread_capacity = cpu_count - 1 + t.concurrent_count; - pool.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { - pool.mutex.unlock(); + t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { + t.mutex.unlock(); gc.free(gpa); return start(group, context.ptr); }; - pool.run_queue.prepend(&gc.closure.node); + t.run_queue.prepend(&gc.closure.node); - if (pool.threads.items.len < thread_capacity) { - const thread = std.Thread.spawn(.{ .stack_size = pool.stack_size }, worker, .{pool}) catch { - assert(pool.run_queue.popFirst() == &gc.closure.node); - pool.mutex.unlock(); + if (t.threads.items.len < thread_capacity) { + const thread = std.Thread.spawn(.{ .stack_size = t.stack_size }, worker, .{t}) catch { + assert(t.run_queue.popFirst() == &gc.closure.node); + t.mutex.unlock(); gc.free(gpa); return start(group, context.ptr); }; - pool.threads.appendAssumeCapacity(thread); + t.threads.appendAssumeCapacity(thread); } // This needs to be done before unlocking the mutex to avoid a race with @@ -568,13 +568,13 @@ fn groupAsync( const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); std.Thread.WaitGroup.startStateless(group_state); - pool.mutex.unlock(); - pool.cond.signal(); + t.mutex.unlock(); + t.cond.signal(); } fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const gpa = pool.allocator; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const gpa = t.allocator; if (builtin.single_threaded) return; @@ -593,8 +593,8 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { } fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const gpa = pool.allocator; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const gpa = t.allocator; if (builtin.single_threaded) return; @@ -629,9 +629,9 @@ fn await( result_alignment: std.mem.Alignment, ) void { _ = result_alignment; - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - closure.waitAndFree(pool.allocator, result); + closure.waitAndFree(t.allocator, result); } fn cancel( @@ -641,31 +641,31 @@ fn cancel( result_alignment: std.mem.Alignment, ) void { _ = result_alignment; - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const ac: *AsyncClosure = @ptrCast(@alignCast(any_future)); ac.closure.requestCancel(); - ac.waitAndFree(pool.allocator, result); + ac.waitAndFree(t.allocator, result); } fn cancelRequested(userdata: ?*anyopaque) bool { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; const closure = current_closure orelse return false; return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == Closure.canceling_tid; } -fn checkCancel(pool: *Pool) error{Canceled}!void { - if (cancelRequested(pool)) return error.Canceled; +fn checkCancel(t: *Threaded) error{Canceled}!void { + if (cancelRequested(t)) return error.Canceled; } fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); if (prev_state == .contended) { - try pool.checkCancel(); + try t.checkCancel(); futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { - try pool.checkCancel(); + try t.checkCancel(); futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } } @@ -689,8 +689,8 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut } fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const pool_io = pool.io(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = t.io(); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); const cond_state = &ints[0]; @@ -704,8 +704,8 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: assert(state & waiter_mask != waiter_mask); state += one_waiter; - mutex.unlock(pool_io); - defer mutex.lockUncancelable(pool_io); + mutex.unlock(t_io); + defer mutex.lockUncancelable(t_io); while (true) { futexWait(cond_epoch, epoch); @@ -719,7 +719,7 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: } fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); const cond_state = &ints[0]; @@ -743,11 +743,11 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I assert(state & waiter_mask != waiter_mask); state += one_waiter; - mutex.unlock(pool.io()); - defer mutex.lockUncancelable(pool.io()); + mutex.unlock(t.io()); + defer mutex.lockUncancelable(t.io()); while (true) { - try pool.checkCancel(); + try t.checkCancel(); futexWait(cond_epoch, epoch); epoch = cond_epoch.load(.acquire); @@ -764,8 +764,8 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I } fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); const cond_state = &ints[0]; @@ -825,13 +825,13 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. } fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) { .SUCCESS => return, .INTR => continue, @@ -858,8 +858,8 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: } fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); _ = dir; @panic("TODO"); @@ -871,7 +871,7 @@ fn dirStatPathLinux( sub_path: []const u8, options: Io.Dir.StatPathOptions, ) Io.Dir.StatPathError!Io.File.Stat { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const linux = std.os.linux; var path_buffer: [posix.PATH_MAX]u8 = undefined; @@ -881,7 +881,7 @@ fn dirStatPathLinux( @as(u32, if (!options.follow_symlinks) linux.AT.SYMLINK_NOFOLLOW else 0); while (true) { - try pool.checkCancel(); + try t.checkCancel(); var statx = std.mem.zeroes(linux.Statx); const rc = linux.statx( dir.handle, @@ -913,7 +913,7 @@ fn dirStatPathPosix( sub_path: []const u8, options: Io.Dir.StatPathOptions, ) Io.Dir.StatPathError!Io.File.Stat { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); @@ -921,7 +921,7 @@ fn dirStatPathPosix( const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; while (true) { - try pool.checkCancel(); + try t.checkCancel(); var stat = std.mem.zeroes(posix.Stat); switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) { .SUCCESS => return statFromPosix(stat), @@ -943,12 +943,12 @@ fn dirStatPathPosix( } fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); if (posix.Stat == void) return error.Streaming; while (true) { - try pool.checkCancel(); + try t.checkCancel(); var stat = std.mem.zeroes(posix.Stat); switch (posix.errno(fstat_sym(file.handle, &stat))) { .SUCCESS => return statFromPosix(&stat), @@ -963,10 +963,10 @@ fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File } fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const linux = std.os.linux; while (true) { - try pool.checkCancel(); + try t.checkCancel(); var statx = std.mem.zeroes(linux.Statx); const rc = linux.statx( file.handle, @@ -993,17 +993,17 @@ fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File } fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); _ = file; @panic("TODO"); } fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { if (builtin.link_libc) return fileStatPosix(userdata, file); - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); while (true) { - try pool.checkCancel(); + try t.checkCancel(); var stat: std.os.wasi.filestat_t = undefined; switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) { .SUCCESS => return statFromWasi(&stat), @@ -1031,7 +1031,7 @@ fn dirCreateFilePosix( sub_path: []const u8, flags: Io.File.CreateFlags, ) Io.File.OpenError!Io.File { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); @@ -1062,7 +1062,7 @@ fn dirCreateFilePosix( }; const fd: posix.fd_t = while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode); switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), @@ -1106,7 +1106,7 @@ fn dirCreateFilePosix( .exclusive => posix.LOCK.EX | lock_nonblocking, }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.flock(fd, lock_flags))) { .SUCCESS => break, .INTR => continue, @@ -1123,7 +1123,7 @@ fn dirCreateFilePosix( if (has_flock_open_flags and flags.lock_nonblocking) { var fl_flags: usize = while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { .SUCCESS => break, .INTR => continue, @@ -1132,7 +1132,7 @@ fn dirCreateFilePosix( }; fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { .SUCCESS => break, .INTR => continue, @@ -1150,7 +1150,7 @@ fn dirOpenFile( sub_path: []const u8, flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); @@ -1191,7 +1191,7 @@ fn dirOpenFile( } } const fd: posix.fd_t = while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = openat_sym(dir.handle, sub_path_posix, os_flags, @as(posix.mode_t, 0)); switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), @@ -1235,7 +1235,7 @@ fn dirOpenFile( .exclusive => posix.LOCK.EX | lock_nonblocking, }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.flock(fd, lock_flags))) { .SUCCESS => break, .INTR => continue, @@ -1252,7 +1252,7 @@ fn dirOpenFile( if (has_flock_open_flags and flags.lock_nonblocking) { var fl_flags: usize = while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { .SUCCESS => break, .INTR => continue, @@ -1261,7 +1261,7 @@ fn dirOpenFile( }; fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { .SUCCESS => break, .INTR => continue, @@ -1274,13 +1274,13 @@ fn dirOpenFile( } fn fileClose(userdata: ?*anyopaque, file: Io.File) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; posix.close(file.handle); } fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); if (is_windows) { const DWORD = windows.DWORD; @@ -1288,7 +1288,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File var truncate: usize = 0; var total: usize = 0; while (index < data.len) { - try pool.checkCancel(); + try t.checkCancel(); { const untruncated = data[index]; data[index] = untruncated[truncate..]; @@ -1333,7 +1333,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File assert(dest[0].len > 0); if (native_os == .wasi and !builtin.link_libc) while (true) { - try pool.checkCancel(); + try t.checkCancel(); var nread: usize = undefined; switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) { .SUCCESS => return nread, @@ -1354,7 +1354,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = posix.system.readv(file.handle, dest.ptr, @intCast(dest.len)); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), @@ -1377,7 +1377,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File } fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); if (is_windows) { const DWORD = windows.DWORD; @@ -1386,7 +1386,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset var truncate: usize = 0; var total: usize = 0; while (true) { - try pool.checkCancel(); + try t.checkCancel(); { const untruncated = data[index]; data[index] = untruncated[truncate..]; @@ -1454,7 +1454,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset assert(dest[0].len > 0); if (native_os == .wasi and !builtin.link_libc) while (true) { - try pool.checkCancel(); + try t.checkCancel(); var nread: usize = undefined; switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) { .SUCCESS => return nread, @@ -1479,7 +1479,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = preadv_sym(file.handle, dest.ptr, @intCast(dest.len), @bitCast(offset)); switch (posix.errno(rc)) { .SUCCESS => return @bitCast(rc), @@ -1505,8 +1505,8 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset } fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); _ = file; _ = offset; @@ -1514,11 +1514,11 @@ fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekErr } fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const fd = file.handle; if (native_os == .linux and !builtin.link_libc and @sizeOf(usize) == 4) while (true) { - try pool.checkCancel(); + try t.checkCancel(); var result: u64 = undefined; switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.SET))) { .SUCCESS => return, @@ -1533,12 +1533,12 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr }; if (native_os == .windows) { - try pool.checkCancel(); + try t.checkCancel(); return windows.SetFilePointerEx_BEGIN(fd, offset); } if (native_os == .wasi and !builtin.link_libc) while (true) { - try pool.checkCancel(); + try t.checkCancel(); var new_offset: std.os.wasi.filesize_t = undefined; switch (std.os.wasi.fd_seek(fd, @bitCast(offset), .SET, &new_offset)) { .SUCCESS => return, @@ -1556,7 +1556,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr if (posix.SEEK == void) return error.Unseekable; while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(lseek_sym(fd, @bitCast(offset), posix.SEEK.SET))) { .SUCCESS => return, .INTR => continue, @@ -1571,8 +1571,8 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr } fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posix.off_t) Io.File.PWriteError!usize { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); const fs_file: std.fs.File = .{ .handle = file.handle }; return switch (offset) { -1 => fs_file.write(buffer), @@ -1581,8 +1581,8 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posi } fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; const clock_id: posix.clockid_t = clockToPosix(clock); var tp: posix.timespec = undefined; switch (posix.errno(posix.system.clock_gettime(clock_id, &tp))) { @@ -1593,8 +1593,8 @@ fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp } fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; switch (clock) { .realtime => { // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds @@ -1612,8 +1612,8 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestam } fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; var ns: std.os.wasi.timestamp_t = undefined; const err = std.os.wasi.clock_time_get(clockToWasi(clock), 1, &ns); if (err != .SUCCESS) return error.Unexpected; @@ -1621,7 +1621,7 @@ fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { } fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const clock_id: posix.clockid_t = clockToPosix(switch (timeout) { .none => .awake, .duration => |d| d.clock, @@ -1634,7 +1634,7 @@ fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { }; var timespec: posix.timespec = timestampToPosix(deadline_nanoseconds); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (std.os.linux.E.init(std.os.linux.clock_nanosleep(clock_id, .{ .ABSTIME = switch (timeout) { .none, .duration => false, .deadline => true, @@ -1648,10 +1648,10 @@ fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); const ms = ms: { - const duration_and_clock = (try timeout.toDurationFromNow(pool.io())) orelse + const duration_and_clock = (try timeout.toDurationFromNow(t.io())) orelse break :ms std.math.maxInt(windows.DWORD); break :ms std.math.lossyCast(windows.DWORD, duration_and_clock.duration.toMilliseconds()); }; @@ -1659,12 +1659,12 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); const w = std.os.wasi; - const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(pool.io())) |d| .{ + const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t.io())) |d| .{ .id = clockToWasi(d.clock), .timeout = std.math.lossyCast(u64, d.duration.nanoseconds), .precision = 0, @@ -1688,19 +1688,19 @@ fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type; const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type; var timespec: posix.timespec = t: { - const d = (try timeout.toDurationFromNow(pool.io())) orelse break :t .{ + const d = (try timeout.toDurationFromNow(t.io())) orelse break :t .{ .sec = std.math.maxInt(sec_type), .nsec = std.math.maxInt(nsec_type), }; break :t timestampToPosix(d.duration.nanoseconds); }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.nanosleep(×pec, ×pec))) { .INTR => continue, else => return, // This prong handles success as well as unexpected errors. @@ -1709,8 +1709,8 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; var reset_event: ResetEvent = .unset; @@ -1745,26 +1745,26 @@ fn netListenIpPosix( address: IpAddress, options: IpAddress.ListenOptions, ) IpAddress.ListenError!net.Server { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(&address); - const socket_fd = try openSocketPosix(pool, family, .{ + const socket_fd = try openSocketPosix(t, family, .{ .mode = options.mode, .protocol = options.protocol, }); errdefer posix.close(socket_fd); if (options.reuse_address) { - try setSocketOption(pool, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1); + try setSocketOption(t, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1); if (@hasDecl(posix.SO, "REUSEPORT")) - try setSocketOption(pool, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1); + try setSocketOption(t, socket_fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, 1); } var storage: PosixAddress = undefined; var addr_len = addressToPosix(&address, &storage); - try posixBind(pool, socket_fd, &storage.any, addr_len); + try posixBind(t, socket_fd, &storage.any, addr_len); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { .SUCCESS => break, .ADDRINUSE => return error.AddressInUse, @@ -1773,7 +1773,7 @@ fn netListenIpPosix( } } - try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); + try posixGetSockName(t, socket_fd, &storage.any, &addr_len); return .{ .socket = .{ .handle = socket_fd, @@ -1788,8 +1788,8 @@ fn netListenUnix( options: net.UnixAddress.ListenOptions, ) net.UnixAddress.ListenError!net.Socket.Handle { if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const socket_fd = openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const socket_fd = openSocketPosix(t, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported, error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported, error.SocketModeUnsupported => return error.AddressFamilyUnsupported, @@ -1799,10 +1799,10 @@ fn netListenUnix( var storage: UnixAddress = undefined; const addr_len = addressUnixToPosix(address, &storage); - try posixBindUnix(pool, socket_fd, &storage.any, addr_len); + try posixBindUnix(t, socket_fd, &storage.any, addr_len); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { .SUCCESS => break, .ADDRINUSE => return error.AddressInUse, @@ -1814,9 +1814,9 @@ fn netListenUnix( return socket_fd; } -fn posixBindUnix(pool: *Pool, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { +fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.bind(fd, addr, addr_len))) { .SUCCESS => break, .INTR => continue, @@ -1842,9 +1842,9 @@ fn posixBindUnix(pool: *Pool, fd: posix.socket_t, addr: *const posix.sockaddr, a } } -fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { +fn posixBind(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) { .SUCCESS => break, .INTR => continue, @@ -1861,9 +1861,9 @@ fn posixBind(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr } } -fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { +fn posixConnect(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { .SUCCESS => return, .INTR => continue, @@ -1890,9 +1890,9 @@ fn posixConnect(pool: *Pool, socket_fd: posix.socket_t, addr: *const posix.socka } } -fn posixConnectUnix(pool: *Pool, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { +fn posixConnectUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.connect(fd, addr, addr_len))) { .SUCCESS => return, .INTR => continue, @@ -1919,9 +1919,9 @@ fn posixConnectUnix(pool: *Pool, fd: posix.socket_t, addr: *const posix.sockaddr } } -fn posixGetSockName(pool: *Pool, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { +fn posixGetSockName(t: *Threaded, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) { .SUCCESS => break, .INTR => continue, @@ -1935,10 +1935,10 @@ fn posixGetSockName(pool: *Pool, socket_fd: posix.fd_t, addr: *posix.sockaddr, a } } -fn setSocketOption(pool: *Pool, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { +fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { const o: []const u8 = @ptrCast(&option); while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) { .SUCCESS => return, .INTR => continue, @@ -1957,17 +1957,17 @@ fn netConnectIpPosix( options: IpAddress.ConnectOptions, ) IpAddress.ConnectError!net.Stream { if (options.timeout != .none) @panic("TODO"); - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); - const socket_fd = try openSocketPosix(pool, family, .{ + const socket_fd = try openSocketPosix(t, family, .{ .mode = options.mode, .protocol = options.protocol, }); errdefer posix.close(socket_fd); var storage: PosixAddress = undefined; var addr_len = addressToPosix(address, &storage); - try posixConnect(pool, socket_fd, &storage.any, addr_len); - try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); + try posixConnect(t, socket_fd, &storage.any, addr_len); + try posixGetSockName(t, socket_fd, &storage.any, &addr_len); return .{ .socket = .{ .handle = socket_fd, .address = addressFromPosix(&storage), @@ -1979,12 +1979,12 @@ fn netConnectUnix( address: *const net.UnixAddress, ) net.UnixAddress.ConnectError!net.Socket.Handle { if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const socket_fd = try openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .stream }); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const socket_fd = try openSocketPosix(t, posix.AF.UNIX, .{ .mode = .stream }); errdefer posix.close(socket_fd); var storage: UnixAddress = undefined; const addr_len = addressUnixToPosix(address, &storage); - try posixConnectUnix(pool, socket_fd, &storage.any, addr_len); + try posixConnectUnix(t, socket_fd, &storage.any, addr_len); return socket_fd; } @@ -1993,25 +1993,25 @@ fn netBindIpPosix( address: *const IpAddress, options: IpAddress.BindOptions, ) IpAddress.BindError!net.Socket { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); - const socket_fd = try openSocketPosix(pool, family, options); + const socket_fd = try openSocketPosix(t, family, options); errdefer posix.close(socket_fd); var storage: PosixAddress = undefined; var addr_len = addressToPosix(address, &storage); - try posixBind(pool, socket_fd, &storage.any, addr_len); - try posixGetSockName(pool, socket_fd, &storage.any, &addr_len); + try posixBind(t, socket_fd, &storage.any, addr_len); + try posixGetSockName(t, socket_fd, &storage.any, &addr_len); return .{ .handle = socket_fd, .address = addressFromPosix(&storage), }; } -fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: IpAddress.BindOptions) !posix.socket_t { +fn openSocketPosix(t: *Threaded, family: posix.sa_family_t, options: IpAddress.BindOptions) !posix.socket_t { const mode = posixSocketMode(options.mode); const protocol = posixProtocol(options.protocol); const socket_fd = while (true) { - try pool.checkCancel(); + try t.checkCancel(); const flags: u32 = mode | if (socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; const socket_rc = posix.system.socket(family, flags, protocol); switch (posix.errno(socket_rc)) { @@ -2019,7 +2019,7 @@ fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: IpAddress.Bi const fd: posix.fd_t = @intCast(socket_rc); errdefer posix.close(fd); if (socket_flags_unsupported) while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, @@ -2044,7 +2044,7 @@ fn openSocketPosix(pool: *Pool, family: posix.sa_family_t, options: IpAddress.Bi if (options.ip6_only) { if (posix.IPV6 == void) return error.OptionUnsupported; - try setSocketOption(pool, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); + try setSocketOption(t, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); } return socket_fd; @@ -2054,11 +2054,11 @@ const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haik const have_accept4 = !socket_flags_unsupported; fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); var storage: PosixAddress = undefined; var addr_len: posix.socklen_t = @sizeOf(PosixAddress); const fd = while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = if (have_accept4) posix.system.accept4(listen_fd, &storage.any, &addr_len, posix.SOCK.CLOEXEC) else @@ -2068,7 +2068,7 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Serve const fd: posix.fd_t = @intCast(rc); errdefer posix.close(fd); if (!have_accept4) while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, @@ -2101,7 +2101,7 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Serve } fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; @@ -2116,7 +2116,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. assert(dest[0].len > 0); if (native_os == .wasi and !builtin.link_libc) while (true) { - try pool.checkCancel(); + try t.checkCancel(); var n: usize = undefined; switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) { .SUCCESS => return n, @@ -2137,7 +2137,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), @@ -2167,7 +2167,7 @@ fn netSend( messages: []net.OutgoingMessage, flags: net.SendFlags, ) struct { ?net.Socket.SendError, usize } { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); const posix_flags: u32 = @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) | @@ -2180,17 +2180,17 @@ fn netSend( var i: usize = 0; while (messages.len - i != 0) { if (have_sendmmsg) { - i += netSendMany(pool, handle, messages[i..], posix_flags) catch |err| return .{ err, i }; + i += netSendMany(t, handle, messages[i..], posix_flags) catch |err| return .{ err, i }; continue; } - netSendOne(pool, handle, &messages[i], posix_flags) catch |err| return .{ err, i }; + netSendOne(t, handle, &messages[i], posix_flags) catch |err| return .{ err, i }; i += 1; } return .{ null, i }; } fn netSendOne( - pool: *Pool, + t: *Threaded, handle: net.Socket.Handle, message: *net.OutgoingMessage, flags: u32, @@ -2207,7 +2207,7 @@ fn netSendOne( .flags = 0, }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = posix.system.sendmsg(handle, msg, flags); if (is_windows) { if (rc == windows.ws2_32.SOCKET_ERROR) { @@ -2274,7 +2274,7 @@ fn netSendOne( } fn netSendMany( - pool: *Pool, + t: *Threaded, handle: net.Socket.Handle, messages: []net.OutgoingMessage, flags: u32, @@ -2305,7 +2305,7 @@ fn netSendMany( } while (true) { - try pool.checkCancel(); + try t.checkCancel(); const rc = posix.system.sendmmsg(handle, clamped_msgs.ptr, @intCast(clamped_msgs.len), flags); switch (posix.errno(rc)) { .SUCCESS => { @@ -2348,7 +2348,7 @@ fn netReceive( flags: net.ReceiveFlags, timeout: Io.Timeout, ) struct { ?net.Socket.ReceiveTimeoutError, usize } { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); // recvmmsg is useless, here's why: // * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371) @@ -2375,10 +2375,10 @@ fn netReceive( var message_i: usize = 0; var data_i: usize = 0; - const deadline = timeout.toDeadline(pool.io()) catch |err| return .{ err, message_i }; + const deadline = timeout.toDeadline(t.io()) catch |err| return .{ err, message_i }; recv: while (true) { - pool.checkCancel() catch |err| return .{ err, message_i }; + t.checkCancel() catch |err| return .{ err, message_i }; if (message_buffer.len - message_i == 0) return .{ null, message_i }; const message = &message_buffer[message_i]; @@ -2416,12 +2416,12 @@ fn netReceive( continue; }, .AGAIN => while (true) { - pool.checkCancel() catch |err| return .{ err, message_i }; + t.checkCancel() catch |err| return .{ err, message_i }; if (message_i != 0) return .{ null, message_i }; const max_poll_ms = std.math.maxInt(u31); const timeout_ms: u31 = if (deadline) |d| t: { - const duration = d.durationFromNow(pool.io()) catch |err| return .{ err, message_i }; + const duration = d.durationFromNow(t.io()) catch |err| return .{ err, message_i }; if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i }; break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds())); } else max_poll_ms; @@ -2473,8 +2473,8 @@ fn netWritePosix( data: []const []const u8, splat: usize, ) net.Stream.Writer.Error!usize { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); var iovecs: [max_iovecs_len]posix.iovec_const = undefined; var msg: posix.msghdr_const = .{ @@ -2527,8 +2527,8 @@ fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), } fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - _ = pool; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; switch (native_os) { .windows => windows.closesocket(handle) catch recoverableOsBugDetected(), else => posix.close(handle), @@ -2539,10 +2539,10 @@ fn netInterfaceNameResolve( userdata: ?*anyopaque, name: *const net.Interface.Name, ) net.Interface.Name.ResolveError!net.Interface { - const pool: *Pool = @ptrCast(@alignCast(userdata)); + const t: *Threaded = @ptrCast(@alignCast(userdata)); if (native_os == .linux) { - const sock_fd = openSocketPosix(pool, posix.AF.UNIX, .{ .mode = .dgram }) catch |err| switch (err) { + const sock_fd = openSocketPosix(t, posix.AF.UNIX, .{ .mode = .dgram }) catch |err| switch (err) { error.ProcessFdQuotaExceeded => return error.SystemResources, error.SystemFdQuotaExceeded => return error.SystemResources, error.AddressFamilyUnsupported => return error.Unexpected, @@ -2559,7 +2559,7 @@ fn netInterfaceNameResolve( }; while (true) { - try pool.checkCancel(); + try t.checkCancel(); switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) }, .INTR => continue, @@ -2576,14 +2576,14 @@ fn netInterfaceNameResolve( } if (native_os == .windows) { - try pool.checkCancel(); + try t.checkCancel(); const index = std.os.windows.ws2_32.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; return .{ .index = index }; } if (builtin.link_libc) { - try pool.checkCancel(); + try t.checkCancel(); const index = std.c.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; return .{ .index = @bitCast(index) }; @@ -2593,8 +2593,8 @@ fn netInterfaceNameResolve( } fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); if (native_os == .linux) { _ = interface; @@ -2618,18 +2618,18 @@ fn netLookup( resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) void { - const pool: *Pool = @ptrCast(@alignCast(userdata)); - const pool_io = pool.io(); - resolved.putOneUncancelable(pool_io, .{ .end = netLookupFallible(pool, host_name, resolved, options) }); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = t.io(); + resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, host_name, resolved, options) }); } fn netLookupFallible( - pool: *Pool, + t: *Threaded, host_name: HostName, resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) !void { - const pool_io = pool.io(); + const t_io = t.io(); const name = host_name.bytes; assert(name.len <= HostName.max_len); @@ -2648,7 +2648,7 @@ fn netLookupFallible( if (native_os == .linux) { if (options.family != .ip4) { if (IpAddress.parseIp6(name, options.port)) |addr| { - try resolved.putAll(pool_io, &.{ + try resolved.putAll(t_io, &.{ .{ .address = addr }, .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, }); @@ -2658,7 +2658,7 @@ fn netLookupFallible( if (options.family != .ip6) { if (IpAddress.parseIp4(name, options.port)) |addr| { - try resolved.putAll(pool_io, &.{ + try resolved.putAll(t_io, &.{ .{ .address = addr }, .{ .canonical_name = copyCanon(options.canonical_name_buffer, name) }, }); @@ -2666,7 +2666,7 @@ fn netLookupFallible( } else |_| {} } - lookupHosts(pool, host_name, resolved, options) catch |err| switch (err) { + lookupHosts(t, host_name, resolved, options) catch |err| switch (err) { error.UnknownHostName => {}, else => |e| return e, }; @@ -2697,11 +2697,11 @@ fn netLookupFallible( canon_name_dest.* = canon_name.*; results_buffer[results_index] = .{ .canonical_name = .{ .bytes = canon_name_dest } }; results_index += 1; - try resolved.putAll(pool_io, results_buffer[0..results_index]); + try resolved.putAll(t_io, results_buffer[0..results_index]); return; } - return lookupDnsSearch(pool, host_name, resolved, options); + return lookupDnsSearch(t, host_name, resolved, options); } if (native_os == .openbsd) { @@ -2953,13 +2953,13 @@ fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNa } fn lookupDnsSearch( - pool: *Pool, + t: *Threaded, host_name: HostName, resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) HostName.LookupError!void { - const pool_io = pool.io(); - const rc = HostName.ResolvConf.init(pool_io) catch return error.ResolvConfParseFailed; + const t_io = t.io(); + const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed; // Count dots, suppress search when >=ndots or name ends in // a dot, which is an explicit request for global scope. @@ -2983,7 +2983,7 @@ fn lookupDnsSearch( while (it.next()) |token| { @memcpy(options.canonical_name_buffer[canon_name.len + 1 ..][0..token.len], token); const lookup_canon_name = options.canonical_name_buffer[0 .. canon_name.len + 1 + token.len]; - if (lookupDns(pool, lookup_canon_name, &rc, resolved, options)) |result| { + if (lookupDns(t, lookup_canon_name, &rc, resolved, options)) |result| { return result; } else |err| switch (err) { error.UnknownHostName => continue, @@ -2992,17 +2992,17 @@ fn lookupDnsSearch( } const lookup_canon_name = options.canonical_name_buffer[0..canon_name.len]; - return lookupDns(pool, lookup_canon_name, &rc, resolved, options); + return lookupDns(t, lookup_canon_name, &rc, resolved, options); } fn lookupDns( - pool: *Pool, + t: *Threaded, lookup_canon_name: []const u8, rc: *const HostName.ResolvConf, resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) HostName.LookupError!void { - const pool_io = pool.io(); + const t_io = t.io(); const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ .{ .af = .ip6, .rr = std.posix.RR.A }, .{ .af = .ip4, .rr = std.posix.RR.AAAA }, @@ -3032,7 +3032,7 @@ fn lookupDns( var socket = s: { if (any_ip6) ip6: { const ip6_addr: IpAddress = .{ .ip6 = .unspecified(0) }; - const socket = ip6_addr.bind(pool_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { + const socket = ip6_addr.bind(t_io, .{ .ip6_only = true, .mode = .dgram }) catch |err| switch (err) { error.AddressFamilyUnsupported => break :ip6, else => |e| return e, }; @@ -3040,10 +3040,10 @@ fn lookupDns( } any_ip6 = false; const ip4_addr: IpAddress = .{ .ip4 = .unspecified(0) }; - const socket = try ip4_addr.bind(pool_io, .{ .mode = .dgram }); + const socket = try ip4_addr.bind(t_io, .{ .mode = .dgram }); break :s socket; }; - defer socket.close(pool_io); + defer socket.close(t_io); const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); const queries = queries_buffer[0..nq]; @@ -3054,13 +3054,13 @@ fn lookupDns( // boot clock is chosen because time the computer is suspended should count // against time spent waiting for external messages to arrive. const clock: Io.Clock = .boot; - var now_ts = try clock.now(pool_io); + var now_ts = try clock.now(t_io); const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); const attempt_duration: Io.Duration = .{ .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, }; - send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(pool_io)) { + send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(t_io)) { const max_messages = queries_buffer.len * HostName.ResolvConf.max_nameservers; { var message_buffer: [max_messages]Io.net.OutgoingMessage = undefined; @@ -3076,7 +3076,7 @@ fn lookupDns( message_i += 1; } } - _ = netSend(pool, socket.handle, message_buffer[0..message_i], .{}); + _ = netSend(t, socket.handle, message_buffer[0..message_i], .{}); } const timeout: Io.Timeout = .{ .deadline = .{ @@ -3087,7 +3087,7 @@ fn lookupDns( while (true) { var message_buffer: [max_messages]Io.net.IncomingMessage = undefined; const buf = answer_buffer[answer_buffer_i..]; - const recv_err, const recv_n = socket.receiveManyTimeout(pool_io, &message_buffer, buf, .{}, timeout); + const recv_err, const recv_n = socket.receiveManyTimeout(t_io, &message_buffer, buf, .{}, timeout); for (message_buffer[0..recv_n]) |*received_message| { const reply = received_message.data; // Ignore non-identifiable packets. @@ -3124,7 +3124,7 @@ fn lookupDns( .data_ptr = query.ptr, .data_len = query.len, }; - _ = netSend(pool, socket.handle, (&retry_message)[0..1], .{}); + _ = netSend(t, socket.handle, (&retry_message)[0..1], .{}); continue; }, else => continue, @@ -3155,7 +3155,7 @@ fn lookupDns( std.posix.RR.A => { const data = record.packet[record.data_off..][0..record.data_len]; if (data.len != 4) return error.InvalidDnsARecord; - try resolved.putOne(pool_io, .{ .address = .{ .ip4 = .{ + try resolved.putOne(t_io, .{ .address = .{ .ip4 = .{ .bytes = data[0..4].*, .port = options.port, } } }); @@ -3164,7 +3164,7 @@ fn lookupDns( std.posix.RR.AAAA => { const data = record.packet[record.data_off..][0..record.data_len]; if (data.len != 16) return error.InvalidDnsAAAARecord; - try resolved.putOne(pool_io, .{ .address = .{ .ip6 = .{ + try resolved.putOne(t_io, .{ .address = .{ .ip6 = .{ .bytes = data[0..16].*, .port = options.port, } } }); @@ -3178,18 +3178,18 @@ fn lookupDns( }; } - try resolved.putOne(pool_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } }); + try resolved.putOne(t_io, .{ .canonical_name = canonical_name orelse .{ .bytes = lookup_canon_name } }); if (addresses_len == 0) return error.NameServerFailure; } fn lookupHosts( - pool: *Pool, + t: *Threaded, host_name: HostName, resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) !void { - const pool_io = pool.io(); - const file = Io.File.openAbsolute(pool_io, "/etc/hosts", .{}) catch |err| switch (err) { + const t_io = t.io(); + const file = Io.File.openAbsolute(t_io, "/etc/hosts", .{}) catch |err| switch (err) { error.FileNotFound, error.NotDir, error.AccessDenied, @@ -3202,11 +3202,11 @@ fn lookupHosts( return error.DetectingNetworkConfigurationFailed; }, }; - defer file.close(pool_io); + defer file.close(t_io); var line_buf: [512]u8 = undefined; - var file_reader = file.reader(pool_io, &line_buf); - return lookupHostsReader(pool, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) { + var file_reader = file.reader(t_io, &line_buf); + return lookupHostsReader(t, host_name, resolved, options, &file_reader.interface) catch |err| switch (err) { error.ReadFailed => switch (file_reader.err.?) { error.Canceled => |e| return e, else => { @@ -3220,13 +3220,13 @@ fn lookupHosts( } fn lookupHostsReader( - pool: *Pool, + t: *Threaded, host_name: HostName, resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, reader: *Io.Reader, ) error{ ReadFailed, Canceled, UnknownHostName }!void { - const pool_io = pool.io(); + const t_io = t.io(); var addresses_len: usize = 0; var canonical_name: ?HostName = null; while (true) { @@ -3268,19 +3268,19 @@ fn lookupHostsReader( if (options.family != .ip6) { if (IpAddress.parseIp4(ip_text, options.port)) |addr| { - try resolved.putOne(pool_io, .{ .address = addr }); + try resolved.putOne(t_io, .{ .address = addr }); addresses_len += 1; } else |_| {} } if (options.family != .ip4) { if (IpAddress.parseIp6(ip_text, options.port)) |addr| { - try resolved.putOne(pool_io, .{ .address = addr }); + try resolved.putOne(t_io, .{ .address = addr }); addresses_len += 1; } else |_| {} } } - if (canonical_name) |canon_name| try resolved.putOne(pool_io, .{ .canonical_name = canon_name }); + if (canonical_name) |canon_name| try resolved.putOne(t_io, .{ .canonical_name = canon_name }); if (addresses_len == 0) return error.UnknownHostName; } From 060fd975d95d4472f98bb2c7760afb111d162580 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 13:48:51 -0700 Subject: [PATCH 114/244] std.Io.Group: add cancellation support to "wait" --- BRANCH_TODO | 2 +- lib/std/Io.zig | 20 +++- lib/std/Io/Threaded.zig | 224 ++++++++++++++++++++++++++++++++---- lib/std/Io/net/HostName.zig | 2 +- 4 files changed, 216 insertions(+), 32 deletions(-) diff --git a/BRANCH_TODO b/BRANCH_TODO index d09d2f9273..e43cb401c1 100644 --- a/BRANCH_TODO +++ b/BRANCH_TODO @@ -3,7 +3,7 @@ * Threaded: finish windows impl * Threaded: glibc impl of netLookup -* fix Group.wait not handling cancelation (need to move impl of ResetEvent to Threaded) +* eliminate dependency on std.Thread (Mutex, Condition, maybe more) * implement cancelRequest for non-linux posix * finish converting all Threaded into directly calling system functions and handling EINTR * audit the TODOs diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 2e15c4b8cb..bcaf643f14 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -641,12 +641,13 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (*Group, context: *const anyopaque) void, ) void, - groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, + groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void, + groupWaitUncancelable: *const fn (?*anyopaque, *Group, token: *anyopaque) void, groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, /// Blocks until one of the futures from the list has a result ready, such /// that awaiting it will not block. Returns that index. - select: *const fn (?*anyopaque, futures: []const *AnyFuture) usize, + select: *const fn (?*anyopaque, futures: []const *AnyFuture) Cancelable!usize, mutexLock: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) Cancelable!void, mutexLockUncancelable: *const fn (?*anyopaque, prev_state: Mutex.State, mutex: *Mutex) void, @@ -1017,10 +1018,19 @@ pub const Group = struct { /// Blocks until all tasks of the group finish. /// /// Idempotent. Not threadsafe. - pub fn wait(g: *Group, io: Io) void { + pub fn wait(g: *Group, io: Io) Cancelable!void { const token = g.token orelse return; g.token = null; - io.vtable.groupWait(io.userdata, g, token); + return io.vtable.groupWait(io.userdata, g, token); + } + + /// Equivalent to `wait` except uninterruptible. + /// + /// Idempotent. Not threadsafe. + pub fn waitUncancelable(g: *Group, io: Io) void { + const token = g.token orelse return; + g.token = null; + io.vtable.groupWaitUncancelable(io.userdata, g, token); } /// Equivalent to `wait` but requests cancellation on all tasks owned by @@ -1095,7 +1105,7 @@ pub fn Select(comptime U: type) type { /// Asserts there is at least one more `outstanding` task. /// /// Not threadsafe. - pub fn wait(s: *S) Io.Cancelable!U { + pub fn wait(s: *S) Cancelable!U { s.outstanding -= 1; return s.queue.getOne(s.io); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 76d1e49daf..b8d05ea10c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -13,7 +13,6 @@ const IpAddress = std.Io.net.IpAddress; const Allocator = std.mem.Allocator; const assert = std.debug.assert; const posix = std.posix; -const ResetEvent = std.Thread.ResetEvent; /// Thread-safe. allocator: Allocator, @@ -153,8 +152,10 @@ pub fn io(t: *Threaded) Io { .cancel = cancel, .cancelRequested = cancelRequested, .select = select, + .groupAsync = groupAsync, .groupWait = groupWait, + .groupWaitUncancelable = groupWaitUncancelable, .groupCancel = groupCancel, .mutexLock = mutexLock, @@ -300,7 +301,7 @@ const AsyncClosure = struct { } fn waitAndFree(ac: *AsyncClosure, gpa: Allocator, result: []u8) void { - ac.reset_event.wait(); + ac.reset_event.waitUncancelable(); @memcpy(result, ac.resultPointer()[0..result.len]); free(ac, gpa, result.len); } @@ -472,7 +473,7 @@ const GroupClosure = struct { assert(cancel_tid == Closure.canceling_tid); // We already know the task is canceled before running the callback. Since all closures // in a Group have void return type, we can return early. - std.Thread.WaitGroup.finishStateless(group_state, reset_event); + syncFinish(group_state, reset_event); return; } current_closure = closure; @@ -485,7 +486,7 @@ const GroupClosure = struct { assert(cancel_tid == Closure.canceling_tid); } - std.Thread.WaitGroup.finishStateless(group_state, reset_event); + syncFinish(group_state, reset_event); } fn free(gc: *GroupClosure, gpa: Allocator) void { @@ -505,6 +506,32 @@ const GroupClosure = struct { const base: [*]u8 = @ptrCast(gc); return base + contextOffset(gc.context_alignment); } + + const sync_is_waiting: usize = 1 << 0; + const sync_one_pending: usize = 1 << 1; + + fn syncStart(state: *std.atomic.Value(usize)) void { + const prev_state = state.fetchAdd(sync_one_pending, .monotonic); + assert((prev_state / sync_one_pending) < (std.math.maxInt(usize) / sync_one_pending)); + } + + fn syncFinish(state: *std.atomic.Value(usize), event: *ResetEvent) void { + const prev_state = state.fetchSub(sync_one_pending, .acq_rel); + assert((prev_state / sync_one_pending) > 0); + if (prev_state == (sync_one_pending | sync_is_waiting)) event.set(); + } + + fn syncWait(t: *Threaded, state: *std.atomic.Value(usize), event: *ResetEvent) Io.Cancelable!void { + const prev_state = state.fetchAdd(sync_is_waiting, .acquire); + assert(prev_state & sync_is_waiting == 0); + if ((prev_state / sync_one_pending) > 0) try event.wait(t); + } + + fn syncWaitUncancelable(state: *std.atomic.Value(usize), event: *ResetEvent) void { + const prev_state = state.fetchAdd(sync_is_waiting, .acquire); + assert(prev_state & sync_is_waiting == 0); + if ((prev_state / sync_one_pending) > 0) event.waitUncancelable(); + } }; fn groupAsync( @@ -566,22 +593,40 @@ fn groupAsync( // This needs to be done before unlocking the mutex to avoid a race with // the associated task finishing. const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); - std.Thread.WaitGroup.startStateless(group_state); + GroupClosure.syncStart(group_state); t.mutex.unlock(); t.cond.signal(); } -fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) Io.Cancelable!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const gpa = t.allocator; if (builtin.single_threaded) return; - // TODO these primitives are too high level, need to check cancel on EINTR const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); - std.Thread.WaitGroup.waitStateless(group_state, reset_event); + try GroupClosure.syncWait(t, group_state, reset_event); + + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + const node_next = node.next; + gc.free(gpa); + node = node_next orelse break; + } +} + +fn groupWaitUncancelable(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const gpa = t.allocator; + + if (builtin.single_threaded) return; + + const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); + const reset_event: *ResetEvent = @ptrCast(&group.context); + GroupClosure.syncWaitUncancelable(group_state, reset_event); var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); while (true) { @@ -609,7 +654,7 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); - std.Thread.WaitGroup.waitStateless(group_state, reset_event); + GroupClosure.syncWaitUncancelable(group_state, reset_event); { var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); @@ -661,22 +706,20 @@ fn checkCancel(t: *Threaded) error{Canceled}!void { fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); if (prev_state == .contended) { - try t.checkCancel(); - futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + try futexWait(t, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { - try t.checkCancel(); - futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + try futexWait(t, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } } fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { _ = userdata; if (prev_state == .contended) { - futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } while (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .contended, .acquire) != .unlocked) { - futexWait(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); + futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); } } @@ -708,7 +751,7 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: defer mutex.lockUncancelable(t_io); while (true) { - futexWait(cond_epoch, epoch); + futexWaitUncancelable(cond_epoch, epoch); epoch = cond_epoch.load(.acquire); state = cond_state.load(.monotonic); while (state & signal_mask != 0) { @@ -747,8 +790,7 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I defer mutex.lockUncancelable(t.io()); while (true) { - try t.checkCancel(); - futexWait(cond_epoch, epoch); + try futexWait(t, cond_epoch, epoch); epoch = cond_epoch.load(.acquire); state = cond_state.load(.monotonic); @@ -1708,9 +1750,8 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } } -fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; var reset_event: ResetEvent = .unset; @@ -1720,20 +1761,20 @@ fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) usize { for (futures[0..i]) |cleanup_future| { const cleanup_closure: *AsyncClosure = @ptrCast(@alignCast(cleanup_future)); if (@atomicRmw(?*ResetEvent, &cleanup_closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { - cleanup_closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. + cleanup_closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event. } } return i; } } - reset_event.wait(); + try reset_event.wait(t); var result: ?usize = null; for (futures, 0..) |future, i| { const closure: *AsyncClosure = @ptrCast(@alignCast(future)); if (@atomicRmw(?*ResetEvent, &closure.select_condition, .Xchg, null, .seq_cst) == AsyncClosure.done_reset_event) { - closure.reset_event.wait(); // Ensure no reference to our stack-allocated reset_event. + closure.reset_event.waitUncancelable(); // Ensure no reference to our stack-allocated reset_event. if (result == null) result = i; // In case multiple are ready, return first. } } @@ -3320,7 +3361,29 @@ fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) Hos return .{ .bytes = dest }; } -pub fn futexWait(ptr: *const std.atomic.Value(u32), expect: u32) void { +fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Cancelable!void { + @branchHint(.cold); + + if (native_os == .linux) { + const linux = std.os.linux; + try t.checkCancel(); + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); + if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .INVAL => {}, // possibly timeout overflow + .TIMEDOUT => unreachable, + .FAULT => unreachable, // ptr was invalid + else => unreachable, + }; + return; + } + + @compileError("TODO"); +} + +pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) void { @branchHint(.cold); if (native_os == .linux) { @@ -3341,7 +3404,7 @@ pub fn futexWait(ptr: *const std.atomic.Value(u32), expect: u32) void { @compileError("TODO"); } -pub fn futexWaitDuration(ptr: *const std.atomic.Value(u32), expect: u32, timeout: Io.Duration) void { +pub fn futexWaitDurationUncancelable(ptr: *const std.atomic.Value(u32), expect: u32, timeout: Io.Duration) void { @branchHint(.cold); if (native_os == .linux) { @@ -3384,3 +3447,114 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { @compileError("TODO"); } + +/// A thread-safe logical boolean value which can be `set` and `unset`. +/// +/// It can also block threads until the value is set with cancelation via timed +/// waits. Statically initializable; four bytes on all targets. +pub const ResetEvent = enum(u32) { + unset = 0, + waiting = 1, + is_set = 2, + + /// Returns whether the logical boolean is `set`. + /// + /// Once `reset` is called, this returns false until the next `set`. + /// + /// The memory accesses before the `set` can be said to happen before + /// `isSet` returns true. + pub fn isSet(re: *const ResetEvent) bool { + if (builtin.single_threaded) return switch (re.*) { + .unset => false, + .waiting => unreachable, + .is_set => true, + }; + // Acquire barrier ensures memory accesses before `set` happen before + // returning true. + return @atomicLoad(ResetEvent, re, .acquire) == .is_set; + } + + /// Blocks the calling thread until `set` is called. + /// + /// This is effectively a more efficient version of `while (!isSet()) {}`. + /// + /// The memory accesses before the `set` can be said to happen before `wait` returns. + pub fn wait(re: *ResetEvent, t: *Threaded) Io.Cancelable!void { + if (builtin.single_threaded) switch (re.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + if (re.isSet()) { + @branchHint(.likely); + return; + } + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEvent. Avoid using + // any strict barriers until we know the ResetEvent is set. + var state = @atomicLoad(ResetEvent, re, .acquire); + if (state == .unset) { + state = @cmpxchgStrong(ResetEvent, re, state, .waiting, .acquire, .acquire) orelse .waiting; + } + while (state == .waiting) { + try futexWait(t, @ptrCast(re), @intFromEnum(ResetEvent.waiting)); + state = @atomicLoad(ResetEvent, re, .acquire); + } + assert(state == .is_set); + } + + /// Same as `wait` except uninterruptible. + pub fn waitUncancelable(re: *ResetEvent) void { + if (builtin.single_threaded) switch (re.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + if (re.isSet()) { + @branchHint(.likely); + return; + } + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEvent. Avoid using + // any strict barriers until we know the ResetEvent is set. + var state = @atomicLoad(ResetEvent, re, .acquire); + if (state == .unset) { + state = @cmpxchgStrong(ResetEvent, re, state, .waiting, .acquire, .acquire) orelse .waiting; + } + while (state == .waiting) { + futexWaitUncancelable(@ptrCast(re), @intFromEnum(ResetEvent.waiting)); + state = @atomicLoad(ResetEvent, re, .acquire); + } + assert(state == .is_set); + } + + /// Marks the logical boolean as `set` and unblocks any threads in `wait` + /// or `timedWait` to observe the new state. + /// + /// The logical boolean stays `set` until `reset` is called, making future + /// `set` calls do nothing semantically. + /// + /// The memory accesses before `set` can be said to happen before `isSet` + /// returns true or `wait`/`timedWait` return successfully. + pub fn set(re: *ResetEvent) void { + if (builtin.single_threaded) { + re.* = .is_set; + return; + } + if (@atomicRmw(ResetEvent, re, .Xchg, .is_set, .release) == .waiting) { + futexWake(@ptrCast(re), std.math.maxInt(u32)); + } + } + + /// Unmarks the ResetEvent as if `set` was never called. + /// + /// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent + /// calls to `set`, `isSet` and `reset` are allowed. + pub fn reset(re: *ResetEvent) void { + if (builtin.single_threaded) { + re.* = .unset; + return; + } + @atomicStore(ResetEvent, re, .unset, .monotonic); + } +}; diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 6352d82dd0..4c64b4dec0 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -273,7 +273,7 @@ pub fn connectMany( .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), .canonical_name => continue, .end => |lookup_result| { - group.wait(io); + group.waitUncancelable(io); results.putOneUncancelable(io, .{ .end = lookup_result }); return; }, From 81b1bfbfbbf4f71bc79191ca36ac62c00c8ac92c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 14:09:25 -0700 Subject: [PATCH 115/244] std.Io.Threaded: wrangle TODOs --- BRANCH_TODO | 21 --------------------- lib/std/Io/Threaded.zig | 14 ++++++-------- 2 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 BRANCH_TODO diff --git a/BRANCH_TODO b/BRANCH_TODO deleted file mode 100644 index e43cb401c1..0000000000 --- a/BRANCH_TODO +++ /dev/null @@ -1,21 +0,0 @@ -* Threaded: finish linux impl (all tests passing) -* Threaded: finish macos impl -* Threaded: finish windows impl -* Threaded: glibc impl of netLookup - -* eliminate dependency on std.Thread (Mutex, Condition, maybe more) -* implement cancelRequest for non-linux posix -* finish converting all Threaded into directly calling system functions and handling EINTR -* audit the TODOs - -* move max_iovecs_len to std.Io -* address the cancelation race condition (signal received between checkCancel and syscall) -* update signal values to be an enum -* delete the deprecated fs.File functions -* move fs.File.Writer to Io -* add non-blocking flag to net and fs operations, handle EAGAIN -* finish moving std.fs to Io -* migrate child process into std.Io -* eliminate std.Io.poll (it should be replaced by "select" functionality) -* finish moving all of std.posix into Threaded -* TCP fastopen - sends initial payload along with connection. can be done for idempotent http requests diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index b8d05ea10c..0df6cdccff 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -133,7 +133,6 @@ fn worker(t: *Threaded) void { closure.start(closure); t.mutex.lock(); if (is_concurrent) { - // TODO also pop thread and join sometimes t.concurrent_count -= 1; } } @@ -1175,7 +1174,7 @@ fn dirCreateFilePosix( fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { try t.checkCancel(); - switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { .SUCCESS => break, .INTR => continue, else => |err| return posix.unexpectedErrno(err), @@ -1304,7 +1303,7 @@ fn dirOpenFile( fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { try t.checkCancel(); - switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { .SUCCESS => break, .INTR => continue, else => |err| return posix.unexpectedErrno(err), @@ -2263,7 +2262,6 @@ fn netSendOne( .WSAEDESTADDRREQ => unreachable, // A destination address is required. .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. .WSAEHOSTUNREACH => return error.NetworkUnreachable, - // TODO: WSAEINPROGRESS, WSAEINTR .WSAEINVAL => unreachable, .WSAENETDOWN => return error.NetworkDown, .WSAENETRESET => return error.ConnectionResetByPeer, @@ -3186,11 +3184,11 @@ fn lookupDns( for (answers) |answer| { var it = HostName.DnsResponse.init(answer) catch { - // TODO accept a diagnostics struct and append warnings + // Here we could potentially add diagnostics to the results queue. continue; }; while (it.next() catch { - // TODO accept a diagnostics struct and append warnings + // Here we could potentially add diagnostics to the results queue. continue; }) |record| switch (record.rr) { std.posix.RR.A => { @@ -3239,7 +3237,7 @@ fn lookupHosts( error.Canceled => |e| return e, else => { - // TODO populate optional diagnostic struct + // Here we could add more detailed diagnostics to the results queue. return error.DetectingNetworkConfigurationFailed; }, }; @@ -3251,7 +3249,7 @@ fn lookupHosts( error.ReadFailed => switch (file_reader.err.?) { error.Canceled => |e| return e, else => { - // TODO populate optional diagnostic struct + // Here we could add more detailed diagnostics to the results queue. return error.DetectingNetworkConfigurationFailed; }, }, From 1e81c3a9259873fac197ed5dc6e2133f0a097243 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 14:47:41 -0700 Subject: [PATCH 116/244] std.Io: rename EventLoop to IoUring `std.Io.Evented` is introduced to select an appropriate Io implementation depending on OS --- lib/std/Io.zig | 5 ++++- lib/std/Io/{EventLoop.zig => IoUring.zig} | 0 2 files changed, 4 insertions(+), 1 deletion(-) rename lib/std/Io/{EventLoop.zig => IoUring.zig} (100%) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index bcaf643f14..f3a3074ca7 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -557,7 +557,10 @@ test { const Io = @This(); -pub const EventLoop = @import("Io/EventLoop.zig"); +pub const Evented = switch (builtin.os.tag) { + .linux => @import("Io/IoUring.zig"), + else => void, +}; pub const Threaded = @import("Io/Threaded.zig"); pub const net = @import("Io/net.zig"); diff --git a/lib/std/Io/EventLoop.zig b/lib/std/Io/IoUring.zig similarity index 100% rename from lib/std/Io/EventLoop.zig rename to lib/std/Io/IoUring.zig From bb1bf5b96f966696f88933a07513b001c99d66f5 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 15:00:12 -0700 Subject: [PATCH 117/244] std.Io: stub file writing rather than incorrect impl --- lib/std/Io.zig | 3 ++- lib/std/Io/File.zig | 17 ++++++----------- lib/std/Io/Threaded.zig | 33 +++++++++++++++++++++++++-------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index f3a3074ca7..d1fa7afc68 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -667,7 +667,8 @@ pub const VTable = struct { dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, fileClose: *const fn (?*anyopaque, File) void, - pwrite: *const fn (?*anyopaque, File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, + fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize, + fileWritePositional: *const fn (?*anyopaque, File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize, /// Returns 0 on end of stream. fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.ReadStreamingError!usize, /// Returns 0 on end of stream. diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 7bd3669e2e..715667b0d4 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -172,24 +172,19 @@ pub const ReadStreamingError = error{ pub const ReadPositionalError = ReadStreamingError || error{Unseekable}; pub fn readPositional(file: File, io: Io, buffer: []u8, offset: u64) ReadPositionalError!usize { - return io.vtable.pread(io.userdata, file, buffer, offset); + return io.vtable.fileReadPositional(io.userdata, file, buffer, offset); } -pub const WriteError = std.fs.File.WriteError || Io.Cancelable; +pub const WriteStreamingError = error{} || Io.UnexpectedError || Io.Cancelable; -pub fn write(file: File, io: Io, buffer: []const u8) WriteError!usize { +pub fn write(file: File, io: Io, buffer: []const u8) WriteStreamingError!usize { return @errorCast(file.pwrite(io, buffer, -1)); } -pub fn writeAll(file: File, io: Io, bytes: []const u8) WriteError!void { - var index: usize = 0; - while (index < bytes.len) index += try file.write(io, bytes[index..]); -} +pub const WritePositionalError = WriteStreamingError || error{Unseekable}; -pub const PWriteError = std.fs.File.PWriteError || Io.Cancelable; - -pub fn pwrite(file: File, io: Io, buffer: []const u8, offset: std.posix.off_t) PWriteError!usize { - return io.vtable.pwrite(io.userdata, file, buffer, offset); +pub fn writePositional(file: File, io: Io, buffer: []const u8, offset: u64) WritePositionalError!usize { + return io.vtable.fileWritePositional(io.userdata, file, buffer, offset); } pub fn openAbsolute(io: Io, absolute_path: []const u8, flags: OpenFlags) OpenError!File { diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 0df6cdccff..fb70c8af79 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -190,7 +190,8 @@ pub fn io(t: *Threaded) Io { }, .dirOpenFile = dirOpenFile, .fileClose = fileClose, - .pwrite = pwrite, + .fileWriteStreaming = fileWriteStreaming, + .fileWritePositional = fileWritePositional, .fileReadStreaming = fileReadStreaming, .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, @@ -1611,14 +1612,30 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr } } -fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: posix.off_t) Io.File.PWriteError!usize { +fn fileWritePositional( + userdata: ?*anyopaque, + file: Io.File, + buffer: [][]const u8, + offset: u64, +) Io.File.WritePositionalError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); - const fs_file: std.fs.File = .{ .handle = file.handle }; - return switch (offset) { - -1 => fs_file.write(buffer), - else => fs_file.pwrite(buffer, @bitCast(offset)), - }; + while (true) { + try t.checkCancel(); + _ = file; + _ = buffer; + _ = offset; + @panic("TODO"); + } +} + +fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, buffer: [][]const u8) Io.File.WriteStreamingError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + while (true) { + try t.checkCancel(); + _ = file; + _ = buffer; + @panic("TODO"); + } } fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { From f7d47aed47b3fb8f593c398a8e38e66e2d10e1c1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 18:32:58 -0700 Subject: [PATCH 118/244] README: LLVM-less builds are more capable now --- README.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5a346c24d7..04fa4c618e 100644 --- a/README.md +++ b/README.md @@ -76,23 +76,15 @@ This produces a `zig2` executable in the current working directory. This is a [without LLVM extensions](https://github.com/ziglang/zig/issues/16270), and is therefore lacking these features: - Release mode optimizations -- [aarch64 machine code backend](https://github.com/ziglang/zig/issues/21172) -- [@cImport](https://github.com/ziglang/zig/issues/20630) -- [zig translate-c](https://github.com/ziglang/zig/issues/20875) -- [Ability to compile assembly files](https://github.com/ziglang/zig/issues/21169) - [Some ELF linking features](https://github.com/ziglang/zig/issues/17749) -- [Most COFF/PE linking features](https://github.com/ziglang/zig/issues/17751) +- [Some COFF/PE linking features](https://github.com/ziglang/zig/issues/17751) - [Some WebAssembly linking features](https://github.com/ziglang/zig/issues/17750) -- [Ability to create import libs from def files](https://github.com/ziglang/zig/issues/17807) - [Ability to create static archives from object files](https://github.com/ziglang/zig/issues/9828) +- [Ability to compile assembly files](https://github.com/ziglang/zig/issues/21169) - Ability to compile C, C++, Objective-C, and Objective-C++ files -However, a compiler built this way does provide a C backend, which may be -useful for creating system packages of Zig projects using the system C -toolchain. **In this case, LLVM is not needed!** - -Furthermore, a compiler built this way provides an LLVM backend that produces -bitcode files, which may be compiled into object files via a system Clang +Even when built this way, Zig provides an LLVM backend that produces bitcode +files, which may be optimized and compiled into object files via a system Clang package. This can be used to produce system packages of Zig applications without the Zig package dependency on LLVM. From 031044b3994d4510f64bd08ecba0ab01a7ed48c6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 19:31:28 -0700 Subject: [PATCH 119/244] std: fix macos compilation errors --- lib/std/Io/IoUring.zig | 2 +- lib/std/Io/Threaded.zig | 82 +++++++++++++-------- lib/std/Io/net.zig | 9 ++- lib/std/c.zig | 2 +- lib/std/crypto/Certificate/Bundle.zig | 45 +++++------ lib/std/crypto/Certificate/Bundle/macos.zig | 12 +-- lib/std/debug/SelfInfo/MachO.zig | 3 + lib/std/fs.zig | 2 + lib/std/http/Client.zig | 23 +++--- lib/std/http/Server.zig | 8 +- lib/std/os/windows.zig | 2 +- lib/std/posix.zig | 52 ++++++------- 12 files changed, 138 insertions(+), 104 deletions(-) diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig index 84ea0035ae..c6dca1597d 100644 --- a/lib/std/Io/IoUring.zig +++ b/lib/std/Io/IoUring.zig @@ -1469,7 +1469,7 @@ fn pwrite(userdata: ?*anyopaque, file: Io.File, buffer: []const u8, offset: std. .OVERFLOW => return error.Unseekable, .BUSY => return error.DeviceBusy, .CONNRESET => return error.ConnectionResetByPeer, - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, else => |err| return std.posix.unexpectedErrno(err), } } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index fb70c8af79..6ceebc79e1 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -966,7 +966,7 @@ fn dirStatPathPosix( try t.checkCancel(); var stat = std.mem.zeroes(posix.Stat); switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) { - .SUCCESS => return statFromPosix(stat), + .SUCCESS => return statFromPosix(&stat), .INTR => continue, .INVAL => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), // Always a race condition. @@ -1166,8 +1166,9 @@ fn dirCreateFilePosix( if (has_flock_open_flags and flags.lock_nonblocking) { var fl_flags: usize = while (true) { try t.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { - .SUCCESS => break, + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), .INTR => continue, else => |err| return posix.unexpectedErrno(err), } @@ -1295,8 +1296,9 @@ fn dirOpenFile( if (has_flock_open_flags and flags.lock_nonblocking) { var fl_flags: usize = while (true) { try t.checkCancel(); - switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { - .SUCCESS => break, + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), .INTR => continue, else => |err| return posix.unexpectedErrno(err), } @@ -1755,7 +1757,7 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { .sec = std.math.maxInt(sec_type), .nsec = std.math.maxInt(nsec_type), }; - break :t timestampToPosix(d.duration.nanoseconds); + break :t timestampToPosix(d.raw.toNanoseconds()); }; while (true) { try t.checkCancel(); @@ -1850,6 +1852,7 @@ fn netListenUnix( error.ProtocolUnsupportedBySystem => return error.AddressFamilyUnsupported, error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported, error.SocketModeUnsupported => return error.AddressFamilyUnsupported, + error.OptionUnsupported => return error.Unexpected, else => |e| return e, }; errdefer posix.close(socket_fd); @@ -2037,7 +2040,10 @@ fn netConnectUnix( ) net.UnixAddress.ConnectError!net.Socket.Handle { if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); - const socket_fd = try openSocketPosix(t, posix.AF.UNIX, .{ .mode = .stream }); + const socket_fd = openSocketPosix(t, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + error.OptionUnsupported => return error.Unexpected, + else => |e| return e, + }; errdefer posix.close(socket_fd); var storage: UnixAddress = undefined; const addr_len = addressUnixToPosix(address, &storage); @@ -2064,7 +2070,22 @@ fn netBindIpPosix( }; } -fn openSocketPosix(t: *Threaded, family: posix.sa_family_t, options: IpAddress.BindOptions) !posix.socket_t { +fn openSocketPosix( + t: *Threaded, + family: posix.sa_family_t, + options: IpAddress.BindOptions, +) error{ + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + SystemResources, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + OptionUnsupported, + Unexpected, + Canceled, +}!posix.socket_t { const mode = posixSocketMode(options.mode); const protocol = posixProtocol(options.protocol); const socket_fd = while (true) { @@ -2209,7 +2230,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. .NOTCONN => return error.SocketUnconnected, .CONNRESET => return error.ConnectionResetByPeer, .TIMEDOUT => return error.Timeout, - .PIPE => return error.BrokenPipe, + .PIPE => return error.SocketUnconnected, .NETDOWN => return error.NetworkDown, else => |err| return posix.unexpectedErrno(err), } @@ -2253,29 +2274,29 @@ fn netSendOne( flags: u32, ) net.Socket.SendError!void { var addr: PosixAddress = undefined; - var iovec: posix.iovec = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; - const msg: posix.msghdr = .{ + var iovec: posix.iovec_const = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + const msg: posix.msghdr_const = .{ .name = &addr.any, .namelen = addressToPosix(message.address, &addr), - .iov = iovec[0..1], + .iov = (&iovec)[0..1], .iovlen = 1, .control = @constCast(message.control.ptr), - .controllen = message.control.len, + .controllen = @intCast(message.control.len), .flags = 0, }; while (true) { try t.checkCancel(); - const rc = posix.system.sendmsg(handle, msg, flags); + const rc = posix.system.sendmsg(handle, &msg, flags); if (is_windows) { if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { .WSAEACCES => return error.AccessDenied, .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageTooBig, + .WSAEMSGSIZE => return error.MessageOversize, .WSAENOBUFS => return error.SystemResources, .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, + .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, .WSAEDESTADDRREQ => unreachable, // A destination address is required. .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. .WSAEHOSTUNREACH => return error.NetworkUnreachable, @@ -2285,7 +2306,6 @@ fn netSendOne( .WSAENETUNREACH => return error.NetworkUnreachable, .WSAENOTCONN => return error.SocketUnconnected, .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .WSAEWOULDBLOCK => return error.WouldBlock, .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. else => |err| return windows.unexpectedWSAError(err), } @@ -2299,28 +2319,24 @@ fn netSendOne( message.data_len = @intCast(rc); return; }, + .INTR => continue, + .ACCES => return error.AccessDenied, - .AGAIN => return error.WouldBlock, .ALREADY => return error.FastOpenAlreadyInProgress, .BADF => |err| return errnoBug(err), .CONNRESET => return error.ConnectionResetByPeer, .DESTADDRREQ => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), - .INTR => continue, .INVAL => |err| return errnoBug(err), .ISCONN => |err| return errnoBug(err), - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTSOCK => |err| return errnoBug(err), .OPNOTSUPP => |err| return errnoBug(err), - .PIPE => return error.BrokenPipe, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, - .LOOP => return error.SymLinkLoop, - .NAMETOOLONG => return error.NameTooLong, - .NOENT => return error.FileNotFound, - .NOTDIR => return error.NotDir, - .HOSTUNREACH => return error.NetworkUnreachable, + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTCONN => return error.SocketUnconnected, .NETDOWN => return error.NetworkDown, @@ -2447,7 +2463,7 @@ fn netReceive( .iov = (&iov)[0..1], .iovlen = 1, .control = message.control.ptr, - .controllen = message.control.len, + .controllen = @intCast(message.control.len), .flags = undefined, }; @@ -2465,7 +2481,7 @@ fn netReceive( .trunc = (msg.flags & posix.MSG.TRUNC) != 0, .ctrunc = (msg.flags & posix.MSG.CTRUNC) != 0, .oob = (msg.flags & posix.MSG.OOB) != 0, - .errqueue = (msg.flags & posix.MSG.ERRQUEUE) != 0, + .errqueue = if (@hasDecl(posix.MSG, "ERRQUEUE")) (msg.flags & posix.MSG.ERRQUEUE) != 0 else false, }, }; message_i += 1; @@ -2605,6 +2621,7 @@ fn netInterfaceNameResolve( error.ProtocolUnsupportedBySystem => return error.Unexpected, error.ProtocolUnsupportedByAddressFamily => return error.Unexpected, error.SocketModeUnsupported => return error.Unexpected, + error.OptionUnsupported => return error.Unexpected, else => |e| return e, }; defer posix.close(sock_fd); @@ -3079,9 +3096,10 @@ fn lookupDns( } } - var ip4_mapped: [HostName.ResolvConf.max_nameservers]IpAddress = undefined; + var ip4_mapped_buffer: [HostName.ResolvConf.max_nameservers]IpAddress = undefined; + const ip4_mapped = ip4_mapped_buffer[0..rc.nameservers_len]; var any_ip6 = false; - for (rc.nameservers(), &ip4_mapped) |*ns, *m| { + for (rc.nameservers(), ip4_mapped) |*ns, *m| { m.* = .{ .ip6 = .fromAny(ns.*) }; any_ip6 = any_ip6 or ns.* == .ip6; } @@ -3101,7 +3119,7 @@ fn lookupDns( }; defer socket.close(t_io); - const mapped_nameservers = if (any_ip6) ip4_mapped[0..rc.nameservers_len] else rc.nameservers(); + const mapped_nameservers = if (any_ip6) ip4_mapped else rc.nameservers(); const queries = queries_buffer[0..nq]; const answers = answers_buffer[0..queries.len]; var answers_remaining = answers.len; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 6b407eb504..81ad077667 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -209,6 +209,9 @@ pub const IpAddress = union(enum) { ProtocolUnsupportedBySystem, ProtocolUnsupportedByAddressFamily, SocketModeUnsupported, + /// One of the `ListenOptions` is not supported by the Io + /// implementation. + OptionUnsupported, } || Io.UnexpectedError || Io.Cancelable; pub const ListenOptions = struct { @@ -1057,6 +1060,9 @@ pub const Socket = struct { /// Local end has been shut down on a connection-oriented socket, or /// the socket was never connected. SocketUnconnected, + /// An attempt was made to send to a network/broadcast address as + /// though it was a unicast address. + AccessDenied, } || Io.UnexpectedError || Io.Cancelable; /// Transfers `data` to `dest`, connectionless, in one packet. @@ -1167,7 +1173,6 @@ pub const Stream = struct { pub const Error = error{ SystemResources, - BrokenPipe, ConnectionResetByPeer, Timeout, SocketUnconnected, @@ -1233,7 +1238,7 @@ pub const Stream = struct { pub const Error = std.posix.SendMsgError || error{ ConnectionResetByPeer, SocketNotBound, - MessageTooBig, + MessageOversize, NetworkDown, SystemResources, SocketUnconnected, diff --git a/lib/std/c.zig b/lib/std/c.zig index 24ad58e242..b96ca2e458 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -4104,7 +4104,7 @@ pub const msghdr = switch (native_os) { .visionos, .watchos, .serenity, // https://github.com/SerenityOS/serenity/blob/ac44ec5ebc707f9dd0c3d4759a1e17e91db5d74f/Kernel/API/POSIX/sys/socket.h#L74-L82 - => private.posix_msghdr, + => posix_msghdr, else => void, }; diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index 1a96688a0f..e2090e01ac 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -70,18 +70,18 @@ pub const RescanError = RescanLinuxError || RescanMacError || RescanWithPathErro /// file system standard locations for certificates. /// For operating systems that do not have standard CA installations to be /// found, this function clears the set of certificates. -pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io) RescanError!void { +pub fn rescan(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanError!void { switch (builtin.os.tag) { - .linux => return rescanLinux(cb, gpa, io), - .macos => return rescanMac(cb, gpa), - .freebsd, .openbsd => return rescanWithPath(cb, gpa, io, "/etc/ssl/cert.pem"), - .netbsd => return rescanWithPath(cb, gpa, io, "/etc/openssl/certs/ca-certificates.crt"), - .dragonfly => return rescanWithPath(cb, gpa, io, "/usr/local/etc/ssl/cert.pem"), - .illumos => return rescanWithPath(cb, gpa, io, "/etc/ssl/cacert.pem"), - .haiku => return rescanWithPath(cb, gpa, io, "/boot/system/data/ssl/CARootCertificates.pem"), + .linux => return rescanLinux(cb, gpa, io, now), + .macos => return rescanMac(cb, gpa, io, now), + .freebsd, .openbsd => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cert.pem"), + .netbsd => return rescanWithPath(cb, gpa, io, now, "/etc/openssl/certs/ca-certificates.crt"), + .dragonfly => return rescanWithPath(cb, gpa, io, now, "/usr/local/etc/ssl/cert.pem"), + .illumos => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/cacert.pem"), + .haiku => return rescanWithPath(cb, gpa, io, now, "/boot/system/data/ssl/CARootCertificates.pem"), // https://github.com/SerenityOS/serenity/blob/222acc9d389bc6b490d4c39539761b043a4bfcb0/Ports/ca-certificates/package.sh#L19 - .serenity => return rescanWithPath(cb, gpa, io, "/etc/ssl/certs/ca-certificates.crt"), - .windows => return rescanWindows(cb, gpa), + .serenity => return rescanWithPath(cb, gpa, io, now, "/etc/ssl/certs/ca-certificates.crt"), + .windows => return rescanWindows(cb, gpa, io, now), else => {}, } } @@ -91,7 +91,7 @@ const RescanMacError = @import("Bundle/macos.zig").RescanMacError; const RescanLinuxError = AddCertsFromFilePathError || AddCertsFromDirPathError; -fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io) RescanLinuxError!void { +fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanLinuxError!void { // Possible certificate files; stop after finding one. const cert_file_paths = [_][]const u8{ "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. @@ -114,7 +114,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io) RescanLinuxError!void { scan: { for (cert_file_paths) |cert_file_path| { - if (addCertsFromFilePathAbsolute(cb, gpa, io, cert_file_path)) |_| { + if (addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path)) |_| { break :scan; } else |err| switch (err) { error.FileNotFound => continue, @@ -123,7 +123,7 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io) RescanLinuxError!void { } for (cert_dir_paths) |cert_dir_path| { - addCertsFromDirPathAbsolute(cb, gpa, io, cert_dir_path) catch |err| switch (err) { + addCertsFromDirPathAbsolute(cb, gpa, io, now, cert_dir_path) catch |err| switch (err) { error.FileNotFound => continue, else => |e| return e, }; @@ -135,10 +135,10 @@ fn rescanLinux(cb: *Bundle, gpa: Allocator, io: Io) RescanLinuxError!void { const RescanWithPathError = AddCertsFromFilePathError; -fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, cert_file_path: []const u8) RescanWithPathError!void { +fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, cert_file_path: []const u8) RescanWithPathError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); - try addCertsFromFilePathAbsolute(cb, gpa, io, cert_file_path); + try addCertsFromFilePathAbsolute(cb, gpa, io, now, cert_file_path); cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); } @@ -187,17 +187,18 @@ pub fn addCertsFromDirPathAbsolute( cb: *Bundle, gpa: Allocator, io: Io, + now: Io.Timestamp, abs_dir_path: []const u8, ) AddCertsFromDirPathError!void { assert(fs.path.isAbsolute(abs_dir_path)); var iterable_dir = try fs.openDirAbsolute(abs_dir_path, .{ .iterate = true }); defer iterable_dir.close(); - return addCertsFromDir(cb, gpa, io, iterable_dir); + return addCertsFromDir(cb, gpa, io, now, iterable_dir); } pub const AddCertsFromDirError = AddCertsFromFilePathError; -pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, iterable_dir: fs.Dir) AddCertsFromDirError!void { +pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, iterable_dir: fs.Dir) AddCertsFromDirError!void { var it = iterable_dir.iterate(); while (try it.next()) |entry| { switch (entry.kind) { @@ -205,7 +206,7 @@ pub fn addCertsFromDir(cb: *Bundle, gpa: Allocator, io: Io, iterable_dir: fs.Dir else => continue, } - try addCertsFromFilePath(cb, gpa, io, iterable_dir.adaptToNewApi(), entry.name); + try addCertsFromFilePath(cb, gpa, io, now, iterable_dir.adaptToNewApi(), entry.name); } } @@ -215,9 +216,9 @@ pub fn addCertsFromFilePathAbsolute( cb: *Bundle, gpa: Allocator, io: Io, + now: Io.Timestamp, abs_file_path: []const u8, ) AddCertsFromFilePathError!void { - const now = try Io.Clock.real.now(io); var file = try fs.openFileAbsolute(abs_file_path, .{}); defer file.close(); var file_reader = file.reader(io, &.{}); @@ -228,10 +229,10 @@ pub fn addCertsFromFilePath( cb: *Bundle, gpa: Allocator, io: Io, + now: Io.Timestamp, dir: Io.Dir, sub_file_path: []const u8, ) AddCertsFromFilePathError!void { - const now = try Io.Clock.real.now(io); var file = try dir.openFile(io, sub_file_path, .{}); defer file.close(io); var file_reader = file.reader(io, &.{}); @@ -335,5 +336,7 @@ test "scan for OS-provided certificates" { var bundle: Bundle = .{}; defer bundle.deinit(gpa); - try bundle.rescan(gpa, io); + const now = try Io.Clock.real.now(io); + + try bundle.rescan(gpa, io, now); } diff --git a/lib/std/crypto/Certificate/Bundle/macos.zig b/lib/std/crypto/Certificate/Bundle/macos.zig index 5aa842f245..d32f1be8e0 100644 --- a/lib/std/crypto/Certificate/Bundle/macos.zig +++ b/lib/std/crypto/Certificate/Bundle/macos.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const mem = std.mem; @@ -7,7 +8,7 @@ const Bundle = @import("../Bundle.zig"); pub const RescanMacError = Allocator.Error || fs.File.OpenError || fs.File.ReadError || fs.File.SeekError || Bundle.ParseCertError || error{EndOfStream}; -pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { +pub fn rescanMac(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanMacError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); @@ -16,6 +17,7 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { "/Library/Keychains/System.keychain", }; + _ = io; // TODO migrate file system to use std.Io for (keychain_paths) |keychain_path| { const bytes = std.fs.cwd().readFileAlloc(keychain_path, gpa, .limited(std.math.maxInt(u32))) catch |err| switch (err) { error.StreamTooLong => return error.FileTooBig, @@ -23,8 +25,8 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { }; defer gpa.free(bytes); - var reader: std.Io.Reader = .fixed(bytes); - scanReader(cb, gpa, &reader) catch |err| switch (err) { + var reader: Io.Reader = .fixed(bytes); + scanReader(cb, gpa, &reader, now.toSeconds()) catch |err| switch (err) { error.ReadFailed => unreachable, // prebuffered else => |e| return e, }; @@ -33,7 +35,7 @@ pub fn rescanMac(cb: *Bundle, gpa: Allocator) RescanMacError!void { cb.bytes.shrinkAndFree(gpa, cb.bytes.items.len); } -fn scanReader(cb: *Bundle, gpa: Allocator, reader: *std.Io.Reader) !void { +fn scanReader(cb: *Bundle, gpa: Allocator, reader: *Io.Reader, now_sec: i64) !void { const db_header = try reader.takeStruct(ApplDbHeader, .big); assert(mem.eql(u8, &db_header.signature, "kych")); @@ -49,8 +51,6 @@ fn scanReader(cb: *Bundle, gpa: Allocator, reader: *std.Io.Reader) !void { table_list[table_idx] = try reader.takeInt(u32, .big); } - const now_sec = std.time.timestamp(); - for (table_list) |table_offset| { reader.seek = db_header.schema_offset + table_offset; diff --git a/lib/std/debug/SelfInfo/MachO.zig b/lib/std/debug/SelfInfo/MachO.zig index a89a2f0fb5..8a0d9f0e1d 100644 --- a/lib/std/debug/SelfInfo/MachO.zig +++ b/lib/std/debug/SelfInfo/MachO.zig @@ -117,11 +117,14 @@ pub fn unwindFrame(si: *SelfInfo, gpa: Allocator, context: *UnwindContext) Error error.ReadFailed, error.OutOfMemory, error.Unexpected, + error.Canceled, => |e| return e, + error.UnsupportedRegister, error.UnsupportedAddrSize, error.UnimplementedUserOpcode, => return error.UnsupportedDebugInfo, + error.Overflow, error.EndOfStream, error.StreamTooLong, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index e03d426276..96aa962e67 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -458,6 +458,8 @@ pub const SelfExePathError = error{ /// On Windows, the volume does not contain a recognized file system. File /// system drivers might not be loaded, or the volume may be corrupt. UnrecognizedVolume, + + Canceled, } || posix.SysCtlError; /// `selfExePath` except allocates the result on the heap. diff --git a/lib/std/http/Client.zig b/lib/std/http/Client.zig index f05d6ff5b1..431f239db3 100644 --- a/lib/std/http/Client.zig +++ b/lib/std/http/Client.zig @@ -35,9 +35,11 @@ tls_buffer_size: if (disable_tls) u0 else usize = if (disable_tls) 0 else std.cr /// traffic over connections created with this `Client`. ssl_key_log: ?*std.crypto.tls.Client.SslKeyLog = null, -/// When this is `true`, the next time this client performs an HTTPS request, -/// it will first rescan the system for root certificates. -next_https_rescan_certs: bool = true, +/// The time used to decide whether certificates are expired. +/// +/// When this is `null`, the next time this client performs an HTTPS request, +/// it will first check the time and rescan the system for root certificates. +now: ?Io.Timestamp = null, /// The pool of connections that can be reused (and currently in use). connection_pool: ConnectionPool = .{}, @@ -295,6 +297,7 @@ pub const Connection = struct { client: std.crypto.tls.Client, connection: Connection, + /// Asserts that `client.now` is non-null. fn create( client: *Client, remote_host: HostName, @@ -320,7 +323,6 @@ pub const Connection = struct { const tls: *Tls = @ptrCast(base); var random_buffer: [176]u8 = undefined; std.crypto.random.bytes(&random_buffer); - const now_ts = if (Io.Clock.real.now(io)) |ts| ts.toSeconds() else |err| return err; tls.* = .{ .connection = .{ .client = client, @@ -333,7 +335,7 @@ pub const Connection = struct { .closing = false, .protocol = .tls, }, - // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true + // TODO data race here on ca_bundle if the user sets `now` to null .client = std.crypto.tls.Client.init( &tls.connection.stream_reader.interface, &tls.connection.stream_writer.interface, @@ -344,7 +346,7 @@ pub const Connection = struct { .read_buffer = tls_read_buffer, .write_buffer = socket_write_buffer, .entropy = &random_buffer, - .realtime_now_seconds = now_ts, + .realtime_now_seconds = client.now.?.toSeconds(), // This is appropriate for HTTPS because the HTTP headers contain // the content length which is used to detect truncation attacks. .allow_truncation_attacks = true, @@ -1687,14 +1689,15 @@ pub fn request( if (protocol == .tls) { if (disable_tls) unreachable; - if (@atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) { + { client.ca_bundle_mutex.lock(); defer client.ca_bundle_mutex.unlock(); - if (client.next_https_rescan_certs) { - client.ca_bundle.rescan(client.allocator, io) catch + if (client.now == null) { + const now = try Io.Clock.real.now(io); + client.now = now; + client.ca_bundle.rescan(client.allocator, io, now) catch return error.CertificateBundleLoadFailure; - @atomicStore(bool, &client.next_https_rescan_certs, false, .release); } } } diff --git a/lib/std/http/Server.zig b/lib/std/http/Server.zig index b64253f975..084f2f5855 100644 --- a/lib/std/http/Server.zig +++ b/lib/std/http/Server.zig @@ -688,7 +688,7 @@ pub const WebSocket = struct { pub const ReadSmallTextMessageError = error{ ConnectionClose, UnexpectedOpCode, - MessageTooBig, + MessageOversize, MissingMaskBit, ReadFailed, EndOfStream, @@ -717,15 +717,15 @@ pub const WebSocket = struct { _ => return error.UnexpectedOpCode, } - if (!h0.fin) return error.MessageTooBig; + if (!h0.fin) return error.MessageOversize; if (!h1.mask) return error.MissingMaskBit; const len: usize = switch (h1.payload_len) { .len16 => try in.takeInt(u16, .big), - .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageTooBig, + .len64 => std.math.cast(usize, try in.takeInt(u64, .big)) orelse return error.MessageOversize, else => @intFromEnum(h1.payload_len), }; - if (len > in.buffer.len) return error.MessageTooBig; + if (len > in.buffer.len) return error.MessageOversize; const mask: u32 = @bitCast((try in.takeArray(4)).*); const payload = try in.take(len); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 27b40fcd60..cbf49638a3 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1653,7 +1653,7 @@ pub fn WSASocketW( const rc = ws2_32.WSASocketW(af, socket_type, protocol, protocolInfo, g, dwFlags); if (rc == ws2_32.INVALID_SOCKET) { switch (ws2_32.WSAGetLastError()) { - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, + .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, .WSAEMFILE => return error.ProcessFdQuotaExceeded, .WSAENOBUFS => return error.SystemResources, .WSAEPROTONOSUPPORT => return error.ProtocolNotSupported, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 8844a8249e..f62c8c9edd 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1205,7 +1205,7 @@ pub const WriteError = error{ /// The socket type requires that message be sent atomically, and the size of the message /// to be sent made this impossible. The message is not transmitted. - MessageTooBig, + MessageOversize, } || UnexpectedError; /// Write to a file descriptor. @@ -1287,7 +1287,7 @@ pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize { .CONNRESET => return error.ConnectionResetByPeer, .BUSY => return error.DeviceBusy, .NXIO => return error.NoDevice, - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, else => |err| return unexpectedErrno(err), } } @@ -3487,7 +3487,7 @@ pub const SocketError = error{ AccessDenied, /// The implementation does not support the specified address family. - AddressFamilyNotSupported, + AddressFamilyUnsupported, /// Unknown protocol, or protocol family not available. ProtocolFamilyNotAvailable, @@ -3553,7 +3553,7 @@ pub fn socket(domain: u32, socket_type: u32, protocol: u32) SocketError!socket_t return fd; }, .ACCES => return error.AccessDenied, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .INVAL => return error.ProtocolFamilyNotAvailable, .MFILE => return error.ProcessFdQuotaExceeded, .NFILE => return error.SystemFdQuotaExceeded, @@ -3593,7 +3593,7 @@ pub fn socketpair(domain: u32, socket_type: u32, protocol: u32) SocketError![2]s return socks; }, .ACCES => return error.AccessDenied, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .INVAL => return error.ProtocolFamilyNotAvailable, .MFILE => return error.ProcessFdQuotaExceeded, .NFILE => return error.SystemFdQuotaExceeded, @@ -3676,7 +3676,7 @@ pub const BindError = error{ AddressNotAvailable, /// The address is not valid for the address family of socket. - AddressFamilyNotSupported, + AddressFamilyUnsupported, /// Too many symbolic links were encountered in resolving addr. SymLinkLoop, @@ -3733,7 +3733,7 @@ pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!voi .BADF => unreachable, // always a race condition if this error is returned .INVAL => unreachable, // invalid parameters .NOTSOCK => unreachable, // invalid `sockfd` - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .ADDRNOTAVAIL => return error.AddressNotAvailable, .FAULT => unreachable, // invalid `addr` pointer .LOOP => return error.SymLinkLoop, @@ -4192,7 +4192,7 @@ pub const ConnectError = error{ AddressNotAvailable, /// The passed address didn't have the correct address family in its sa_family field. - AddressFamilyNotSupported, + AddressFamilyUnsupported, /// Insufficient entries in the routing cache. SystemResources, @@ -4247,7 +4247,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .WSAEWOULDBLOCK => return error.WouldBlock, .WSAEACCES => unreachable, .WSAENOBUFS => return error.SystemResources, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, + .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, else => |err| return windows.unexpectedWSAError(err), } return; @@ -4260,7 +4260,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .PERM => return error.PermissionDenied, .ADDRINUSE => return error.AddressInUse, .ADDRNOTAVAIL => return error.AddressNotAvailable, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, .ALREADY => return error.ConnectionPending, .BADF => unreachable, // sockfd is not a valid open file descriptor. @@ -4322,7 +4322,7 @@ pub fn getsockoptError(sockfd: fd_t) ConnectError!void { .PERM => return error.PermissionDenied, .ADDRINUSE => return error.AddressInUse, .ADDRNOTAVAIL => return error.AddressNotAvailable, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN => return error.SystemResources, .ALREADY => return error.ConnectionPending, .BADF => unreachable, // sockfd is not a valid open file descriptor. @@ -6039,7 +6039,7 @@ pub const SendError = error{ /// The socket type requires that message be sent atomically, and the size of the message /// to be sent made this impossible. The message is not transmitted. - MessageTooBig, + MessageOversize, /// The output queue for a network interface was full. This generally indicates that the /// interface has stopped sending, but may be caused by transient congestion. (Normally, @@ -6066,7 +6066,7 @@ pub const SendError = error{ pub const SendMsgError = SendError || error{ /// The passed address didn't have the correct address family in its sa_family field. - AddressFamilyNotSupported, + AddressFamilyUnsupported, /// Returned when socket is AF.UNIX and the given path has a symlink loop. SymLinkLoop, @@ -6098,10 +6098,10 @@ pub fn sendmsg( .WSAEACCES => return error.AccessDenied, .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageTooBig, + .WSAEMSGSIZE => return error.MessageOversize, .WSAENOBUFS => return error.SystemResources, .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, + .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, .WSAEDESTADDRREQ => unreachable, // A destination address is required. .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. .WSAEHOSTUNREACH => return error.NetworkUnreachable, @@ -6133,13 +6133,13 @@ pub fn sendmsg( .INTR => continue, .INVAL => unreachable, // Invalid argument passed. .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .PIPE => return error.BrokenPipe, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, @@ -6178,7 +6178,7 @@ pub const SendToError = SendMsgError || error{ /// Otherwise, the address of the target is given by `dest_addr` with `addrlen` specifying its size. /// /// If the message is too long to pass atomically through the underlying protocol, -/// `SendError.MessageTooBig` is returned, and the message is not transmitted. +/// `SendError.MessageOversize` is returned, and the message is not transmitted. /// /// There is no indication of failure to deliver. /// @@ -6201,10 +6201,10 @@ pub fn sendto( .WSAEACCES => return error.AccessDenied, .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageTooBig, + .WSAEMSGSIZE => return error.MessageOversize, .WSAENOBUFS => return error.SystemResources, .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyNotSupported, + .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, .WSAEDESTADDRREQ => unreachable, // A destination address is required. .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. .WSAEHOSTUNREACH => return error.NetworkUnreachable, @@ -6238,13 +6238,13 @@ pub fn sendto( .INTR => continue, .INVAL => return error.UnreachableAddress, .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .PIPE => return error.BrokenPipe, - .AFNOSUPPORT => return error.AddressFamilyNotSupported, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, @@ -6284,7 +6284,7 @@ pub fn send( flags: u32, ) SendError!usize { return sendto(sockfd, buf, flags, null, 0) catch |err| switch (err) { - error.AddressFamilyNotSupported => unreachable, + error.AddressFamilyUnsupported => unreachable, error.SymLinkLoop => unreachable, error.NameTooLong => unreachable, error.FileNotFound => unreachable, @@ -6471,7 +6471,7 @@ pub const RecvFromError = error{ SocketNotBound, /// The UDP message was too big for the buffer and part of it has been discarded - MessageTooBig, + MessageOversize, /// The network subsystem has failed. NetworkDown, @@ -6504,7 +6504,7 @@ pub fn recvfrom( .WSANOTINITIALISED => unreachable, .WSAECONNRESET => return error.ConnectionResetByPeer, .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageTooBig, + .WSAEMSGSIZE => return error.MessageOversize, .WSAENETDOWN => return error.NetworkDown, .WSAENOTCONN => return error.SocketUnconnected, .WSAEWOULDBLOCK => return error.WouldBlock, @@ -6575,7 +6575,7 @@ pub fn recvmsg( .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. - .MSGSIZE => return error.MessageTooBig, + .MSGSIZE => return error.MessageOversize, .PIPE => return error.BrokenPipe, .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. .CONNRESET => return error.ConnectionResetByPeer, From 18ec9685fbdac7e2e49cb1d0979d4aeea3775bc8 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 20:04:13 -0700 Subject: [PATCH 120/244] std.Io.Threaded: add support for getaddrinfo this API sucks but it's the best we've got on some operating systems --- lib/std/Io/Threaded.zig | 56 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 6ceebc79e1..50cc10a765 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2792,7 +2792,61 @@ fn netLookupFallible( if (builtin.link_libc) { // This operating system lacks a way to resolve asynchronously. We are // stuck with getaddrinfo. - @compileError("TODO"); + var name_buffer: [HostName.max_len + 1]u8 = undefined; + @memcpy(name_buffer[0..host_name.bytes.len], host_name.bytes); + name_buffer[host_name.bytes.len] = 0; + const name_c = name_buffer[0..host_name.bytes.len :0]; + + var port_buffer: [8]u8 = undefined; + const port_c = std.fmt.bufPrintZ(&port_buffer, "{d}", .{options.port}) catch unreachable; + + const hints: posix.addrinfo = .{ + .flags = .{ .NUMERICSERV = true }, + .family = posix.AF.UNSPEC, + .socktype = posix.SOCK.STREAM, + .protocol = posix.IPPROTO.TCP, + .canonname = null, + .addr = null, + .addrlen = 0, + .next = null, + }; + var res: ?*posix.addrinfo = null; + while (true) { + try t.checkCancel(); + switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) { + @as(posix.system.EAI, @enumFromInt(0)) => {}, + .ADDRFAMILY => return error.AddressFamilyUnsupported, + .AGAIN => return error.NameServerFailure, + .FAIL => return error.NameServerFailure, + .FAMILY => return error.AddressFamilyUnsupported, + .MEMORY => return error.SystemResources, + .NODATA => return error.UnknownHostName, + .NONAME => return error.UnknownHostName, + .SYSTEM => switch (posix.errno(-1)) { + .INTR => continue, + else => |e| return posix.unexpectedErrno(e), + }, + else => return error.Unexpected, + } + } + defer if (res) |some| posix.system.freeaddrinfo(some); + + var it = res; + var canon_name: ?[]const u8 = null; + while (it) |info| : (it = info.next) { + const addr = info.addr orelse continue; + try resolved.putOne(addressFromPosix(addr)); + + if (info.canonname) |n| { + if (canon_name == null) { + canon_name = n; + } + } + } + if (canon_name) |n| { + try resolved.putOne(.{ .canonical_name = copyCanon(options.canonical_name_buffer, n) }); + } + return; } return error.OptionUnsupported; From bf841bb4ae6d6c7cf9494ccc45282b704b40e7f6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 20:16:19 -0700 Subject: [PATCH 121/244] std.Io.Threaded: implement futexes on darwin --- lib/std/Io/Threaded.zig | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 50cc10a765..0796b141ff 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -3448,6 +3448,16 @@ fn copyCanon(canonical_name_buffer: *[HostName.max_len]u8, name: []const u8) Hos return .{ .bytes = dest }; } +/// Darwin XNU 7195.50.7.100.1 introduced __ulock_wait2 and migrated code paths (notably pthread_cond_t) towards it: +/// https://github.com/apple/darwin-xnu/commit/d4061fb0260b3ed486147341b72468f836ed6c8f#diff-08f993cc40af475663274687b7c326cc6c3031e0db3ac8de7b24624610616be6 +/// +/// This XNU version appears to correspond to 11.0.1: +/// https://kernelshaman.blogspot.com/2021/01/building-xnu-for-macos-big-sur-1101.html +/// +/// ulock_wait() uses 32-bit micro-second timeouts where 0 = INFINITE or no-timeout +/// ulock_wait2() uses 64-bit nano-second timeouts (with the same convention) +const darwin_supports_ulock_wait2 = builtin.os.version_range.semver.min.major >= 11; + fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Cancelable!void { @branchHint(.cold); @@ -3467,6 +3477,33 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca return; } + if (native_os.isDarwin()) { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + }; + try t.checkCancel(); + const status = if (darwin_supports_ulock_wait2) + c.__ulock_wait2(flags, ptr, expect, 0, 0) + else + c.__ulock_wait(flags, ptr, expect, 0); + + if (status >= 0) return; + + if (builtin.mode == .Debug) switch (@as(c.E, @enumFromInt(-status))) { + // Wait was interrupted by the OS or other spurious signalling. + .INTR => {}, + // Address of the futex was paged out. This is unlikely, but possible in theory, and + // pthread/libdispatch on darwin bother to handle it. In this case we'll return + // without waiting, but the caller should retry anyway. + .FAULT => {}, + .TIMEDOUT => unreachable, + else => unreachable, + }; + return; + } + @compileError("TODO"); } @@ -3488,6 +3525,32 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi return; } + if (native_os.isDarwin()) { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + }; + const status = if (darwin_supports_ulock_wait2) + c.__ulock_wait2(flags, ptr, expect, 0, 0) + else + c.__ulock_wait(flags, ptr, expect, 0); + + if (status >= 0) return; + + if (builtin.mode == .Debug) switch (@as(c.E, @enumFromInt(-status))) { + // Wait was interrupted by the OS or other spurious signalling. + .INTR => {}, + // Address of the futex was paged out. This is unlikely, but possible in theory, and + // pthread/libdispatch on darwin bother to handle it. In this case we'll return + // without waiting, but the caller should retry anyway. + .FAULT => {}, + .TIMEDOUT => unreachable, + else => unreachable, + }; + return; + } + @compileError("TODO"); } @@ -3532,6 +3595,27 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { return; } + if (native_os.isDarwin()) { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + .WAKE_ALL = max_waiters > 1, + }; + const is_debug = builtin.mode == .Debug; + while (true) { + const status = c.__ulock_wake(flags, ptr, 0); + if (status >= 0) return; + switch (@as(c.E, @enumFromInt(-status))) { + .INTR => continue, // spurious wake() + .FAULT => assert(!is_debug), // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t + .NOENT => return, // nothing was woken up + .ALREADY => assert(!is_debug), // only for UL.Op.WAKE_THREAD + else => assert(!is_debug), + } + } + } + @compileError("TODO"); } From 90fdd21df6663d0ad99379cd056c8940e3a3e267 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 15 Oct 2025 21:08:42 -0700 Subject: [PATCH 122/244] std: move DNS record enum to a better namespace --- lib/std/Io/Threaded.zig | 18 +++++++++--------- lib/std/Io/net/HostName.zig | 11 +++++++++-- lib/std/os/linux.zig | 6 ------ lib/std/posix.zig | 1 - src/main.zig | 1 + 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 0796b141ff..745dfee2ea 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -3130,9 +3130,9 @@ fn lookupDns( options: HostName.LookupOptions, ) HostName.LookupError!void { const t_io = t.io(); - const family_records: [2]struct { af: IpAddress.Family, rr: u8 } = .{ - .{ .af = .ip6, .rr = std.posix.RR.A }, - .{ .af = .ip4, .rr = std.posix.RR.AAAA }, + const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{ + .{ .af = .ip6, .rr = .A }, + .{ .af = .ip4, .rr = .AAAA }, }; var query_buffers: [2][280]u8 = undefined; var answer_buffer: [2 * 512]u8 = undefined; @@ -3280,7 +3280,7 @@ fn lookupDns( // Here we could potentially add diagnostics to the results queue. continue; }) |record| switch (record.rr) { - std.posix.RR.A => { + .A => { const data = record.packet[record.data_off..][0..record.data_len]; if (data.len != 4) return error.InvalidDnsARecord; try resolved.putOne(t_io, .{ .address = .{ .ip4 = .{ @@ -3289,7 +3289,7 @@ fn lookupDns( } } }); addresses_len += 1; }, - std.posix.RR.AAAA => { + .AAAA => { const data = record.packet[record.data_off..][0..record.data_len]; if (data.len != 16) return error.InvalidDnsAAAARecord; try resolved.putOne(t_io, .{ .address = .{ .ip6 = .{ @@ -3298,11 +3298,11 @@ fn lookupDns( } } }); addresses_len += 1; }, - std.posix.RR.CNAME => { + .CNAME => { _, canonical_name = HostName.expand(record.packet, record.data_off, options.canonical_name_buffer) catch return error.InvalidDnsCnameRecord; }, - else => continue, + _ => continue, }; } @@ -3413,7 +3413,7 @@ fn lookupHostsReader( } /// Writes DNS resolution query packet data to `w`; at most 280 bytes. -fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u8, entropy: [2]u8) usize { +fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: HostName.DnsRecord, entropy: [2]u8) usize { // This implementation is ported from musl libc. // A more idiomatic "ziggy" implementation would be welcome. var name = dname; @@ -3437,7 +3437,7 @@ fn writeResolutionQuery(q: *[280]u8, op: u4, dname: []const u8, class: u8, ty: u if (j - i - 1 > 62) unreachable; q[i - 1] = @intCast(j - i); } - q[i + 1] = ty; + q[i + 1] = @intFromEnum(ty); q[i + 3] = class; return n; } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 4c64b4dec0..5ae170fc73 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -147,13 +147,20 @@ pub fn expand(noalias packet: []const u8, start_i: usize, noalias dest_buffer: [ return error.InvalidDnsPacket; } +pub const DnsRecord = enum(u8) { + A = 1, + CNAME = 5, + AAAA = 28, + _, +}; + pub const DnsResponse = struct { bytes: []const u8, bytes_index: u32, answers_remaining: u16, pub const Answer = struct { - rr: u8, + rr: DnsRecord, packet: []const u8, data_off: u32, data_len: u16, @@ -190,7 +197,7 @@ pub const DnsResponse = struct { if (i + 10 + len > r.len) return error.InvalidDnsPacket; defer dr.bytes_index = i + 10 + len; return .{ - .rr = r[i + 1], + .rr = @enumFromInt(r[i + 1]), .packet = r, .data_off = i + 10, .data_len = len, diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 5ae50e0270..96010fb203 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -7096,12 +7096,6 @@ pub const IPPROTO = struct { pub const MAX = 256; }; -pub const RR = struct { - pub const A = 1; - pub const CNAME = 5; - pub const AAAA = 28; -}; - pub const tcp_repair_opt = extern struct { opt_code: u32, opt_val: u32, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index f62c8c9edd..f7b9a449ae 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -98,7 +98,6 @@ pub const POSIX_FADV = system.POSIX_FADV; pub const PR = system.PR; pub const PROT = system.PROT; pub const RLIM = system.RLIM; -pub const RR = system.RR; pub const S = system.S; pub const SA = system.SA; pub const SC = system.SC; diff --git a/src/main.zig b/src/main.zig index bd086c67f4..03d42ba898 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5062,6 +5062,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) // Prevents bootstrap from depending on a bunch of unnecessary stuff. var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct { allocator: Allocator, + io: Io, fn deinit(_: @This()) void {} } = .{ .allocator = gpa, .io = io }; defer http_client.deinit(); From 22334f573074bec8112bf686014381e3d8c4cc08 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 11:52:52 -0700 Subject: [PATCH 123/244] std: make IPv6 address parsing system-independent before, the max length of the host name depended on the target. --- lib/std/Io/net.zig | 19 +++++++++++++------ lib/std/c.zig | 2 +- lib/std/os.zig | 11 ++++++++++- lib/std/posix.zig | 1 + 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 81ad077667..046aec3054 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -447,7 +447,9 @@ pub const Ip6Address = struct { pub const Unresolved = struct { /// Big endian bytes: [16]u8, - interface_name: ?Interface.Name, + /// Has not been checked to be a valid native interface name. + /// Externally managed memory. + interface_name: ?[]const u8, pub const Parsed = union(enum) { success: Unresolved, @@ -536,7 +538,6 @@ pub const Ip6Address = struct { parts_i += 1; text_i += 1; const name = text[text_i..]; - if (name.len > Interface.Name.max_len) return .{ .interface_name_oversized = text_i }; if (name.len == 0) return .incomplete; interface_name_text = name; text_i = @intCast(text.len); @@ -563,7 +564,7 @@ pub const Ip6Address = struct { return .{ .success = .{ .bytes = @bitCast(parts), - .interface_name = if (interface_name_text) |t| .fromSliceUnchecked(t) else null, + .interface_name = interface_name_text, } }; }, } @@ -646,7 +647,7 @@ pub const Ip6Address = struct { } } } - if (u.interface_name) |n| try w.print("%{s}", .{n.toSlice()}); + if (u.interface_name) |n| try w.print("%{s}", .{n}); } }; @@ -678,6 +679,8 @@ pub const Ip6Address = struct { /// If this is returned, more detailed diagnostics can be obtained by /// calling the `Parsed.init` function. ParseFailed, + /// The interface name is longer than the host operating system supports. + NameTooLong, } || Interface.Name.ResolveError; /// This function requires an `Io` parameter because it must query the operating @@ -689,7 +692,11 @@ pub const Ip6Address = struct { .success => |p| return .{ .bytes = p.bytes, .port = port, - .interface = if (p.interface_name) |n| try n.resolve(io) else .none, + .interface = i: { + const text = p.interface_name orelse break :i .none; + const name: Interface.Name = try .fromSlice(text); + break :i try name.resolve(io); + }, }, else => return error.ParseFailed, }; @@ -946,7 +953,7 @@ pub const Interface = struct { pub const Name = struct { bytes: [max_len:0]u8, - pub const max_len = std.posix.IFNAMESIZE - 1; + pub const max_len = if (@TypeOf(std.posix.IFNAMESIZE) == void) 0 else std.posix.IFNAMESIZE - 1; pub fn toSlice(n: *const Name) []const u8 { return std.mem.sliceTo(&n.bytes, 0); diff --git a/lib/std/c.zig b/lib/std/c.zig index b96ca2e458..877b9ae4e3 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -6855,7 +6855,7 @@ pub const IFNAMESIZE = switch (native_os) { // https://github.com/SerenityOS/serenity/blob/9882848e0bf783dfc8e8a6d887a848d70d9c58f4/Kernel/API/POSIX/net/if.h#L50 .openbsd, .dragonfly, .netbsd, .freebsd, .macos, .ios, .tvos, .watchos, .visionos, .serenity => 16, .illumos => 32, - else => void, + else => {}, }; pub const stack_t = switch (native_os) { diff --git a/lib/std/os.zig b/lib/std/os.zig index a3d659d4ca..b7122ca03b 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -201,10 +201,19 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. } } +pub const FstatatError = error{ + SystemResources, + AccessDenied, + NameTooLong, + FileNotFound, + InvalidUtf8, + Unexpected, +}; + /// WASI-only. Same as `fstatat` but targeting WASI. /// `pathname` should be encoded as valid UTF-8. /// See also `fstatat`. -pub fn fstatat_wasi(dirfd: posix.fd_t, pathname: []const u8, flags: wasi.lookupflags_t) posix.FStatAtError!wasi.filestat_t { +pub fn fstatat_wasi(dirfd: posix.fd_t, pathname: []const u8, flags: wasi.lookupflags_t) FstatatError!wasi.filestat_t { var stat: wasi.filestat_t = undefined; switch (wasi.path_filestat_get(dirfd, flags, pathname.ptr, pathname.len, &stat)) { .SUCCESS => return stat, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index f7b9a449ae..b27468617c 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -4945,6 +4945,7 @@ pub const AccessError = error{ /// Windows-only; file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ InvalidWtf8, + Canceled, } || UnexpectedError; /// check user's permissions for a file From f7bbcb4a4b56925110370d2c28a3eaba3854f88c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 12:00:30 -0700 Subject: [PATCH 124/244] fix miscellaneous compilation failures --- lib/std/Io.zig | 6 +++--- lib/std/Io/Threaded.zig | 2 +- lib/std/Io/net.zig | 2 +- lib/std/Io/net/test.zig | 1 - lib/std/Thread/Futex.zig | 2 +- lib/std/fs/Dir.zig | 5 +++++ lib/std/zig/system.zig | 8 ++++---- lib/std/zig/system/linux.zig | 12 +++++++----- 8 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index d1fa7afc68..6ae4c3346c 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -1282,7 +1282,7 @@ pub const TypeErasedQueue = struct { var remaining = elements; while (true) { - const getter: *Get = @fieldParentPtr("node", q.getters.popFirst() orelse break); + const getter: *Get = @alignCast(@fieldParentPtr("node", q.getters.popFirst() orelse break)); const copy_len = @min(getter.remaining.len, remaining.len); @memcpy(getter.remaining[0..copy_len], remaining[0..copy_len]); remaining = remaining[copy_len..]; @@ -1379,7 +1379,7 @@ pub const TypeErasedQueue = struct { } // Copy directly from putters into buffer. while (remaining.len > 0) { - const putter: *Put = @fieldParentPtr("node", q.putters.popFirst() orelse break); + const putter: *Put = @alignCast(@fieldParentPtr("node", q.putters.popFirst() orelse break)); const copy_len = @min(putter.remaining.len, remaining.len); @memcpy(remaining[0..copy_len], putter.remaining[0..copy_len]); putter.remaining = putter.remaining[copy_len..]; @@ -1412,7 +1412,7 @@ pub const TypeErasedQueue = struct { /// buffers been fully copied. fn fillRingBufferFromPutters(q: *TypeErasedQueue, io: Io, len: usize) usize { while (true) { - const putter: *Put = @fieldParentPtr("node", q.putters.popFirst() orelse return len); + const putter: *Put = @alignCast(@fieldParentPtr("node", q.putters.popFirst() orelse return len)); const available = q.buffer[q.put_index..]; const copy_len = @min(available.len, putter.remaining.len); @memcpy(available[0..copy_len], putter.remaining[0..copy_len]); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 745dfee2ea..86a804b516 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -541,7 +541,7 @@ fn groupAsync( context_alignment: std.mem.Alignment, start: *const fn (*Io.Group, context: *const anyopaque) void, ) void { - if (builtin.single_threaded) return start(context.ptr); + if (builtin.single_threaded) return start(group, context.ptr); const t: *Threaded = @ptrCast(@alignCast(userdata)); const cpu_count = t.cpu_count catch 1; const gpa = t.allocator; diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 046aec3054..1f47d7e1f5 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1027,7 +1027,7 @@ pub const Socket = struct { /// Underlying platform-defined type which may or may not be /// interchangeable with a file system file descriptor. pub const Handle = switch (native_os) { - .windows => std.windows.ws2_32.SOCKET, + .windows => std.os.windows.ws2_32.SOCKET, else => std.posix.fd_t, }; diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 1c8f8bd8c7..feef19e0e4 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -81,7 +81,6 @@ test "IPv6 address parse failures" { try testing.expectEqual(Unresolved.Parsed{ .invalid_byte = 5 }, Unresolved.parse("::123.123.123.123")); try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("1")); try testing.expectEqual(Unresolved.Parsed.incomplete, Unresolved.parse("ff01::fb%")); - try testing.expectEqual(Unresolved.Parsed{ .interface_name_oversized = 9 }, Unresolved.parse("ff01::fb%wlp3" ++ "s0" ** @divExact(std.posix.IFNAMESIZE - 4, 2))); } test "invalid but parseable IPv6 scope ids" { diff --git a/lib/std/Thread/Futex.zig b/lib/std/Thread/Futex.zig index bad34996f5..0047f5400e 100644 --- a/lib/std/Thread/Futex.zig +++ b/lib/std/Thread/Futex.zig @@ -116,7 +116,7 @@ const SingleThreadedImpl = struct { unreachable; // deadlock detected }; - std.Thread.sleep(delay); + _ = delay; return error.Timeout; } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 1b5f3808ff..c8236b9f7a 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1356,6 +1356,11 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir error.FileLocksNotSupported => unreachable, // locking folders is not supported error.WouldBlock => unreachable, // can't happen for directories error.FileBusy => unreachable, // can't happen for directories + error.SharingViolation => unreachable, + error.PipeBusy => unreachable, + error.ProcessNotFound => unreachable, + error.AntivirusInterference => unreachable, + else => |e| return e, }; return .{ .fd = fd }; diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 2b11e971ca..6313bff374 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -367,10 +367,10 @@ pub fn resolveTargetQuery(io: Io, query: Target.Query) DetectError!Target { } var cpu = switch (query.cpu_model) { - .native => detectNativeCpuAndFeatures(query_cpu_arch, os, query), + .native => detectNativeCpuAndFeatures(io, query_cpu_arch, os, query), .baseline => Target.Cpu.baseline(query_cpu_arch, os), .determined_by_arch_os => if (query.cpu_arch == null) - detectNativeCpuAndFeatures(query_cpu_arch, os, query) + detectNativeCpuAndFeatures(io, query_cpu_arch, os, query) else Target.Cpu.baseline(query_cpu_arch, os), .explicit => |model| model.toCpu(query_cpu_arch), @@ -521,7 +521,7 @@ fn updateCpuFeatures( set.removeFeatureSet(sub_set); } -fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: Target.Query) ?Target.Cpu { +fn detectNativeCpuAndFeatures(io: Io, cpu_arch: Target.Cpu.Arch, os: Target.Os, query: Target.Query) ?Target.Cpu { // Here we switch on a comptime value rather than `cpu_arch`. This is valid because `cpu_arch`, // although it is a runtime value, is guaranteed to be one of the architectures in the set // of the respective switch prong. @@ -532,7 +532,7 @@ fn detectNativeCpuAndFeatures(cpu_arch: Target.Cpu.Arch, os: Target.Os, query: T } switch (builtin.os.tag) { - .linux => return linux.detectNativeCpuAndFeatures(), + .linux => return linux.detectNativeCpuAndFeatures(io), .macos => return darwin.macos.detectNativeCpuAndFeatures(), .windows => return windows.detectNativeCpuAndFeatures(), else => {}, diff --git a/lib/std/zig/system/linux.zig b/lib/std/zig/system/linux.zig index bbe7f8eaf9..8bdfae1de4 100644 --- a/lib/std/zig/system/linux.zig +++ b/lib/std/zig/system/linux.zig @@ -1,5 +1,7 @@ -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; const mem = std.mem; const fs = std.fs; const fmt = std.fmt; @@ -344,7 +346,7 @@ fn testParser( expected_model: *const Target.Cpu.Model, input: []const u8, ) !void { - var r: std.Io.Reader = .fixed(input); + var r: Io.Reader = .fixed(input); const result = try parser.parse(arch, &r); try testing.expectEqual(expected_model, result.?.model); try testing.expect(expected_model.features.eql(result.?.features)); @@ -357,7 +359,7 @@ fn testParser( // When all the lines have been analyzed the finalize method is called. fn CpuinfoParser(comptime impl: anytype) type { return struct { - fn parse(arch: Target.Cpu.Arch, reader: *std.Io.Reader) !?Target.Cpu { + fn parse(arch: Target.Cpu.Arch, reader: *Io.Reader) !?Target.Cpu { var obj: impl = .{}; while (try reader.takeDelimiter('\n')) |line| { const colon_pos = mem.indexOfScalar(u8, line, ':') orelse continue; @@ -376,14 +378,14 @@ inline fn getAArch64CpuFeature(comptime feat_reg: []const u8) u64 { ); } -pub fn detectNativeCpuAndFeatures() ?Target.Cpu { +pub fn detectNativeCpuAndFeatures(io: Io) ?Target.Cpu { var file = fs.openFileAbsolute("/proc/cpuinfo", .{}) catch |err| switch (err) { else => return null, }; defer file.close(); var buffer: [4096]u8 = undefined; // "flags" lines can get pretty long. - var file_reader = file.reader(&buffer); + var file_reader = file.reader(io, &buffer); const current_arch = builtin.cpu.arch; switch (current_arch) { From 6336d586614a547a550d6d7055b364ca53775a60 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 15:59:20 -0700 Subject: [PATCH 125/244] std.Io.Threaded: fix getaddrinfo usage --- lib/std/Io/Threaded.zig | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 86a804b516..d88a8465d7 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2814,7 +2814,7 @@ fn netLookupFallible( while (true) { try t.checkCancel(); switch (posix.system.getaddrinfo(name_c.ptr, port_c.ptr, &hints, &res)) { - @as(posix.system.EAI, @enumFromInt(0)) => {}, + @as(posix.system.EAI, @enumFromInt(0)) => break, .ADDRFAMILY => return error.AddressFamilyUnsupported, .AGAIN => return error.NameServerFailure, .FAIL => return error.NameServerFailure, @@ -2832,10 +2832,11 @@ fn netLookupFallible( defer if (res) |some| posix.system.freeaddrinfo(some); var it = res; - var canon_name: ?[]const u8 = null; + var canon_name: ?[*:0]const u8 = null; while (it) |info| : (it = info.next) { const addr = info.addr orelse continue; - try resolved.putOne(addressFromPosix(addr)); + const storage: PosixAddress = .{ .any = addr.* }; + try resolved.putOne(t_io, .{ .address = addressFromPosix(&storage) }); if (info.canonname) |n| { if (canon_name == null) { @@ -2844,7 +2845,9 @@ fn netLookupFallible( } } if (canon_name) |n| { - try resolved.putOne(.{ .canonical_name = copyCanon(options.canonical_name_buffer, n) }); + try resolved.putOne(t_io, .{ + .canonical_name = copyCanon(options.canonical_name_buffer, std.mem.sliceTo(n, 0)), + }); } return; } @@ -2870,7 +2873,7 @@ fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t { }; } -fn addressFromPosix(posix_address: *PosixAddress) IpAddress { +fn addressFromPosix(posix_address: *const PosixAddress) IpAddress { return switch (posix_address.any.family) { posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, @@ -2898,14 +2901,14 @@ fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.so return @sizeOf(posix.sockaddr.un); } -fn address4FromPosix(in: *posix.sockaddr.in) net.Ip4Address { +fn address4FromPosix(in: *const posix.sockaddr.in) net.Ip4Address { return .{ .port = std.mem.bigToNative(u16, in.port), .bytes = @bitCast(in.addr), }; } -fn address6FromPosix(in6: *posix.sockaddr.in6) net.Ip6Address { +fn address6FromPosix(in6: *const posix.sockaddr.in6) net.Ip6Address { return .{ .port = std.mem.bigToNative(u16, in6.port), .bytes = in6.addr, From f14c4c3db88b6194205369312d0e616c793c5e47 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 16:02:35 -0700 Subject: [PATCH 126/244] std.Io.net.HostName: fix connectMany not running DNS async --- lib/std/Io/net/HostName.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 5ae170fc73..4cd0621b97 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -268,20 +268,20 @@ pub fn connectMany( var canonical_name_buffer: [max_len]u8 = undefined; var lookup_buffer: [32]HostName.LookupResult = undefined; var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer); + var group: Io.Group = .init; - host_name.lookup(io, &lookup_queue, .{ + group.async(io, lookup, .{ host_name, io, &lookup_queue, .{ .port = port, .canonical_name_buffer = &canonical_name_buffer, - }); - - var group: Io.Group = .init; + } }); while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) { .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), .canonical_name => continue, .end => |lookup_result| { - group.waitUncancelable(io); - results.putOneUncancelable(io, .{ .end = lookup_result }); + results.putOneUncancelable(io, .{ + .end = if (group.wait(io)) lookup_result else |err| err, + }); return; }, } else |err| switch (err) { @@ -409,7 +409,7 @@ test ResolvConf { \\# Generated by resolvconf \\nameserver 1.0.0.1 \\nameserver 1.1.1.1 - \\nameserver fe80::e0e:76ff:fed4:cf22%eno1 + \\nameserver fe80::e0e:76ff:fed4:cf22 \\options edns0 \\ ; From 7b1502f327f580254f5699754ec21521eac10cc7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 16:03:10 -0700 Subject: [PATCH 127/244] std: fix compilation errors on macos --- lib/compiler/libc.zig | 6 +++++- lib/std/fs/test.zig | 8 -------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/compiler/libc.zig b/lib/compiler/libc.zig index ef3aabb6dc..a18a7a0e06 100644 --- a/lib/compiler/libc.zig +++ b/lib/compiler/libc.zig @@ -29,6 +29,10 @@ pub fn main() !void { const arena = arena_instance.allocator(); const gpa = arena; + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(arena); const zig_lib_directory = args[1]; @@ -66,7 +70,7 @@ pub fn main() !void { const target_query = std.zig.parseTargetQueryOrReportFatalError(gpa, .{ .arch_os_abi = target_arch_os_abi, }); - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); if (print_includes) { const libc_installation: ?*LibCInstallation = libc: { diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 22451acb66..6bb0d871b6 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1433,14 +1433,6 @@ test "setEndPos" { try testing.expectEqual(0, try f.getEndPos()); try reader.seekTo(0); try testing.expectEqual(0, try reader.interface.readSliceShort(&buffer)); - - // Invalid file length should error gracefully. Actual limit is host - // and file-system dependent, but 1PB should fail on filesystems like - // EXT4 and NTFS. But XFS or Btrfs support up to 8EiB files. - try testing.expectError(error.FileTooBig, f.setEndPos(0x4_0000_0000_0000)); - try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u63))); - try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u63) + 1)); - try testing.expectError(error.FileTooBig, f.setEndPos(std.math.maxInt(u64))); } test "access file" { From 3bf0ce65a514e6f86241364c4a11089e32b3ba57 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 17:05:35 -0700 Subject: [PATCH 128/244] fix miscellaneous compilation errors - ILSEQ -> error.BadPathName - implement dirStatPath for WASI --- lib/compiler/aro/aro/Compilation.zig | 3 +- lib/std/Io/Threaded.zig | 38 +++++++- lib/std/fs/Dir.zig | 4 - lib/std/fs/test.zig | 22 ----- lib/std/os.zig | 30 ------- lib/std/os/windows.zig | 8 +- lib/std/posix.zig | 130 +++++++-------------------- lib/std/posix/test.zig | 43 --------- test/src/convert-stack-trace.zig | 8 +- tools/docgen.zig | 8 +- tools/doctest.zig | 44 +++++---- 11 files changed, 114 insertions(+), 224 deletions(-) diff --git a/lib/compiler/aro/aro/Compilation.zig b/lib/compiler/aro/aro/Compilation.zig index 46f783097b..e0b9a508cf 100644 --- a/lib/compiler/aro/aro/Compilation.zig +++ b/lib/compiler/aro/aro/Compilation.zig @@ -171,10 +171,11 @@ pub fn init(gpa: Allocator, arena: Allocator, io: Io, diagnostics: *Diagnostics, /// Initialize Compilation with default environment, /// pragma handlers and emulation mode set to target. -pub fn initDefault(gpa: Allocator, arena: Allocator, diagnostics: *Diagnostics, cwd: std.fs.Dir) !Compilation { +pub fn initDefault(gpa: Allocator, arena: Allocator, io: Io, diagnostics: *Diagnostics, cwd: std.fs.Dir) !Compilation { var comp: Compilation = .{ .gpa = gpa, .arena = arena, + .io = io, .diagnostics = diagnostics, .environment = try Environment.loadAll(gpa), .cwd = cwd, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index d88a8465d7..57578628fc 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -174,7 +174,7 @@ pub fn io(t: *Threaded) Io { .dirStatPath = switch (builtin.os.tag) { .linux => dirStatPathLinux, .windows => @panic("TODO"), - .wasi => @panic("TODO"), + .wasi => dirStatPathWasi, else => dirStatPathPosix, }, .fileStat = switch (builtin.os.tag) { @@ -984,6 +984,42 @@ fn dirStatPathPosix( } } +fn dirStatPathWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + if (builtin.link_libc) return dirStatPathPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const dir_fd = dir.handle; + const wasi = std.os.wasi; + const flags: wasi.lookupflags_t = .{ + .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), + }; + var stat: wasi.filestat_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_filestat_get(dir_fd, flags, sub_path.ptr, sub_path.len, &stat)) { + .SUCCESS => return statFromWasi(stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| errnoBug(err), + .BADF => |err| errnoBug(err), // Always a race condition. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .FAULT => |err| errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index c8236b9f7a..3565d2fc2c 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2500,10 +2500,6 @@ pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { defer file.close(); return file.stat(); } - if (native_os == .wasi and !builtin.link_libc) { - const st = try std.os.fstatat_wasi(self.fd, sub_path, .{ .SYMLINK_FOLLOW = true }); - return Stat.fromWasi(st); - } var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{}); diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 6bb0d871b6..052afc3a02 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -2031,37 +2031,28 @@ test "invalid UTF-8/WTF-8 paths" { const invalid_path = try ctx.transformPath("\xFF"); try testing.expectError(expected_err, ctx.dir.openFile(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.openFileZ(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.createFile(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.createFileZ(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.makeDir(invalid_path)); - try testing.expectError(expected_err, ctx.dir.makeDirZ(invalid_path)); try testing.expectError(expected_err, ctx.dir.makePath(invalid_path)); try testing.expectError(expected_err, ctx.dir.makeOpenPath(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.openDir(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.openDirZ(invalid_path, .{})); try testing.expectError(expected_err, ctx.dir.deleteFile(invalid_path)); - try testing.expectError(expected_err, ctx.dir.deleteFileZ(invalid_path)); try testing.expectError(expected_err, ctx.dir.deleteDir(invalid_path)); - try testing.expectError(expected_err, ctx.dir.deleteDirZ(invalid_path)); try testing.expectError(expected_err, ctx.dir.rename(invalid_path, invalid_path)); - try testing.expectError(expected_err, ctx.dir.renameZ(invalid_path, invalid_path)); try testing.expectError(expected_err, ctx.dir.symLink(invalid_path, invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.symLinkZ(invalid_path, invalid_path, .{})); if (native_os == .wasi) { try testing.expectError(expected_err, ctx.dir.symLinkWasi(invalid_path, invalid_path, .{})); } try testing.expectError(expected_err, ctx.dir.readLink(invalid_path, &[_]u8{})); - try testing.expectError(expected_err, ctx.dir.readLinkZ(invalid_path, &[_]u8{})); if (native_os == .wasi) { try testing.expectError(expected_err, ctx.dir.readLinkWasi(invalid_path, &[_]u8{})); } @@ -2075,7 +2066,6 @@ test "invalid UTF-8/WTF-8 paths" { try testing.expectError(expected_err, ctx.dir.writeFile(.{ .sub_path = invalid_path, .data = "" })); try testing.expectError(expected_err, ctx.dir.access(invalid_path, .{})); - try testing.expectError(expected_err, ctx.dir.accessZ(invalid_path, .{})); var dir = ctx.dir.adaptToNewApi(); try testing.expectError(expected_err, dir.updateFile(io, invalid_path, dir, invalid_path, .{})); @@ -2085,37 +2075,25 @@ test "invalid UTF-8/WTF-8 paths" { if (native_os != .wasi) { try testing.expectError(expected_err, ctx.dir.realpath(invalid_path, &[_]u8{})); - try testing.expectError(expected_err, ctx.dir.realpathZ(invalid_path, &[_]u8{})); try testing.expectError(expected_err, ctx.dir.realpathAlloc(testing.allocator, invalid_path)); } try testing.expectError(expected_err, fs.rename(ctx.dir, invalid_path, ctx.dir, invalid_path)); - try testing.expectError(expected_err, fs.renameZ(ctx.dir, invalid_path, ctx.dir, invalid_path)); if (native_os != .wasi and ctx.path_type != .relative) { try testing.expectError(expected_err, fs.copyFileAbsolute(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.makeDirAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.makeDirAbsoluteZ(invalid_path)); try testing.expectError(expected_err, fs.deleteDirAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.deleteDirAbsoluteZ(invalid_path)); try testing.expectError(expected_err, fs.renameAbsolute(invalid_path, invalid_path)); - try testing.expectError(expected_err, fs.renameAbsoluteZ(invalid_path, invalid_path)); try testing.expectError(expected_err, fs.openDirAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.openDirAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.openFileAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.openFileAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.accessAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.accessAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.createFileAbsolute(invalid_path, .{})); - try testing.expectError(expected_err, fs.createFileAbsoluteZ(invalid_path, .{})); try testing.expectError(expected_err, fs.deleteFileAbsolute(invalid_path)); - try testing.expectError(expected_err, fs.deleteFileAbsoluteZ(invalid_path)); try testing.expectError(expected_err, fs.deleteTreeAbsolute(invalid_path)); var readlink_buf: [fs.max_path_bytes]u8 = undefined; try testing.expectError(expected_err, fs.readLinkAbsolute(invalid_path, &readlink_buf)); - try testing.expectError(expected_err, fs.readLinkAbsoluteZ(invalid_path, &readlink_buf)); try testing.expectError(expected_err, fs.symLinkAbsolute(invalid_path, invalid_path, .{})); - try testing.expectError(expected_err, fs.symLinkAbsoluteZ(invalid_path, invalid_path, .{})); try testing.expectError(expected_err, fs.realpathAlloc(testing.allocator, invalid_path)); } } diff --git a/lib/std/os.zig b/lib/std/os.zig index b7122ca03b..7fe64290b1 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -201,36 +201,6 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. } } -pub const FstatatError = error{ - SystemResources, - AccessDenied, - NameTooLong, - FileNotFound, - InvalidUtf8, - Unexpected, -}; - -/// WASI-only. Same as `fstatat` but targeting WASI. -/// `pathname` should be encoded as valid UTF-8. -/// See also `fstatat`. -pub fn fstatat_wasi(dirfd: posix.fd_t, pathname: []const u8, flags: wasi.lookupflags_t) FstatatError!wasi.filestat_t { - var stat: wasi.filestat_t = undefined; - switch (wasi.path_filestat_get(dirfd, flags, pathname.ptr, pathname.len, &stat)) { - .SUCCESS => return stat, - .INVAL => unreachable, - .BADF => unreachable, // Always a race condition. - .NOMEM => return error.SystemResources, - .ACCES => return error.AccessDenied, - .FAULT => unreachable, - .NAMETOOLONG => return error.NameTooLong, - .NOENT => return error.FileNotFound, - .NOTDIR => return error.FileNotFound, - .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, - else => |err| return posix.unexpectedErrno(err), - } -} - pub fn fstat_wasi(fd: posix.fd_t) posix.FStatError!wasi.filestat_t { var stat: wasi.filestat_t = undefined; switch (wasi.fd_filestat_get(fd, &stat)) { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index cbf49638a3..f36effe380 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -146,7 +146,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN // call has failed. There is not really a sane way to handle // this other than retrying the creation after the OS finishes // the deletion. - std.Thread.sleep(std.time.ns_per_ms); + kernel32.Sleep(1); continue; }, .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, @@ -604,7 +604,7 @@ pub const ReadFileError = error{ BrokenPipe, /// The specified network name is no longer available. ConnectionResetByPeer, - OperationAborted, + Canceled, /// Unable to read file due to lock. LockViolation, /// Known to be possible when: @@ -654,7 +654,7 @@ pub fn ReadFile(in_hFile: HANDLE, buffer: []u8, offset: ?u64) ReadFileError!usiz pub const WriteFileError = error{ SystemResources, - OperationAborted, + Canceled, BrokenPipe, NotOpenForWriting, /// The process cannot access the file because another process has locked @@ -694,7 +694,7 @@ pub fn WriteFile( switch (GetLastError()) { .INVALID_USER_BUFFER => return error.SystemResources, .NOT_ENOUGH_MEMORY => return error.SystemResources, - .OPERATION_ABORTED => return error.OperationAborted, + .OPERATION_ABORTED => return error.Canceled, .NOT_ENOUGH_QUOTA => return error.SystemResources, .IO_PENDING => unreachable, .NO_DATA => return error.BrokenPipe, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index b27468617c..651f6eb117 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -821,9 +821,6 @@ pub const ReadError = std.Io.File.ReadStreamingError; /// The corresponding POSIX limit is `maxInt(isize)`. pub fn read(fd: fd_t, buf: []u8) ReadError!usize { if (buf.len == 0) return 0; - if (native_os == .windows) { - return windows.ReadFile(fd, buf, null); - } if (native_os == .wasi and !builtin.link_libc) { const iovs = [1]iovec{iovec{ .base = buf.ptr, @@ -1181,7 +1178,7 @@ pub const WriteError = error{ PermissionDenied, BrokenPipe, SystemResources, - OperationAborted, + Canceled, NotOpenForWriting, /// The process cannot access the file because another process has locked @@ -1597,10 +1594,7 @@ pub fn openZ(file_path: [*:0]const u8, flags: O, perm: mode_t) OpenError!fd_t { .PERM => return error.PermissionDenied, .EXIST => return error.PathAlreadyExists, .BUSY => return error.DeviceBusy, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -1678,7 +1672,7 @@ pub fn openatWasi( .EXIST => return error.PathAlreadyExists, .BUSY => return error.DeviceBusy, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -1773,10 +1767,7 @@ pub fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: O, mode: mode_t) O .AGAIN => return error.WouldBlock, .TXTBSY => return error.FileBusy, .NXIO => return error.NoDevice, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2084,10 +2075,7 @@ pub fn symlinkZ(target_path: [*:0]const u8, sym_link_path: [*:0]const u8) SymLin .NOMEM => return error.SystemResources, .NOSPC => return error.NoSpaceLeft, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2133,7 +2121,7 @@ pub fn symlinkatWasi(target_path: []const u8, newdirfd: fd_t, sym_link_path: []c .NOSPC => return error.NoSpaceLeft, .ROFS => return error.ReadOnlyFileSystem, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2162,10 +2150,7 @@ pub fn symlinkatZ(target_path: [*:0]const u8, newdirfd: fd_t, sym_link_path: [*: .NOMEM => return error.SystemResources, .NOSPC => return error.NoSpaceLeft, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2184,9 +2169,7 @@ pub const LinkError = UnexpectedError || error{ NoSpaceLeft, ReadOnlyFileSystem, NotSameFileSystem, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, + BadPathName, }; /// On WASI, both paths should be encoded as valid UTF-8. @@ -2212,10 +2195,7 @@ pub fn linkZ(oldpath: [*:0]const u8, newpath: [*:0]const u8) LinkError!void { .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.NotSameFileSystem, .INVAL => unreachable, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2266,10 +2246,7 @@ pub fn linkatZ( .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.NotSameFileSystem, .INVAL => unreachable, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2315,7 +2292,7 @@ pub fn linkat( .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.NotSameFileSystem, .INVAL => unreachable, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2398,10 +2375,7 @@ pub fn unlinkZ(file_path: [*:0]const u8) UnlinkError!void { .NOTDIR => return error.NotDir, .NOMEM => return error.SystemResources, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2460,7 +2434,7 @@ pub fn unlinkatWasi(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatErro .ROFS => return error.ReadOnlyFileSystem, .NOTEMPTY => return error.DirNotEmpty, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, .INVAL => unreachable, // invalid flags, or pathname has . as last component .BADF => unreachable, // always a race condition @@ -2493,10 +2467,7 @@ pub fn unlinkatZ(dirfd: fd_t, file_path_c: [*:0]const u8, flags: u32) UnlinkatEr .ROFS => return error.ReadOnlyFileSystem, .EXIST => return error.DirNotEmpty, .NOTEMPTY => return error.DirNotEmpty, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, .INVAL => unreachable, // invalid flags, or pathname has . as last component .BADF => unreachable, // always a race condition @@ -2598,10 +2569,7 @@ pub fn renameZ(old_path: [*:0]const u8, new_path: [*:0]const u8) RenameError!voi .NOTEMPTY => return error.PathAlreadyExists, .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.RenameAcrossMountPoints, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2662,7 +2630,7 @@ fn renameatWasi(old: RelativePathWasi, new: RelativePathWasi) RenameError!void { .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.RenameAcrossMountPoints, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2713,10 +2681,7 @@ pub fn renameatZ( .NOTEMPTY => return error.PathAlreadyExists, .ROFS => return error.ReadOnlyFileSystem, .XDEV => return error.RenameAcrossMountPoints, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2870,7 +2835,7 @@ pub fn mkdiratWasi(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDir .NOTDIR => return error.NotDir, .ROFS => return error.ReadOnlyFileSystem, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2901,10 +2866,7 @@ pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDir .ROFS => return error.ReadOnlyFileSystem, // dragonfly: when dir_fd is unlinked from filesystem .NOTCONN => return error.FileNotFound, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -2973,10 +2935,7 @@ pub fn mkdirZ(dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void { .NOSPC => return error.NoSpaceLeft, .NOTDIR => return error.NotDir, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3067,10 +3026,7 @@ pub fn rmdirZ(dir_path: [*:0]const u8) DeleteDirError!void { .EXIST => return error.DirNotEmpty, .NOTEMPTY => return error.DirNotEmpty, .ROFS => return error.ReadOnlyFileSystem, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3145,10 +3101,7 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { .NOENT => return error.FileNotFound, .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3254,10 +3207,7 @@ pub fn readlinkZ(file_path: [*:0]const u8, out_buffer: []u8) ReadLinkError![]u8 .NOENT => return error.FileNotFound, .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3299,7 +3249,7 @@ pub fn readlinkatWasi(dirfd: fd_t, file_path: []const u8, out_buffer: []u8) Read .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -3332,10 +3282,7 @@ pub fn readlinkatZ(dirfd: fd_t, file_path: [*:0]const u8, out_buffer: []u8) Read .NOENT => return error.FileNotFound, .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -4421,13 +4368,10 @@ pub const FStatAtError = FStatError || error{ /// which is relative to `dirfd` handle. /// On WASI, `pathname` should be encoded as valid UTF-8. /// On other platforms, `pathname` is an opaque sequence of bytes with no particular encoding. -/// See also `fstatatZ` and `std.os.fstatat_wasi`. +/// See also `fstatatZ`. pub fn fstatat(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!Stat { if (native_os == .wasi and !builtin.link_libc) { - const filestat = try std.os.fstatat_wasi(dirfd, pathname, .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - return Stat.fromFilestat(filestat); + @compileError("use std.Io instead"); } else if (native_os == .windows) { @compileError("fstatat is not yet implemented on Windows"); } else { @@ -4440,10 +4384,7 @@ pub fn fstatat(dirfd: fd_t, pathname: []const u8, flags: u32) FStatAtError!Stat /// See also `fstatat`. pub fn fstatatZ(dirfd: fd_t, pathname: [*:0]const u8, flags: u32) FStatAtError!Stat { if (native_os == .wasi and !builtin.link_libc) { - const filestat = try std.os.fstatat_wasi(dirfd, mem.sliceTo(pathname, 0), .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - return Stat.fromFilestat(filestat); + @compileError("use std.Io instead"); } const fstatat_sym = if (lfs64_abi) system.fstatat64 else system.fstatat; @@ -4460,10 +4401,7 @@ pub fn fstatatZ(dirfd: fd_t, pathname: [*:0]const u8, flags: u32) FStatAtError!S .LOOP => return error.SymLinkLoop, .NOENT => return error.FileNotFound, .NOTDIR => return error.FileNotFound, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -4991,10 +4929,7 @@ pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { .FAULT => unreachable, .IO => return error.InputOutput, .NOMEM => return error.SystemResources, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } @@ -5072,10 +5007,7 @@ pub fn faccessatZ(dirfd: fd_t, path: [*:0]const u8, mode: u32, flags: u32) Acces .FAULT => unreachable, .IO => return error.InputOutput, .NOMEM => return error.SystemResources, - .ILSEQ => |err| if (native_os == .wasi) - return error.InvalidUtf8 - else - return unexpectedErrno(err), + .ILSEQ => return error.BadPathName, else => |err| return unexpectedErrno(err), } } diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index b5ce476441..230440a2f1 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -226,49 +226,6 @@ test "linkat with different directories" { } } -test "fstatat" { - if ((builtin.cpu.arch == .riscv32 or builtin.cpu.arch.isLoongArch()) and builtin.os.tag == .linux and !builtin.link_libc) return error.SkipZigTest; // No `fstatat()`. - // enable when `fstat` and `fstatat` are implemented on Windows - if (native_os == .windows) return error.SkipZigTest; - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - // create dummy file - const contents = "nonsense"; - try tmp.dir.writeFile(.{ .sub_path = "file.txt", .data = contents }); - - // fetch file's info on the opened fd directly - const file = try tmp.dir.openFile("file.txt", .{}); - const stat = try posix.fstat(file.handle); - defer file.close(); - - // now repeat but using `fstatat` instead - const statat = try posix.fstatat(tmp.dir.fd, "file.txt", posix.AT.SYMLINK_NOFOLLOW); - - try expectEqual(stat.dev, statat.dev); - try expectEqual(stat.ino, statat.ino); - try expectEqual(stat.nlink, statat.nlink); - try expectEqual(stat.mode, statat.mode); - try expectEqual(stat.uid, statat.uid); - try expectEqual(stat.gid, statat.gid); - try expectEqual(stat.rdev, statat.rdev); - try expectEqual(stat.size, statat.size); - try expectEqual(stat.blksize, statat.blksize); - - // The stat.blocks/statat.blocks count is managed by the filesystem and may - // change if the file is stored in a journal or "inline". - // try expectEqual(stat.blocks, statat.blocks); - - // s390x-linux does not have nanosecond precision for fstat(), but it does for - // fstatat(). As a result, comparing the timestamps isn't worth the effort - if (!(builtin.cpu.arch == .s390x and builtin.os.tag == .linux)) { - try expectEqual(stat.atime(), statat.atime()); - try expectEqual(stat.mtime(), statat.mtime()); - try expectEqual(stat.ctime(), statat.ctime()); - } -} - test "readlinkat" { var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/test/src/convert-stack-trace.zig b/test/src/convert-stack-trace.zig index c7cd01a460..91be53a8e5 100644 --- a/test/src/convert-stack-trace.zig +++ b/test/src/convert-stack-trace.zig @@ -32,6 +32,12 @@ pub fn main() !void { const args = try std.process.argsAlloc(arena); if (args.len != 2) std.process.fatal("usage: convert-stack-trace path/to/test/output", .{}); + const gpa = arena; + + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var read_buf: [1024]u8 = undefined; var write_buf: [1024]u8 = undefined; @@ -40,7 +46,7 @@ pub fn main() !void { const out_file: std.fs.File = .stdout(); - var in_fr = in_file.reader(&read_buf); + var in_fr = in_file.reader(io, &read_buf); var out_fw = out_file.writer(&write_buf); const w = &out_fw.interface; diff --git a/tools/docgen.zig b/tools/docgen.zig index 18311b0d54..d23892e06c 100644 --- a/tools/docgen.zig +++ b/tools/docgen.zig @@ -36,6 +36,12 @@ pub fn main() !void { var args_it = try process.argsWithAllocator(arena); if (!args_it.skip()) @panic("expected self arg"); + const gpa = arena; + + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var opt_code_dir: ?[]const u8 = null; var opt_input: ?[]const u8 = null; var opt_output: ?[]const u8 = null; @@ -77,7 +83,7 @@ pub fn main() !void { var code_dir = try fs.cwd().openDir(code_dir_path, .{}); defer code_dir.close(); - var in_file_reader = in_file.reader(&.{}); + var in_file_reader = in_file.reader(io, &.{}); const input_file_bytes = try in_file_reader.interface.allocRemaining(arena, .limited(max_doc_file_size)); var tokenizer = Tokenizer.init(input_path, input_file_bytes); diff --git a/tools/doctest.zig b/tools/doctest.zig index ccdbc6b065..de8d3a80ee 100644 --- a/tools/doctest.zig +++ b/tools/doctest.zig @@ -1,5 +1,8 @@ const builtin = @import("builtin"); + const std = @import("std"); +const Io = std.Io; +const Writer = std.Io.Writer; const fatal = std.process.fatal; const mem = std.mem; const fs = std.fs; @@ -7,7 +10,6 @@ const process = std.process; const Allocator = std.mem.Allocator; const testing = std.testing; const getExternalExecutor = std.zig.system.getExternalExecutor; -const Writer = std.Io.Writer; const max_doc_file_size = 10 * 1024 * 1024; @@ -36,6 +38,12 @@ pub fn main() !void { var args_it = try process.argsWithAllocator(arena); if (!args_it.skip()) fatal("missing argv[0]", .{}); + const gpa = arena; + + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var opt_input: ?[]const u8 = null; var opt_output: ?[]const u8 = null; var opt_zig: ?[]const u8 = null; @@ -93,6 +101,7 @@ pub fn main() !void { try printSourceBlock(arena, out, source, fs.path.basename(input_path)); try printOutput( arena, + io, out, code, tmp_dir_path, @@ -109,6 +118,7 @@ pub fn main() !void { fn printOutput( arena: Allocator, + io: Io, out: *Writer, code: Code, /// Relative to this process' cwd. @@ -123,11 +133,11 @@ fn printOutput( var env_map = try process.getEnvMap(arena); try env_map.put("CLICOLOR_FORCE", "1"); - const host = try std.zig.system.resolveTargetQuery(.{}); + const host = try std.zig.system.resolveTargetQuery(io, .{}); const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch); const print = std.debug.print; - var shell_buffer: std.Io.Writer.Allocating = .init(arena); + var shell_buffer: Writer.Allocating = .init(arena); defer shell_buffer.deinit(); const shell_out = &shell_buffer.writer; @@ -238,7 +248,7 @@ fn printOutput( const target_query = try std.Target.Query.parse(.{ .arch_os_abi = code.target_str orelse "native", }); - const target = try std.zig.system.resolveTargetQuery(target_query); + const target = try std.zig.system.resolveTargetQuery(io, target_query); const path_to_exe = try std.fmt.allocPrint(arena, "./{s}{s}", .{ code_name, target.exeFileExt(), @@ -316,9 +326,7 @@ fn printOutput( const target_query = try std.Target.Query.parse(.{ .arch_os_abi = triple, }); - const target = try std.zig.system.resolveTargetQuery( - target_query, - ); + const target = try std.zig.system.resolveTargetQuery(io, target_query); switch (getExternalExecutor(&host, &target, .{ .link_libc = code.link_libc, })) { @@ -1397,7 +1405,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1414,7 +1422,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1428,7 +1436,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1447,7 +1455,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1468,7 +1476,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1487,7 +1495,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1510,7 +1518,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1532,7 +1540,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1549,7 +1557,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1568,7 +1576,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); @@ -1583,7 +1591,7 @@ test "printShell" { \\ ; - var buffer: std.Io.Writer.Allocating = .init(test_allocator); + var buffer: Writer.Allocating = .init(test_allocator); defer buffer.deinit(); try printShell(&buffer.writer, shell_out, false); From f8ea00bd6dccba678901b50904972c017c9e66a1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 20:53:28 -0700 Subject: [PATCH 129/244] std.Io: add dirAccess --- lib/std/Io.zig | 1 + lib/std/Io/Dir.zig | 31 +++++++++++ lib/std/Io/File.zig | 62 ++++++++++++++++++++- lib/std/Io/Threaded.zig | 118 +++++++++++++++++++++++++++++++++++++-- lib/std/fs.zig | 7 ++- lib/std/fs/Dir.zig | 47 +++------------- lib/std/fs/File.zig | 65 ++-------------------- lib/std/posix.zig | 120 +--------------------------------------- 8 files changed, 225 insertions(+), 226 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 6ae4c3346c..a20868afe2 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -663,6 +663,7 @@ pub const VTable = struct { dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, + dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void, dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index f07b8f64a8..634c1f9fff 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -23,6 +23,37 @@ pub const PathNameError = error{ BadPathName, }; +pub const AccessError = error{ + AccessDenied, + PermissionDenied, + FileNotFound, + InputOutput, + SystemResources, + FileBusy, + SymLinkLoop, + ReadOnlyFileSystem, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + +pub const AccessOptions = packed struct { + follow_symlinks: bool = true, + read: bool = false, + write: bool = false, + execute: bool = false, +}; + +/// Test accessing `sub_path`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// +/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this +/// function. For example, instead of testing if a file exists and then opening +/// it, just open it and handle the error for file not found. +pub fn access(dir: Dir, io: Io, sub_path: []const u8, options: AccessOptions) AccessError!void { + return io.vtable.dirAccess(io.userdata, dir, sub_path, options); +} + pub const OpenError = error{ FileNotFound, NotDir, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 715667b0d4..51f3a08df7 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -80,7 +80,67 @@ pub fn stat(file: File, io: Io) StatError!Stat { return io.vtable.fileStat(io.userdata, file); } -pub const OpenFlags = std.fs.File.OpenFlags; +pub const OpenMode = enum { + read_only, + write_only, + read_write, +}; + +pub const Lock = enum { + none, + shared, + exclusive, +}; + +pub const OpenFlags = struct { + mode: OpenMode = .read_only, + + /// Open the file with an advisory lock to coordinate with other processes + /// accessing it at the same time. An exclusive lock will prevent other + /// processes from acquiring a lock. A shared lock will prevent other + /// processes from acquiring a exclusive lock, but does not prevent + /// other process from getting their own shared locks. + /// + /// The lock is advisory, except on Linux in very specific circumstances[1]. + /// This means that a process that does not respect the locking API can still get access + /// to the file, despite the lock. + /// + /// On these operating systems, the lock is acquired atomically with + /// opening the file: + /// * Darwin + /// * DragonFlyBSD + /// * FreeBSD + /// * Haiku + /// * NetBSD + /// * OpenBSD + /// On these operating systems, the lock is acquired via a separate syscall + /// after opening the file: + /// * Linux + /// * Windows + /// + /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt + lock: Lock = .none, + + /// Sets whether or not to wait until the file is locked to return. If set to true, + /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file + /// is available to proceed. + lock_nonblocking: bool = false, + + /// Set this to allow the opened file to automatically become the + /// controlling TTY for the current process. + allow_ctty: bool = false, + + follow_symlinks: bool = true, + + pub fn isRead(self: OpenFlags) bool { + return self.mode != .write_only; + } + + pub fn isWrite(self: OpenFlags) bool { + return self.mode != .read_only; + } +}; + pub const CreateFlags = std.fs.File.CreateFlags; pub const OpenError = error{ diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 57578628fc..fb9319ef26 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -183,6 +183,11 @@ pub fn io(t: *Threaded) Io { .wasi => fileStatWasi, else => fileStatPosix, }, + .dirAccess = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => dirAccessWasi, + else => dirAccessPosix, + }, .dirCreateFile = switch (builtin.os.tag) { .windows => @panic("TODO"), .wasi => @panic("TODO"), @@ -992,7 +997,6 @@ fn dirStatPathWasi( ) Io.Dir.StatPathError!Io.File.Stat { if (builtin.link_libc) return dirStatPathPosix(userdata, dir, sub_path, options); const t: *Threaded = @ptrCast(@alignCast(userdata)); - const dir_fd = dir.handle; const wasi = std.os.wasi; const flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), @@ -1000,16 +1004,16 @@ fn dirStatPathWasi( var stat: wasi.filestat_t = undefined; while (true) { try t.checkCancel(); - switch (wasi.path_filestat_get(dir_fd, flags, sub_path.ptr, sub_path.len, &stat)) { + switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { .SUCCESS => return statFromWasi(stat), .INTR => continue, .CANCELED => return error.Canceled, - .INVAL => |err| errnoBug(err), - .BADF => |err| errnoBug(err), // Always a race condition. + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, - .FAULT => |err| errnoBug(err), + .FAULT => |err| return errnoBug(err), .NAMETOOLONG => return error.NameTooLong, .NOENT => return error.FileNotFound, .NOTDIR => return error.FileNotFound, @@ -1103,6 +1107,110 @@ const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.syste const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; +fn dirAccessPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = @as(u32, if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0); + + const mode: u32 = + @as(u32, if (options.read) posix.R_OK else 0) | + @as(u32, if (options.write) posix.W_OK else 0) | + @as(u32, if (options.execute) posix.X_OK else 0); + + while (true) { + try t.checkCancel(); + switch (posix.errno(posix.system.faccessat(dir.handle, sub_path_posix, mode, flags))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .ROFS => return error.ReadOnlyFileSystem, + .LOOP => return error.SymLinkLoop, + .TXTBSY => return error.FileBusy, + .NOTDIR => return error.FileNotFound, + .NOENT => return error.FileNotFound, + .NAMETOOLONG => return error.NameTooLong, + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .IO => return error.InputOutput, + .NOMEM => return error.SystemResources, + .ILSEQ => return error.BadPathName, // TODO move to wasi + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirAccessWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.File.OpenFlags, +) Io.File.AccessError!void { + if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + const flags: wasi.lookupflags_t = .{ + .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), + }; + const stat = while (true) { + var stat: wasi.filestat_t = undefined; + try t.checkCancel(); + switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { + .SUCCESS => break statFromWasi(stat), + .INTR => continue, + .CANCELED => return error.Canceled, + + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + + if (!options.mode.read and !options.mode.write and !options.mode.execute) + return; + + var directory: wasi.fdstat_t = undefined; + if (wasi.fd_fdstat_get(dir.handle, &directory) != .SUCCESS) + return error.AccessDenied; + + var rights: wasi.rights_t = .{}; + if (options.mode.read) { + if (stat.filetype == .DIRECTORY) { + rights.FD_READDIR = true; + } else { + rights.FD_READ = true; + } + } + if (options.mode.write) + rights.FD_WRITE = true; + + // No validation for execution. + + // https://github.com/ziglang/zig/issues/18882 + const rights_int: u64 = @bitCast(rights); + const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting); + if ((rights_int & inheriting_int) != rights_int) + return error.AccessDenied; +} + fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 96aa962e67..395e18e6e5 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1,14 +1,15 @@ //! File System. +const builtin = @import("builtin"); +const native_os = builtin.os.tag; const std = @import("std.zig"); -const builtin = @import("builtin"); +const Io = std.Io; const root = @import("root"); const mem = std.mem; const base64 = std.base64; const crypto = std.crypto; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const native_os = builtin.os.tag; const posix = std.posix; const windows = std.os.windows; @@ -274,7 +275,7 @@ pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) Fi /// On Windows, `absolute_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `absolute_path` should be encoded as valid UTF-8. /// On other platforms, `absolute_path` is an opaque sequence of bytes with no particular encoding. -pub fn accessAbsolute(absolute_path: []const u8, flags: File.OpenFlags) Dir.AccessError!void { +pub fn accessAbsolute(absolute_path: []const u8, flags: Io.Dir.AccessOptions) Dir.AccessError!void { assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 3565d2fc2c..c7699a83cf 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2353,47 +2353,14 @@ pub fn writeFile(self: Dir, options: WriteFileOptions) WriteFileError!void { try file.writeAll(options.data); } -pub const AccessError = posix.AccessError; +/// Deprecated in favor of `Io.Dir.AccessError`. +pub const AccessError = Io.Dir.AccessError; -/// Test accessing `sub_path`. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -/// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. -/// For example, instead of testing if a file exists and then opening it, just -/// open it and handle the error for file not found. -pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { - if (native_os == .windows) { - const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.accessW(sub_path_w.span().ptr, flags); - } - const path_c = try posix.toPosixPath(sub_path); - return self.accessZ(&path_c, flags); -} - -/// Same as `access` except the path parameter is null-terminated. -pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { - if (native_os == .windows) { - const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path); - return self.accessW(sub_path_w.span().ptr, flags); - } - const os_mode = switch (flags.mode) { - .read_only => @as(u32, posix.F_OK), - .write_only => @as(u32, posix.W_OK), - .read_write => @as(u32, posix.R_OK | posix.W_OK), - }; - const result = posix.faccessatZ(self.fd, sub_path, os_mode, 0); - return result; -} - -/// Same as `access` except asserts the target OS is Windows and the path parameter is -/// * WTF-16 LE encoded -/// * null-terminated -/// * relative or has the NT namespace prefix -/// TODO currently this ignores `flags`. -pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { - _ = flags; - return posix.faccessatW(self.fd, sub_path_w); +/// Deprecated in favor of `Io.Dir.access`. +pub fn access(self: Dir, sub_path: []const u8, options: Io.Dir.AccessOptions) AccessError!void { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.access(self.adaptToNewApi(), io, sub_path, options); } pub const CopyFileOptions = struct { diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 1299a2c2c9..ca3fb47a5a 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -40,65 +40,12 @@ pub const default_mode = switch (builtin.os.tag) { /// Deprecated in favor of `Io.File.OpenError`. pub const OpenError = Io.File.OpenError || error{WouldBlock}; - -pub const OpenMode = enum { - read_only, - write_only, - read_write, -}; - -pub const Lock = enum { - none, - shared, - exclusive, -}; - -pub const OpenFlags = struct { - mode: OpenMode = .read_only, - - /// Open the file with an advisory lock to coordinate with other processes - /// accessing it at the same time. An exclusive lock will prevent other - /// processes from acquiring a lock. A shared lock will prevent other - /// processes from acquiring a exclusive lock, but does not prevent - /// other process from getting their own shared locks. - /// - /// The lock is advisory, except on Linux in very specific circumstances[1]. - /// This means that a process that does not respect the locking API can still get access - /// to the file, despite the lock. - /// - /// On these operating systems, the lock is acquired atomically with - /// opening the file: - /// * Darwin - /// * DragonFlyBSD - /// * FreeBSD - /// * Haiku - /// * NetBSD - /// * OpenBSD - /// On these operating systems, the lock is acquired via a separate syscall - /// after opening the file: - /// * Linux - /// * Windows - /// - /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt - lock: Lock = .none, - - /// Sets whether or not to wait until the file is locked to return. If set to true, - /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file - /// is available to proceed. - lock_nonblocking: bool = false, - - /// Set this to allow the opened file to automatically become the - /// controlling TTY for the current process. - allow_ctty: bool = false, - - pub fn isRead(self: OpenFlags) bool { - return self.mode != .write_only; - } - - pub fn isWrite(self: OpenFlags) bool { - return self.mode != .read_only; - } -}; +/// Deprecated in favor of `Io.File.OpenMode`. +pub const OpenMode = Io.File.OpenMode; +/// Deprecated in favor of `Io.File.Lock`. +pub const Lock = Io.File.Lock; +/// Deprecated in favor of `Io.File.OpenFlags`. +pub const OpenFlags = Io.File.OpenFlags; pub const CreateFlags = struct { /// Whether the file will be created with read access. diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 651f6eb117..999fe5e50a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -4360,8 +4360,7 @@ pub const FStatAtError = FStatError || error{ NameTooLong, FileNotFound, SymLinkLoop, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, + BadPathName, }; /// Similar to `fstat`, but returns stat of a resource pointed to by `pathname` @@ -4900,7 +4899,7 @@ pub fn access(path: []const u8, mode: u32) AccessError!void { _ = try windows.GetFileAttributesW(path_w.span().ptr); return; } else if (native_os == .wasi and !builtin.link_libc) { - return faccessat(AT.FDCWD, path, mode, 0); + @compileError("wasi doesn't support absolute paths"); } const path_c = try toPosixPath(path); return accessZ(&path_c, mode); @@ -4934,121 +4933,6 @@ pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { } } -/// Check user's permissions for a file, based on an open directory handle. -/// -/// * On Windows, asserts `path` is valid [WTF-8](https://wtf-8.codeberg.page/). -/// * On WASI, invalid UTF-8 passed to `path` causes `error.InvalidUtf8`. -/// * On other platforms, `path` is an opaque sequence of bytes with no particular encoding. -/// -/// On Windows, `mode` is ignored. This is a POSIX API that is only partially supported by -/// Windows. See `fs` for the cross-platform file system API. -pub fn faccessat(dirfd: fd_t, path: []const u8, mode: u32, flags: u32) AccessError!void { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(dirfd, path); - return faccessatW(dirfd, path_w.span().ptr); - } else if (native_os == .wasi and !builtin.link_libc) { - const resolved: RelativePathWasi = .{ .dir_fd = dirfd, .relative_path = path }; - - const st = try std.os.fstatat_wasi(dirfd, path, .{ - .SYMLINK_FOLLOW = (flags & AT.SYMLINK_NOFOLLOW) == 0, - }); - - if (mode != F_OK) { - var directory: wasi.fdstat_t = undefined; - if (wasi.fd_fdstat_get(resolved.dir_fd, &directory) != .SUCCESS) { - return error.AccessDenied; - } - - var rights: wasi.rights_t = .{}; - if (mode & R_OK != 0) { - if (st.filetype == .DIRECTORY) { - rights.FD_READDIR = true; - } else { - rights.FD_READ = true; - } - } - if (mode & W_OK != 0) { - rights.FD_WRITE = true; - } - // No validation for X_OK - - // https://github.com/ziglang/zig/issues/18882 - const rights_int: u64 = @bitCast(rights); - const inheriting_int: u64 = @bitCast(directory.fs_rights_inheriting); - if ((rights_int & inheriting_int) != rights_int) { - return error.AccessDenied; - } - } - return; - } - const path_c = try toPosixPath(path); - return faccessatZ(dirfd, &path_c, mode, flags); -} - -/// Same as `faccessat` except the path parameter is null-terminated. -pub fn faccessatZ(dirfd: fd_t, path: [*:0]const u8, mode: u32, flags: u32) AccessError!void { - if (native_os == .windows) { - const path_w = try windows.cStrToPrefixedFileW(dirfd, path); - return faccessatW(dirfd, path_w.span().ptr); - } else if (native_os == .wasi and !builtin.link_libc) { - return faccessat(dirfd, mem.sliceTo(path, 0), mode, flags); - } - switch (errno(system.faccessat(dirfd, path, mode, flags))) { - .SUCCESS => return, - .ACCES => return error.AccessDenied, - .PERM => return error.PermissionDenied, - .ROFS => return error.ReadOnlyFileSystem, - .LOOP => return error.SymLinkLoop, - .TXTBSY => return error.FileBusy, - .NOTDIR => return error.FileNotFound, - .NOENT => return error.FileNotFound, - .NAMETOOLONG => return error.NameTooLong, - .INVAL => unreachable, - .FAULT => unreachable, - .IO => return error.InputOutput, - .NOMEM => return error.SystemResources, - .ILSEQ => return error.BadPathName, - else => |err| return unexpectedErrno(err), - } -} - -/// Same as `faccessat` except asserts the target is Windows and the path parameter -/// is NtDll-prefixed, null-terminated, WTF-16 encoded. -pub fn faccessatW(dirfd: fd_t, sub_path_w: [*:0]const u16) AccessError!void { - if (sub_path_w[0] == '.' and sub_path_w[1] == 0) { - return; - } - if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) { - return; - } - - const path_len_bytes = cast(u16, mem.sliceTo(sub_path_w, 0).len * 2) orelse return error.NameTooLong; - var nt_name = windows.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = windows.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), - .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else dirfd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - var basic_info: windows.FILE_BASIC_INFORMATION = undefined; - switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) { - .SUCCESS => return, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_INVALID => unreachable, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - .OBJECT_PATH_SYNTAX_BAD => unreachable, - else => |rc| return windows.unexpectedStatus(rc), - } -} - pub const PipeError = error{ SystemFdQuotaExceeded, ProcessFdQuotaExceeded, From ec9dfc540b95a5577074e8915670ac2920b32f64 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 22:04:44 -0700 Subject: [PATCH 130/244] std.Io.Threaded: handle ECANCELED none of these APIs are documented to return this error code, but it would be cool if they did. --- lib/std/Io/Threaded.zig | 66 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index fb9319ef26..6a5eab2bdf 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -882,6 +882,8 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, + .ACCES => return error.AccessDenied, .BADF => |err| return errnoBug(err), .PERM => return error.PermissionDenied, @@ -940,6 +942,8 @@ fn dirStatPathLinux( switch (linux.E.init(rc)) { .SUCCESS => return statFromLinux(&statx), .INTR => continue, + .CANCELED => return error.Canceled, + .ACCES => return error.AccessDenied, .BADF => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), @@ -973,6 +977,8 @@ fn dirStatPathPosix( switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) { .SUCCESS => return statFromPosix(&stat), .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), // Always a race condition. .NOMEM => return error.SystemResources, @@ -1035,6 +1041,8 @@ fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File switch (posix.errno(fstat_sym(file.handle, &stat))) { .SUCCESS => return statFromPosix(&stat), .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), .NOMEM => return error.SystemResources, @@ -1060,6 +1068,8 @@ fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File switch (linux.E.init(rc)) { .SUCCESS => return statFromLinux(&statx), .INTR => continue, + .CANCELED => return error.Canceled, + .ACCES => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), @@ -1090,6 +1100,8 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. switch (std.os.wasi.fd_filestat_get(file.handle, &stat)) { .SUCCESS => return statFromWasi(&stat), .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), .NOMEM => return error.SystemResources, @@ -1253,6 +1265,7 @@ fn dirCreateFilePosix( switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, .FAULT => |err| return errnoBug(err), .INVAL => return error.BadPathName, @@ -1296,6 +1309,7 @@ fn dirCreateFilePosix( switch (posix.errno(posix.system.flock(fd, lock_flags))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, .BADF => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), // invalid parameters @@ -1314,6 +1328,7 @@ fn dirCreateFilePosix( switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, else => |err| return posix.unexpectedErrno(err), } }; @@ -1323,6 +1338,7 @@ fn dirCreateFilePosix( switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, else => |err| return posix.unexpectedErrno(err), } } @@ -1383,6 +1399,7 @@ fn dirOpenFile( switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, .FAULT => |err| return errnoBug(err), .INVAL => return error.BadPathName, @@ -1426,6 +1443,7 @@ fn dirOpenFile( switch (posix.errno(posix.system.flock(fd, lock_flags))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, .BADF => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), // invalid parameters @@ -1444,6 +1462,7 @@ fn dirOpenFile( switch (posix.errno(rc)) { .SUCCESS => break @intCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, else => |err| return posix.unexpectedErrno(err), } }; @@ -1453,6 +1472,7 @@ fn dirOpenFile( switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, else => |err| return posix.unexpectedErrno(err), } } @@ -1526,6 +1546,8 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File switch (std.os.wasi.fd_read(file.handle, dest.ptr, dest.len, &nread)) { .SUCCESS => return nread, .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), @@ -1547,6 +1569,8 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, @@ -1647,6 +1671,8 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset switch (std.os.wasi.fd_pread(file.handle, dest.ptr, dest.len, offset, &nread)) { .SUCCESS => return nread, .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), @@ -1672,6 +1698,8 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset switch (posix.errno(rc)) { .SUCCESS => return @bitCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, @@ -1711,6 +1739,8 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr switch (posix.errno(posix.system.llseek(fd, offset, &result, posix.SEEK.SET))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, + .BADF => |err| return errnoBug(err), // Always a race condition. .INVAL => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1731,6 +1761,8 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr switch (std.os.wasi.fd_seek(fd, @bitCast(offset), .SET, &new_offset)) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, + .BADF => |err| return errnoBug(err), // Always a race condition. .INVAL => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1748,6 +1780,8 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr switch (posix.errno(lseek_sym(fd, @bitCast(offset), posix.SEEK.SET))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, + .BADF => |err| return errnoBug(err), // Always a race condition. .INVAL => return error.Unseekable, .OVERFLOW => return error.Unseekable, @@ -1845,6 +1879,7 @@ fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { } }, ×pec, ×pec))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, .INVAL => return error.UnsupportedClock, else => |err| return posix.unexpectedErrno(err), } @@ -1907,6 +1942,7 @@ fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { try t.checkCancel(); switch (posix.errno(posix.system.nanosleep(×pec, ×pec))) { .INTR => continue, + .CANCELED => return error.Canceled, else => return, // This prong handles success as well as unexpected errors. } } @@ -2024,6 +2060,8 @@ fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, switch (posix.errno(posix.system.bind(fd, addr, addr_len))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, + .ACCES => return error.AccessDenied, .ADDRINUSE => return error.AddressInUse, .AFNOSUPPORT => return error.AddressFamilyUnsupported, @@ -2052,6 +2090,8 @@ fn posixBind(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sockadd switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, + .ADDRINUSE => return error.AddressInUse, .BADF => |err| return errnoBug(err), // always a race condition if this error is returned .INVAL => |err| return errnoBug(err), // invalid parameters @@ -2071,6 +2111,8 @@ fn posixConnect(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sock switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, + .ADDRNOTAVAIL => return error.AddressUnavailable, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, @@ -2100,6 +2142,7 @@ fn posixConnectUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockadd switch (posix.errno(posix.system.connect(fd, addr, addr_len))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN => return error.WouldBlock, @@ -2129,6 +2172,8 @@ fn posixGetSockName(t: *Threaded, socket_fd: posix.fd_t, addr: *posix.sockaddr, switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, + .BADF => |err| return errnoBug(err), // always a race condition .FAULT => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), // invalid parameters @@ -2146,6 +2191,8 @@ fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, opti switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) { .SUCCESS => return, .INTR => continue, + .CANCELED => return error.Canceled, + .BADF => |err| return errnoBug(err), // always a race condition .NOTSOCK => |err| return errnoBug(err), // always a race condition .INVAL => |err| return errnoBug(err), @@ -2245,12 +2292,15 @@ fn openSocketPosix( switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, else => |err| return posix.unexpectedErrno(err), } }; break fd; }, .INTR => continue, + .CANCELED => return error.Canceled, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, .INVAL => return error.ProtocolUnsupportedBySystem, .MFILE => return error.ProcessFdQuotaExceeded, @@ -2294,12 +2344,14 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Serve switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { .SUCCESS => break, .INTR => continue, + .CANCELED => return error.Canceled, else => |err| return posix.unexpectedErrno(err), } }; break fd; }, .INTR => continue, + .CANCELED => return error.Canceled, .AGAIN => |err| return errnoBug(err), .BADF => |err| return errnoBug(err), // always a race condition .CONNABORTED => return error.ConnectionAborted, @@ -2343,6 +2395,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. switch (std.os.wasi.fd_read(fd, dest.ptr, dest.len, &n)) { .SUCCESS => return n, .INTR => continue, + .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), @@ -2364,6 +2417,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), .INTR => continue, + .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), @@ -2464,6 +2518,7 @@ fn netSendOne( return; }, .INTR => continue, + .CANCELED => return error.Canceled, .ACCES => return error.AccessDenied, .ALREADY => return error.FastOpenAlreadyInProgress, @@ -2531,13 +2586,15 @@ fn netSendMany( } return n; }, + .INTR => continue, + .CANCELED => return error.Canceled, + .AGAIN => |err| return errnoBug(err), .ALREADY => return error.FastOpenAlreadyInProgress, .BADF => |err| return errnoBug(err), // Always a race condition. .CONNRESET => return error.ConnectionResetByPeer, .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. - .INTR => continue, .INVAL => |err| return errnoBug(err), // Invalid argument passed. .ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified .MSGSIZE => return error.MessageOversize, @@ -2654,6 +2711,7 @@ fn netReceive( continue :recv; }, .INTR => continue, + .CANCELED => return .{ error.Canceled, message_i }, .FAULT => |err| return .{ errnoBug(err), message_i }, .INVAL => |err| return .{ errnoBug(err), message_i }, @@ -2662,6 +2720,7 @@ fn netReceive( } }, .INTR => continue, + .CANCELED => return .{ error.Canceled, message_i }, .BADF => |err| return .{ errnoBug(err), message_i }, .NFILE => return .{ error.SystemFdQuotaExceeded, message_i }, @@ -2780,6 +2839,8 @@ fn netInterfaceNameResolve( switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { .SUCCESS => return .{ .index = @bitCast(ifr.ifru.ivalue) }, .INTR => continue, + .CANCELED => return error.Canceled, + .INVAL => |err| return errnoBug(err), // Bad parameters. .NOTTY => |err| return errnoBug(err), .NXIO => |err| return errnoBug(err), @@ -2968,6 +3029,7 @@ fn netLookupFallible( .NONAME => return error.UnknownHostName, .SYSTEM => switch (posix.errno(-1)) { .INTR => continue, + .CANCELED => return error.Canceled, else => |e| return posix.unexpectedErrno(e), }, else => return error.Unexpected, @@ -3754,7 +3816,7 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { const status = c.__ulock_wake(flags, ptr, 0); if (status >= 0) return; switch (@as(c.E, @enumFromInt(-status))) { - .INTR => continue, // spurious wake() + .INTR, .CANCELED => continue, // spurious wake() .FAULT => assert(!is_debug), // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t .NOENT => return, // nothing was woken up .ALREADY => assert(!is_debug), // only for UL.Op.WAKE_THREAD From 143127529b109971c931cce18cc7a328f6031df3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 22:39:46 -0700 Subject: [PATCH 131/244] std.Io.Threaded: implement dirMake for WASI --- lib/std/Io.zig | 4 +++ lib/std/Io/Threaded.zig | 63 ++++++++++++++++++++++++++++++----------- lib/std/os.zig | 8 +++++- lib/std/posix.zig | 26 +---------------- lib/std/posix/test.zig | 58 ------------------------------------- 5 files changed, 59 insertions(+), 100 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index a20868afe2..2c0fd77dea 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -883,6 +883,10 @@ pub const Timestamp = struct { return .{ .nanoseconds = t.nanoseconds, .clock = clock }; } + pub fn fromNanoseconds(x: i96) Timestamp { + return .{ .nanoseconds = x }; + } + pub fn toSeconds(t: Timestamp) i64 { return @intCast(@divTrunc(t.nanoseconds, std.time.ns_per_s)); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 6a5eab2bdf..743cb845a9 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -167,7 +167,7 @@ pub fn io(t: *Threaded) Io { .dirMake = switch (builtin.os.tag) { .windows => @panic("TODO"), - .wasi => @panic("TODO"), + .wasi => dirMakeWasi, else => dirMakePosix, }, .dirStat = dirStat, @@ -906,6 +906,37 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: } } +fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + if (builtin.link_libc) return dirMakePosix(userdata, dir, sub_path, mode); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + while (true) { + try t.checkCancel(); + switch (std.os.wasi.path_create_directory(dir.handle, sub_path.ptr, sub_path.len)) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), + .PERM => return error.PermissionDenied, + .DQUOT => return error.DiskQuota, + .EXIST => return error.PathAlreadyExists, + .FAULT => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .MLINK => return error.LinkQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .ROFS => return error.ReadOnlyFileSystem, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -1005,13 +1036,13 @@ fn dirStatPathWasi( const t: *Threaded = @ptrCast(@alignCast(userdata)); const wasi = std.os.wasi; const flags: wasi.lookupflags_t = .{ - .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), + .SYMLINK_FOLLOW = options.follow_symlinks, }; var stat: wasi.filestat_t = undefined; while (true) { try t.checkCancel(); switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { - .SUCCESS => return statFromWasi(stat), + .SUCCESS => return statFromWasi(&stat), .INTR => continue, .CANCELED => return error.Canceled, @@ -1166,19 +1197,19 @@ fn dirAccessWasi( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, - options: Io.File.OpenFlags, -) Io.File.AccessError!void { + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { if (builtin.link_libc) return dirAccessPosix(userdata, dir, sub_path, options); const t: *Threaded = @ptrCast(@alignCast(userdata)); const wasi = std.os.wasi; const flags: wasi.lookupflags_t = .{ - .SYMLINK_FOLLOW = @intFromBool(options.follow_symlinks), + .SYMLINK_FOLLOW = options.follow_symlinks, }; - const stat = while (true) { - var stat: wasi.filestat_t = undefined; + var stat: wasi.filestat_t = undefined; + while (true) { try t.checkCancel(); switch (wasi.path_filestat_get(dir.handle, flags, sub_path.ptr, sub_path.len, &stat)) { - .SUCCESS => break statFromWasi(stat), + .SUCCESS => break, .INTR => continue, .CANCELED => return error.Canceled, @@ -1194,9 +1225,9 @@ fn dirAccessWasi( .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), } - }; + } - if (!options.mode.read and !options.mode.write and !options.mode.execute) + if (!options.read and !options.write and !options.execute) return; var directory: wasi.fdstat_t = undefined; @@ -1204,14 +1235,14 @@ fn dirAccessWasi( return error.AccessDenied; var rights: wasi.rights_t = .{}; - if (options.mode.read) { + if (options.read) { if (stat.filetype == .DIRECTORY) { rights.FD_READDIR = true; } else { rights.FD_READ = true; } } - if (options.mode.write) + if (options.write) rights.FD_WRITE = true; // No validation for execution. @@ -3262,9 +3293,9 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat { .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, else => .unknown, }, - .atime = st.atim, - .mtime = st.mtim, - .ctime = st.ctim, + .atime = .fromNanoseconds(st.atim), + .mtime = .fromNanoseconds(st.mtim), + .ctime = .fromNanoseconds(st.ctim), }; } diff --git a/lib/std/os.zig b/lib/std/os.zig index 7fe64290b1..7bee7ed104 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -201,7 +201,13 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. } } -pub fn fstat_wasi(fd: posix.fd_t) posix.FStatError!wasi.filestat_t { +pub const FstatError = error{ + SystemResources, + AccessDenied, + Unexpected, +}; + +pub fn fstat_wasi(fd: posix.fd_t) FstatError!wasi.filestat_t { var stat: wasi.filestat_t = undefined; switch (wasi.fd_filestat_get(fd, &stat)) { .SUCCESS => return stat, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 999fe5e50a..67a3e787f7 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -2809,37 +2809,13 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirErro const sub_dir_path_w = try windows.sliceToPrefixedFileW(dir_fd, sub_dir_path); return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); } else if (native_os == .wasi and !builtin.link_libc) { - return mkdiratWasi(dir_fd, sub_dir_path, mode); + @compileError("use std.Io instead"); } else { const sub_dir_path_c = try toPosixPath(sub_dir_path); return mkdiratZ(dir_fd, &sub_dir_path_c, mode); } } -pub fn mkdiratWasi(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirError!void { - _ = mode; - switch (wasi.path_create_directory(dir_fd, sub_dir_path.ptr, sub_dir_path.len)) { - .SUCCESS => return, - .ACCES => return error.AccessDenied, - .BADF => unreachable, - .PERM => return error.PermissionDenied, - .DQUOT => return error.DiskQuota, - .EXIST => return error.PathAlreadyExists, - .FAULT => unreachable, - .LOOP => return error.SymLinkLoop, - .MLINK => return error.LinkQuotaExceeded, - .NAMETOOLONG => return error.NameTooLong, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOSPC => return error.NoSpaceLeft, - .NOTDIR => return error.NotDir, - .ROFS => return error.ReadOnlyFileSystem, - .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.BadPathName, - else => |err| return unexpectedErrno(err), - } -} - /// Same as `mkdirat` except the parameters are null-terminated. pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void { if (native_os == .windows) { diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 230440a2f1..e85f1d7471 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -109,64 +109,6 @@ test "open smoke test" { } } -test "openat smoke test" { - if (native_os == .windows) return error.SkipZigTest; - - // TODO verify file attributes using `fstatat` - - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - var fd: posix.fd_t = undefined; - const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666; - - // Create some file using `openat`. - fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .CREAT = true, - .EXCL = true, - }), mode); - posix.close(fd); - - // Try this again with the same flags. This op should fail with error.PathAlreadyExists. - try expectError(error.PathAlreadyExists, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .CREAT = true, - .EXCL = true, - }), mode)); - - // Try opening without `EXCL` flag. - fd = try posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .CREAT = true, - }), mode); - posix.close(fd); - - // Try opening as a directory which should fail. - try expectError(error.NotDir, posix.openat(tmp.dir.fd, "some_file", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - .DIRECTORY = true, - }), mode)); - - // Create some directory - try posix.mkdirat(tmp.dir.fd, "some_dir", mode); - - // Open dir using `open` - fd = try posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{ - .ACCMODE = .RDONLY, - .DIRECTORY = true, - }), mode); - posix.close(fd); - - // Try opening as file which should fail (skip on wasi+libc due to - // https://github.com/bytecodealliance/wasmtime/issues/9054) - if (native_os != .wasi or !builtin.link_libc) { - try expectError(error.IsDir, posix.openat(tmp.dir.fd, "some_dir", CommonOpenFlags.lower(.{ - .ACCMODE = .RDWR, - }), mode)); - } -} - test "readlink on Windows" { if (native_os != .windows) return error.SkipZigTest; From d4215ffaa04b976400bd597cca0cca8182068bf6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 22:50:30 -0700 Subject: [PATCH 132/244] std.Io.Threaded: implement dirCreateFile for WASI --- lib/std/Io/Threaded.zig | 70 +++++++++++++++++++++++++- lib/std/fs/Dir.zig | 25 --------- lib/std/posix.zig | 109 +--------------------------------------- 3 files changed, 70 insertions(+), 134 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 743cb845a9..59f7d4270c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -190,7 +190,7 @@ pub fn io(t: *Threaded) Io { }, .dirCreateFile = switch (builtin.os.tag) { .windows => @panic("TODO"), - .wasi => @panic("TODO"), + .wasi => dirCreateFileWasi, else => dirCreateFilePosix, }, .dirOpenFile = dirOpenFile, @@ -1378,6 +1378,74 @@ fn dirCreateFilePosix( return .{ .handle = fd }; } +fn dirCreateFileWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + const lookup_flags: wasi.lookupflags_t = .{}; + const oflags: wasi.oflags_t = .{ + .CREAT = true, + .TRUNC = flags.truncate, + .EXCL = flags.exclusive, + }; + const fdflags: wasi.fdflags_t = .{}; + const base: wasi.rights_t = .{ + .FD_READ = flags.read, + .FD_WRITE = true, + .FD_DATASYNC = true, + .FD_SEEK = true, + .FD_TELL = true, + .FD_FDSTAT_SET_FLAGS = true, + .FD_SYNC = true, + .FD_ALLOCATE = true, + .FD_ADVISE = true, + .FD_FILESTAT_SET_TIMES = true, + .FD_FILESTAT_SET_SIZE = true, + .FD_FILESTAT_GET = true, + // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or + // FD_WRITE is also set. + .POLL_FD_READWRITE = true, + }; + const inheriting: wasi.rights_t = .{}; + var fd: posix.fd_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) { + .SUCCESS => return .{ .handle = fd }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + // Provides INVAL with a linux host on a bad path name, but NOENT on Windows + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), + .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, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn dirOpenFile( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index c7699a83cf..6da4bab684 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -938,31 +938,6 @@ pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File const path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); return self.createFileW(path_w.span(), flags); } - if (native_os == .wasi) { - return .{ - .handle = try posix.openatWasi(self.fd, sub_path, .{}, .{ - .CREAT = true, - .TRUNC = flags.truncate, - .EXCL = flags.exclusive, - }, .{}, .{ - .FD_READ = flags.read, - .FD_WRITE = true, - .FD_DATASYNC = true, - .FD_SEEK = true, - .FD_TELL = true, - .FD_FDSTAT_SET_FLAGS = true, - .FD_SYNC = true, - .FD_ALLOCATE = true, - .FD_ADVISE = true, - .FD_FILESTAT_SET_TIMES = true, - .FD_FILESTAT_SET_SIZE = true, - .FD_FILESTAT_GET = true, - // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or - // FD_WRITE is also set. - .POLL_FD_READWRITE = true, - }, .{}), - }; - } var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags); diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 67a3e787f7..9d58a4f643 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1610,119 +1610,12 @@ pub fn openat(dir_fd: fd_t, file_path: []const u8, flags: O, mode: mode_t) OpenE if (native_os == .windows) { @compileError("Windows does not support POSIX; use Windows-specific API or cross-platform std.fs API"); } else if (native_os == .wasi and !builtin.link_libc) { - // `mode` is ignored on WASI, which does not support unix-style file permissions - const opts = try openOptionsFromFlagsWasi(flags); - const fd = try openatWasi( - dir_fd, - file_path, - opts.lookup_flags, - opts.oflags, - opts.fs_flags, - opts.fs_rights_base, - opts.fs_rights_inheriting, - ); - errdefer close(fd); - - if (flags.write) { - const info = try std.os.fstat_wasi(fd); - if (info.filetype == .DIRECTORY) - return error.IsDir; - } - - return fd; + @compileError("use std.Io instead"); } const file_path_c = try toPosixPath(file_path); return openatZ(dir_fd, &file_path_c, flags, mode); } -/// Open and possibly create a file in WASI. -pub fn openatWasi( - dir_fd: fd_t, - file_path: []const u8, - lookup_flags: wasi.lookupflags_t, - oflags: wasi.oflags_t, - fdflags: wasi.fdflags_t, - base: wasi.rights_t, - inheriting: wasi.rights_t, -) OpenError!fd_t { - while (true) { - var fd: fd_t = undefined; - switch (wasi.path_open(dir_fd, lookup_flags, file_path.ptr, file_path.len, oflags, base, inheriting, fdflags, &fd)) { - .SUCCESS => return fd, - .INTR => continue, - - .FAULT => unreachable, - // Provides INVAL with a linux host on a bad path name, but NOENT on Windows - .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, - .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.BadPathName, - else => |err| return unexpectedErrno(err), - } - } -} - -/// A struct to contain all lookup/rights flags accepted by `wasi.path_open` -const WasiOpenOptions = struct { - oflags: wasi.oflags_t, - lookup_flags: wasi.lookupflags_t, - fs_rights_base: wasi.rights_t, - fs_rights_inheriting: wasi.rights_t, - fs_flags: wasi.fdflags_t, -}; - -/// Compute rights + flags corresponding to the provided POSIX access mode. -fn openOptionsFromFlagsWasi(oflag: O) OpenError!WasiOpenOptions { - const w = std.os.wasi; - - // Next, calculate the read/write rights to request, depending on the - // provided POSIX access mode - var rights: w.rights_t = .{}; - if (oflag.read) { - rights.FD_READ = true; - rights.FD_READDIR = true; - } - if (oflag.write) { - rights.FD_DATASYNC = true; - rights.FD_WRITE = true; - rights.FD_ALLOCATE = true; - rights.FD_FILESTAT_SET_SIZE = true; - } - - // https://github.com/ziglang/zig/issues/18882 - const flag_bits: u32 = @bitCast(oflag); - const oflags_int: u16 = @as(u12, @truncate(flag_bits >> 12)); - const fs_flags_int: u16 = @as(u12, @truncate(flag_bits)); - - return .{ - // https://github.com/ziglang/zig/issues/18882 - .oflags = @bitCast(oflags_int), - .lookup_flags = .{ - .SYMLINK_FOLLOW = !oflag.NOFOLLOW, - }, - .fs_rights_base = rights, - .fs_rights_inheriting = rights, - // https://github.com/ziglang/zig/issues/18882 - .fs_flags = @bitCast(fs_flags_int), - }; -} - /// Open and possibly create a file. Keeps trying if it gets interrupted. /// `file_path` is relative to the open directory handle `dir_fd`. /// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). From cf6fa219fd05b9f2c01e85557bcd140e72802459 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 16 Oct 2025 23:40:32 -0700 Subject: [PATCH 133/244] std.Io.Threaded: fix netWrite cancellation Move std.posix logic over rather than calling into it. --- lib/std/Io/Threaded.zig | 86 +++++++++++++++++++++++++++++------------ lib/std/Io/net.zig | 33 ++++++++++++---- 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 59f7d4270c..cdaf06a7c0 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -250,6 +250,19 @@ pub fn io(t: *Threaded) Io { }; } +const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 +const have_accept4 = !socket_flags_unsupported; +const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); +const have_networking = builtin.os.tag != .wasi; +const have_flock = @TypeOf(posix.system.flock) != void; +const have_sendmmsg = builtin.os.tag == .linux; + +const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; +const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; +const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; +const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; +const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; + /// Trailing data: /// 1. context /// 2. result @@ -1143,13 +1156,6 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. } } -const have_flock = @TypeOf(posix.system.flock) != void; -const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; -const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; -const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; -const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; -const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; - fn dirAccessPosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -1277,8 +1283,7 @@ fn dirCreateFilePosix( // 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) { + if (have_flock_open_flags) switch (flags.lock) { .none => {}, .shared => { os_flags.SHLOCK = true; @@ -1328,7 +1333,7 @@ fn dirCreateFilePosix( }; errdefer posix.close(fd); - if (have_flock and !has_flock_open_flags and flags.lock != .none) { + if (have_flock and !have_flock_open_flags and flags.lock != .none) { const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; const lock_flags = switch (flags.lock) { .none => unreachable, @@ -1352,7 +1357,7 @@ fn dirCreateFilePosix( } } - if (has_flock_open_flags and flags.lock_nonblocking) { + if (have_flock_open_flags and flags.lock_nonblocking) { var fl_flags: usize = while (true) { try t.checkCancel(); const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); @@ -1476,8 +1481,7 @@ fn dirOpenFile( // 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) { + if (have_flock_open_flags) { // Note that the NONBLOCK flag is removed after the openat() call // is successful. switch (flags.lock) { @@ -1530,7 +1534,7 @@ fn dirOpenFile( }; errdefer posix.close(fd); - if (have_flock and !has_flock_open_flags and flags.lock != .none) { + if (have_flock and !have_flock_open_flags and flags.lock != .none) { const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; const lock_flags = switch (flags.lock) { .none => unreachable, @@ -1554,7 +1558,7 @@ fn dirOpenFile( } } - if (has_flock_open_flags and flags.lock_nonblocking) { + if (have_flock_open_flags and flags.lock_nonblocking) { var fl_flags: usize = while (true) { try t.checkCancel(); const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); @@ -1954,7 +1958,7 @@ fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { var ns: std.os.wasi.timestamp_t = undefined; const err = std.os.wasi.clock_time_get(clockToWasi(clock), 1, &ns); if (err != .SUCCESS) return error.Unexpected; - return ns; + return .fromNanoseconds(ns); } fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { @@ -2004,7 +2008,7 @@ fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t.io())) |d| .{ .id = clockToWasi(d.clock), - .timeout = std.math.lossyCast(u64, d.duration.nanoseconds), + .timeout = std.math.lossyCast(u64, d.raw.nanoseconds), .precision = 0, .flags = 0, } else .{ @@ -2083,6 +2087,7 @@ fn netListenIpPosix( address: IpAddress, options: IpAddress.ListenOptions, ) IpAddress.ListenError!net.Server { + if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(&address); const socket_fd = try openSocketPosix(t, family, .{ @@ -2230,6 +2235,7 @@ fn posixConnect(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sock .ACCES => return error.AccessDenied, .PERM => |err| return errnoBug(err), .NOENT => |err| return errnoBug(err), + .NETDOWN => return error.NetworkDown, else => |err| return posix.unexpectedErrno(err), } } @@ -2306,6 +2312,7 @@ fn netConnectIpPosix( address: *const IpAddress, options: IpAddress.ConnectOptions, ) IpAddress.ConnectError!net.Stream { + if (!have_networking) return error.NetworkDown; if (options.timeout != .none) @panic("TODO"); const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); @@ -2346,6 +2353,7 @@ fn netBindIpPosix( address: *const IpAddress, options: IpAddress.BindOptions, ) IpAddress.BindError!net.Socket { + if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); const socket_fd = try openSocketPosix(t, family, options); @@ -2421,9 +2429,6 @@ fn openSocketPosix( return socket_fd; } -const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 -const have_accept4 = !socket_flags_unsupported; - fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream { const t: *Threaded = @ptrCast(@alignCast(userdata)); var storage: PosixAddress = undefined; @@ -2534,14 +2539,13 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. } } -const have_sendmmsg = builtin.os.tag == .linux; - fn netSend( userdata: ?*anyopaque, handle: net.Socket.Handle, messages: []net.OutgoingMessage, flags: net.SendFlags, ) struct { ?net.Socket.SendError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; const t: *Threaded = @ptrCast(@alignCast(userdata)); const posix_flags: u32 = @@ -2703,7 +2707,7 @@ fn netSendMany( .OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type. .PIPE => return error.SocketUnconnected, .AFNOSUPPORT => return error.AddressFamilyUnsupported, - .HOSTUNREACH => return error.NetworkUnreachable, + .HOSTUNREACH => return error.HostUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTCONN => return error.SocketUnconnected, .NETDOWN => return error.NetworkDown, @@ -2720,6 +2724,7 @@ fn netReceive( flags: net.ReceiveFlags, timeout: Io.Timeout, ) struct { ?net.Socket.ReceiveTimeoutError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; const t: *Threaded = @ptrCast(@alignCast(userdata)); // recvmmsg is useless, here's why: @@ -2847,8 +2852,8 @@ fn netWritePosix( data: []const []const u8, splat: usize, ) net.Stream.Writer.Error!usize { + if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); var iovecs: [max_iovecs_len]posix.iovec_const = undefined; var msg: posix.msghdr_const = .{ @@ -2889,7 +2894,37 @@ fn netWritePosix( }, }; const flags = posix.MSG.NOSIGNAL; - return posix.sendmsg(fd, &msg, flags); + while (true) { + try t.checkCancel(); + const rc = posix.system.sendmsg(fd, &msg, flags); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => |err| return errnoBug(err), + .AGAIN => |err| return errnoBug(err), + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // always a race condition + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. + .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. + .INVAL => |err| return errnoBug(err), // Invalid argument passed. + .ISCONN => |err| return errnoBug(err), // connection-mode socket was connected already but a recipient was specified + .MSGSIZE => |err| return errnoBug(err), + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), // The file descriptor sockfd does not refer to a socket. + .OPNOTSUPP => |err| return errnoBug(err), // Some bit in the flags argument is inappropriate for the socket type. + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } } fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { @@ -2913,6 +2948,7 @@ fn netInterfaceNameResolve( userdata: ?*anyopaque, name: *const net.Interface.Name, ) net.Interface.Name.ResolveError!net.Interface { + if (!have_networking) return error.InterfaceNotFound; const t: *Threaded = @ptrCast(@alignCast(userdata)); if (native_os == .linux) { diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 1f47d7e1f5..ca18325e2a 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -309,6 +309,7 @@ pub const IpAddress = union(enum) { AccessDenied, /// Non-blocking was requested and the operation cannot return immediately. WouldBlock, + NetworkDown, } || Io.Timeout.Error || Io.UnexpectedError || Io.Cancelable; pub const ConnectOptions = struct { @@ -1062,7 +1063,7 @@ pub const Socket = struct { AddressFamilyUnsupported, /// Another TCP Fast Open is already in progress. FastOpenAlreadyInProgress, - /// Network connection was unexpectedly closed by recipient. + /// Network session was unexpectedly closed by recipient. ConnectionResetByPeer, /// Local end has been shut down on a connection-oriented socket, or /// the socket was never connected. @@ -1242,15 +1243,33 @@ pub const Stream = struct { stream: Stream, err: ?Error = null, - pub const Error = std.posix.SendMsgError || error{ + pub const Error = error{ + /// Another TCP Fast Open is already in progress. + FastOpenAlreadyInProgress, + /// Network session was unexpectedly closed by recipient. ConnectionResetByPeer, - SocketNotBound, - MessageOversize, - NetworkDown, + /// The output queue for a network interface was full. This generally indicates that the + /// interface has stopped sending, but may be caused by transient congestion. (Normally, + /// this does not occur in Linux. Packets are just silently dropped when a device queue + /// overflows.) + /// + /// This is also caused when there is not enough kernel memory available. SystemResources, + /// No route to network. + NetworkUnreachable, + /// Network reached but no route to host. + HostUnreachable, + /// The local network interface used to reach the destination is down. + NetworkDown, + /// The destination address is not listening. + ConnectionRefused, + /// The passed address didn't have the correct address family in its sa_family field. + AddressFamilyUnsupported, + /// Local end has been shut down on a connection-oriented socket, or + /// the socket was never connected. SocketUnconnected, - Unexpected, - } || Io.Cancelable; + SocketNotBound, + } || Io.UnexpectedError || Io.Cancelable; pub fn init(stream: Stream, io: Io, buffer: []u8) Writer { return .{ From e87ceb76c242cf991f85c9aa6619f5faf4059af3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 17 Oct 2025 00:09:25 -0700 Subject: [PATCH 134/244] std.Io.net.Server: refine AcceptError set --- lib/std/Io/Threaded.zig | 76 +++++++++++++++++++++-------------------- lib/std/Io/net.zig | 21 +++++++++++- lib/std/posix.zig | 39 +-------------------- 3 files changed, 60 insertions(+), 76 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index cdaf06a7c0..1d137ebe85 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -898,7 +898,7 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: .CANCELED => return error.Canceled, .ACCES => return error.AccessDenied, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .PERM => return error.PermissionDenied, .DQUOT => return error.DiskQuota, .EXIST => return error.PathAlreadyExists, @@ -930,7 +930,7 @@ fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: I .CANCELED => return error.Canceled, .ACCES => return error.AccessDenied, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .PERM => return error.PermissionDenied, .DQUOT => return error.DiskQuota, .EXIST => return error.PathAlreadyExists, @@ -989,7 +989,7 @@ fn dirStatPathLinux( .CANCELED => return error.Canceled, .ACCES => return error.AccessDenied, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .FAULT => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), .LOOP => return error.SymLinkLoop, @@ -1024,7 +1024,7 @@ fn dirStatPathPosix( .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, @@ -1060,7 +1060,7 @@ fn dirStatPathWasi( .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, .FAULT => |err| return errnoBug(err), @@ -1088,7 +1088,7 @@ fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, else => |err| return posix.unexpectedErrno(err), @@ -1115,7 +1115,7 @@ fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File .CANCELED => return error.Canceled, .ACCES => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .FAULT => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), .LOOP => |err| return errnoBug(err), @@ -1147,7 +1147,7 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, .NOTCAPABLE => return error.AccessDenied, @@ -1220,7 +1220,7 @@ fn dirAccessWasi( .CANCELED => return error.Canceled, .INVAL => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOMEM => return error.SystemResources, .ACCES => return error.AccessDenied, .FAULT => |err| return errnoBug(err), @@ -1305,7 +1305,7 @@ fn dirCreateFilePosix( .FAULT => |err| return errnoBug(err), .INVAL => return error.BadPathName, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .ACCES => return error.AccessDenied, .FBIG => return error.FileTooBig, .OVERFLOW => return error.FileTooBig, @@ -1347,7 +1347,7 @@ fn dirCreateFilePosix( .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => |err| return errnoBug(err), // invalid parameters .NOLCK => return error.SystemResources, .AGAIN => return error.WouldBlock, @@ -1427,7 +1427,7 @@ fn dirCreateFileWasi( .FAULT => |err| return errnoBug(err), // Provides INVAL with a linux host on a bad path name, but NOENT on Windows .INVAL => return error.BadPathName, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .ACCES => return error.AccessDenied, .FBIG => return error.FileTooBig, .OVERFLOW => return error.FileTooBig, @@ -1506,7 +1506,7 @@ fn dirOpenFile( .FAULT => |err| return errnoBug(err), .INVAL => return error.BadPathName, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .ACCES => return error.AccessDenied, .FBIG => return error.FileTooBig, .OVERFLOW => return error.FileTooBig, @@ -1548,7 +1548,7 @@ fn dirOpenFile( .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => |err| return errnoBug(err), // invalid parameters .NOLCK => return error.SystemResources, .AGAIN => return error.WouldBlock, @@ -1653,7 +1653,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -1678,7 +1678,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, - .BADF => return error.NotOpenForReading, // can be a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -1779,7 +1779,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), - .BADF => return error.NotOpenForReading, // can be a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -1807,7 +1807,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, - .BADF => return error.NotOpenForReading, // can be a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -1844,7 +1844,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => return error.Unseekable, .OVERFLOW => return error.Unseekable, .SPIPE => return error.Unseekable, @@ -1866,7 +1866,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => return error.Unseekable, .OVERFLOW => return error.Unseekable, .SPIPE => return error.Unseekable, @@ -1885,7 +1885,7 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => return error.Unseekable, .OVERFLOW => return error.Unseekable, .SPIPE => return error.Unseekable, @@ -2111,7 +2111,7 @@ fn netListenIpPosix( switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { .SUCCESS => break, .ADDRINUSE => return error.AddressInUse, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. else => |err| return posix.unexpectedErrno(err), } } @@ -2150,7 +2150,7 @@ fn netListenUnix( switch (posix.errno(posix.system.listen(socket_fd, options.kernel_backlog))) { .SUCCESS => break, .ADDRINUSE => return error.AddressInUse, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. else => |err| return posix.unexpectedErrno(err), } } @@ -2178,7 +2178,7 @@ fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, .ROFS => return error.ReadOnlyFileSystem, .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), // always a race condition if this error is returned + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => |err| return errnoBug(err), // invalid parameters .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` .FAULT => |err| return errnoBug(err), // invalid `addr` pointer @@ -2197,7 +2197,7 @@ fn posixBind(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sockadd .CANCELED => return error.Canceled, .ADDRINUSE => return error.AddressInUse, - .BADF => |err| return errnoBug(err), // always a race condition if this error is returned + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .INVAL => |err| return errnoBug(err), // invalid parameters .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` .AFNOSUPPORT => return error.AddressFamilyUnsupported, @@ -2221,7 +2221,7 @@ fn posixConnect(t: *Threaded, socket_fd: posix.socket_t, addr: *const posix.sock .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, .ALREADY => return error.ConnectionPending, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .CONNREFUSED => return error.ConnectionRefused, .CONNRESET => return error.ConnectionResetByPeer, .FAULT => |err| return errnoBug(err), @@ -2260,7 +2260,7 @@ fn posixConnectUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockadd .ROFS => return error.ReadOnlyFileSystem, .PERM => return error.PermissionDenied, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .CONNABORTED => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .ISCONN => |err| return errnoBug(err), @@ -2279,7 +2279,7 @@ fn posixGetSockName(t: *Threaded, socket_fd: posix.fd_t, addr: *posix.sockaddr, .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), // always a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .FAULT => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), // invalid parameters .NOTSOCK => |err| return errnoBug(err), // always a race condition @@ -2298,7 +2298,7 @@ fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, opti .INTR => continue, .CANCELED => return error.Canceled, - .BADF => |err| return errnoBug(err), // always a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOTSOCK => |err| return errnoBug(err), // always a race condition .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), @@ -2430,6 +2430,7 @@ fn openSocketPosix( } fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream { + if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); var storage: PosixAddress = undefined; var addr_len: posix.socklen_t = @sizeOf(PosixAddress); @@ -2456,11 +2457,12 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Serve }, .INTR => continue, .CANCELED => return error.Canceled, + .AGAIN => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // always a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .CONNABORTED => return error.ConnectionAborted, .FAULT => |err| return errnoBug(err), - .INVAL => return error.SocketNotListening, + .INVAL => |err| return errnoBug(err), .NOTSOCK => |err| return errnoBug(err), .MFILE => return error.ProcessFdQuotaExceeded, .NFILE => return error.SystemFdQuotaExceeded, @@ -2504,7 +2506,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, @@ -2526,7 +2528,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .NOBUFS => return error.SystemResources, .NOMEM => return error.SystemResources, .NOTCONN => return error.SocketUnconnected, @@ -2625,7 +2627,7 @@ fn netSendOne( .ACCES => return error.AccessDenied, .ALREADY => return error.FastOpenAlreadyInProgress, - .BADF => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .CONNRESET => return error.ConnectionResetByPeer, .DESTADDRREQ => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), @@ -2694,7 +2696,7 @@ fn netSendMany( .AGAIN => |err| return errnoBug(err), .ALREADY => return error.FastOpenAlreadyInProgress, - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .CONNRESET => return error.ConnectionResetByPeer, .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. @@ -2905,7 +2907,7 @@ fn netWritePosix( .ACCES => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), .ALREADY => return error.FastOpenAlreadyInProgress, - .BADF => |err| return errnoBug(err), // always a race condition + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .CONNRESET => return error.ConnectionResetByPeer, .DESTADDRREQ => |err| return errnoBug(err), // The socket is not connection-mode, and no peer address is set. .FAULT => |err| return errnoBug(err), // An invalid user space address was specified for an argument. @@ -2979,7 +2981,7 @@ fn netInterfaceNameResolve( .INVAL => |err| return errnoBug(err), // Bad parameters. .NOTTY => |err| return errnoBug(err), .NXIO => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // Always a race condition. + .BADF => |err| return errnoBug(err), // File descriptor used after closed. .FAULT => |err| return errnoBug(err), // Bad pointer parameter. .IO => |err| return errnoBug(err), // sock_fd is not a file descriptor .NODEV => return error.InterfaceNotFound, diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index ca18325e2a..17d82451c5 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1312,7 +1312,26 @@ pub const Server = struct { s.* = undefined; } - pub const AcceptError = std.posix.AcceptError || Io.Cancelable; + pub const AcceptError = error{ + /// The per-process limit on the number of open file descriptors has been reached. + ProcessFdQuotaExceeded, + /// The system-wide limit on the total number of open files has been reached. + SystemFdQuotaExceeded, + /// Not enough free memory. This often means that the memory allocation is limited + /// by the socket buffer limits, not by the system memory. + SystemResources, + /// The network subsystem has failed. + NetworkDown, + /// No connection is already queued and ready to be accepted, and + /// the socket is configured as non-blocking. + WouldBlock, + /// An incoming connection was indicated, but was subsequently terminated by the + /// remote peer prior to accepting the call. + ConnectionAborted, + /// Firewall rules forbid connection. + BlockedByFirewall, + ProtocolFailure, + } || Io.UnexpectedError || Io.Cancelable; /// Blocks until a client connects to the server. pub fn accept(s: *Server, io: Io) AcceptError!Stream { diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 9d58a4f643..358715a61f 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3622,44 +3622,7 @@ pub fn listen(sock: socket_t, backlog: u31) ListenError!void { } } -pub const AcceptError = error{ - ConnectionAborted, - - /// The file descriptor sockfd does not refer to a socket. - FileDescriptorNotASocket, - - /// The per-process limit on the number of open file descriptors has been reached. - ProcessFdQuotaExceeded, - - /// The system-wide limit on the total number of open files has been reached. - SystemFdQuotaExceeded, - - /// Not enough free memory. This often means that the memory allocation is limited - /// by the socket buffer limits, not by the system memory. - SystemResources, - - /// Socket is not listening for new connections. - SocketNotListening, - - ProtocolFailure, - - /// Firewall rules forbid connection. - BlockedByFirewall, - - /// This error occurs when no global event loop is configured, - /// and accepting from the socket would block. - WouldBlock, - - /// An incoming connection was indicated, but was subsequently terminated by the - /// remote peer prior to accepting the call. - ConnectionResetByPeer, - - /// The network subsystem has failed. - NetworkDown, - - /// The referenced socket is not a type that supports connection-oriented service. - OperationNotSupported, -} || UnexpectedError; +pub const AcceptError = std.Io.net.Server.AcceptError; /// Accept a connection on a socket. /// If `sockfd` is opened in non blocking mode, the function will From da6b959f647f62aeaf96f380ad2828c16142f23f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 17 Oct 2025 00:31:04 -0700 Subject: [PATCH 135/244] std.Io.Threaded: implement dirOpenFile for WASI --- lib/std/Io/Threaded.zig | 78 +++++++++++++++++++++++++++++++++++++++-- lib/std/fs/Dir.zig | 27 -------------- lib/std/fs/test.zig | 4 +-- 3 files changed, 77 insertions(+), 32 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 1d137ebe85..4f2582885b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -193,7 +193,11 @@ pub fn io(t: *Threaded) Io { .wasi => dirCreateFileWasi, else => dirCreateFilePosix, }, - .dirOpenFile = dirOpenFile, + .dirOpenFile = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => dirOpenFileWasi, + else => dirOpenFilePosix, + }, .fileClose = fileClose, .fileWriteStreaming = fileWriteStreaming, .fileWritePositional = fileWritePositional, @@ -1451,7 +1455,7 @@ fn dirCreateFileWasi( } } -fn dirOpenFile( +fn dirOpenFilePosix( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, @@ -1584,6 +1588,74 @@ fn dirOpenFile( return .{ .handle = fd }; } +fn dirOpenFileWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + if (builtin.link_libc) return dirOpenFilePosix(userdata, dir, sub_path, flags); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + var base: std.os.wasi.rights_t = .{}; + // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or FD_WRITE + // is also set. + if (flags.isRead()) { + base.FD_READ = true; + base.FD_TELL = true; + base.FD_SEEK = true; + base.FD_FILESTAT_GET = true; + base.POLL_FD_READWRITE = true; + } + if (flags.isWrite()) { + base.FD_WRITE = true; + base.FD_TELL = true; + base.FD_SEEK = true; + base.FD_DATASYNC = true; + base.FD_FDSTAT_SET_FLAGS = true; + base.FD_SYNC = true; + base.FD_ALLOCATE = true; + base.FD_ADVISE = true; + base.FD_FILESTAT_SET_TIMES = true; + base.FD_FILESTAT_SET_SIZE = true; + base.POLL_FD_READWRITE = true; + } + const lookup_flags: wasi.lookupflags_t = .{}; + const oflags: wasi.oflags_t = .{}; + const inheriting: wasi.rights_t = .{}; + const fdflags: wasi.fdflags_t = .{}; + var fd: posix.fd_t = undefined; + while (true) { + try t.checkCancel(); + switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, inheriting, fdflags, &fd)) { + .SUCCESS => return .{ .handle = fd }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + .NOTCAPABLE => return error.AccessDenied, + .NAMETOOLONG => return error.NameTooLong, + .INVAL => return error.BadPathName, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn fileClose(userdata: ?*anyopaque, file: Io.File) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -3319,7 +3391,7 @@ fn clockToPosix(clock: Io.Clock) posix.clockid_t { fn clockToWasi(clock: Io.Clock) std.os.wasi.clockid_t { return switch (clock) { - .realtime => .REALTIME, + .real => .REALTIME, .awake => .MONOTONIC, .boot => .MONOTONIC, .cpu_process => .PROCESS_CPUTIME_ID, diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 6da4bab684..615c63efa6 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -858,33 +858,6 @@ pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.Ope const path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); return self.openFileW(path_w.span(), flags); } - if (native_os == .wasi and !builtin.link_libc) { - var base: std.os.wasi.rights_t = .{}; - // POLL_FD_READWRITE only grants extra rights if the corresponding FD_READ and/or FD_WRITE - // is also set. - if (flags.isRead()) { - base.FD_READ = true; - base.FD_TELL = true; - base.FD_SEEK = true; - base.FD_FILESTAT_GET = true; - base.POLL_FD_READWRITE = true; - } - if (flags.isWrite()) { - base.FD_WRITE = true; - base.FD_TELL = true; - base.FD_SEEK = true; - base.FD_DATASYNC = true; - base.FD_FDSTAT_SET_FLAGS = true; - base.FD_SYNC = true; - base.FD_ALLOCATE = true; - base.FD_ADVISE = true; - base.FD_FILESTAT_SET_TIMES = true; - base.FD_FILESTAT_SET_SIZE = true; - base.POLL_FD_READWRITE = true; - } - const fd = try posix.openatWasi(self.fd, sub_path, .{}, .{}, .{}, base, .{}); - return .{ .handle = fd }; - } var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags)); diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 052afc3a02..a20be08fcb 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1407,7 +1407,7 @@ test "setEndPos" { try f.setEndPos(initial_size); try testing.expectEqual(initial_size, try f.getEndPos()); try reader.seekTo(0); - try testing.expectEqual(initial_size, reader.interface.readSliceShort(&buffer)); + try testing.expectEqual(initial_size, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes", buffer[0..@intCast(initial_size)]); } @@ -1416,7 +1416,7 @@ test "setEndPos" { try f.setEndPos(larger); try testing.expectEqual(larger, try f.getEndPos()); try reader.seekTo(0); - try testing.expectEqual(larger, reader.interface.readSliceShort(&buffer)); + try testing.expectEqual(larger, try reader.interface.readSliceShort(&buffer)); try testing.expectEqualStrings("ninebytes\x00\x00\x00\x00", buffer[0..@intCast(larger)]); } From 81e7e9fdbbb822c413649479dd572ffcd244543a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 17 Oct 2025 00:52:33 -0700 Subject: [PATCH 136/244] std.Io: add dirOpenDir and WASI impl --- lib/std/Io.zig | 1 + lib/std/Io/Dir.zig | 24 +++++++++++ lib/std/Io/Threaded.zig | 87 +++++++++++++++++++++++++++++++++++++++- lib/std/fs/Dir.zig | 89 ++++++----------------------------------- lib/std/tar.zig | 10 ++--- 5 files changed, 129 insertions(+), 82 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 2c0fd77dea..859568a15a 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -666,6 +666,7 @@ pub const VTable = struct { dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void, dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, + dirOpenDir: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.OpenError!Dir, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, fileClose: *const fn (?*anyopaque, File) void, fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 634c1f9fff..45833dd7f7 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -69,6 +69,30 @@ pub const OpenError = error{ NetworkNotFound, } || PathNameError || Io.Cancelable || Io.UnexpectedError; +pub const OpenOptions = struct { + /// `true` means the opened directory can be used as the `Dir` parameter + /// for functions which operate based on an open directory handle. When `false`, + /// such operations are Illegal Behavior. + access_sub_paths: bool = true, + /// `true` means the opened directory can be scanned for the files and sub-directories + /// of the result. It means the `iterate` function can be called. + iterate: bool = false, + /// `false` means it won't dereference the symlinks. + follow_symlinks: bool = true, +}; + +/// Opens a directory at the given path. The directory is a system resource that remains +/// open until `close` is called on the result. +/// +/// The directory cannot be iterated unless the `iterate` option is set to `true`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) OpenError!Dir { + return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options); +} + pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 4f2582885b..7f138a25df 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -198,6 +198,11 @@ pub fn io(t: *Threaded) Io { .wasi => dirOpenFileWasi, else => dirOpenFilePosix, }, + .dirOpenDir = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => dirOpenDirWasi, + else => dirOpenDirPosix, + }, .fileClose = fileClose, .fileWriteStreaming = fileWriteStreaming, .fileWritePositional = fileWritePositional, @@ -1429,7 +1434,6 @@ fn dirCreateFileWasi( .CANCELED => return error.Canceled, .FAULT => |err| return errnoBug(err), - // Provides INVAL with a linux host on a bad path name, but NOENT on Windows .INVAL => return error.BadPathName, .BADF => |err| return errnoBug(err), // File descriptor used after closed. .ACCES => return error.AccessDenied, @@ -1656,6 +1660,87 @@ fn dirOpenFileWasi( } } +fn dirOpenDirPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + _ = t; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} + +fn dirOpenDirWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + if (builtin.link_libc) return dirOpenDirPosix(userdata, dir, sub_path, options); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const wasi = std.os.wasi; + + var base: std.os.wasi.rights_t = .{ + .FD_FILESTAT_GET = true, + .FD_FDSTAT_SET_FLAGS = true, + .FD_FILESTAT_SET_TIMES = true, + }; + if (options.access_sub_paths) { + base.FD_READDIR = true; + base.PATH_CREATE_DIRECTORY = true; + base.PATH_CREATE_FILE = true; + base.PATH_LINK_SOURCE = true; + base.PATH_LINK_TARGET = true; + base.PATH_OPEN = true; + base.PATH_READLINK = true; + base.PATH_RENAME_SOURCE = true; + base.PATH_RENAME_TARGET = true; + base.PATH_FILESTAT_GET = true; + base.PATH_FILESTAT_SET_SIZE = true; + base.PATH_FILESTAT_SET_TIMES = true; + base.PATH_SYMLINK = true; + base.PATH_REMOVE_DIRECTORY = true; + base.PATH_UNLINK_FILE = true; + } + + const lookup_flags: wasi.lookupflags_t = .{ .SYMLINK_FOLLOW = options.follow_symlinks }; + const oflags: wasi.oflags_t = .{ .DIRECTORY = true }; + const fdflags: wasi.fdflags_t = .{}; + var fd: posix.fd_t = undefined; + + while (true) { + try t.checkCancel(); + switch (wasi.path_open(dir.handle, lookup_flags, sub_path.ptr, sub_path.len, oflags, base, base, fdflags, &fd)) { + .SUCCESS => return .{ .handle = fd }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .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, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + .NOTCAPABLE => return error.AccessDenied, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn fileClose(userdata: ?*anyopaque, file: Io.File) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 615c63efa6..5714d98e39 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1235,28 +1235,10 @@ pub fn setAsCwd(self: Dir) !void { try posix.fchdir(self.fd); } -pub const OpenOptions = struct { - /// `true` means the opened directory can be used as the `Dir` parameter - /// for functions which operate based on an open directory handle. When `false`, - /// such operations are Illegal Behavior. - access_sub_paths: bool = true, +/// Deprecated in favor of `Io.Dir.OpenOptions`. +pub const OpenOptions = Io.Dir.OpenOptions; - /// `true` means the opened directory can be scanned for the files and sub-directories - /// of the result. It means the `iterate` function can be called. - iterate: bool = false, - - /// `true` means it won't dereference the symlinks. - no_follow: bool = false, -}; - -/// Opens a directory at the given path. The directory is a system resource that remains -/// open until `close` is called on the result. -/// The directory cannot be iterated unless the `iterate` option is set to `true`. -/// -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -/// Asserts that the path parameter has no null bytes. +/// Deprecated in favor of `Io.Dir.openDir`. pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir { switch (native_os) { .windows => { @@ -1264,54 +1246,9 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir return self.openDirW(sub_path_w.span().ptr, args); }, .wasi => if (!builtin.link_libc) { - var base: std.os.wasi.rights_t = .{ - .FD_FILESTAT_GET = true, - .FD_FDSTAT_SET_FLAGS = true, - .FD_FILESTAT_SET_TIMES = true, - }; - if (args.access_sub_paths) { - base.FD_READDIR = true; - base.PATH_CREATE_DIRECTORY = true; - base.PATH_CREATE_FILE = true; - base.PATH_LINK_SOURCE = true; - base.PATH_LINK_TARGET = true; - base.PATH_OPEN = true; - base.PATH_READLINK = true; - base.PATH_RENAME_SOURCE = true; - base.PATH_RENAME_TARGET = true; - base.PATH_FILESTAT_GET = true; - base.PATH_FILESTAT_SET_SIZE = true; - base.PATH_FILESTAT_SET_TIMES = true; - base.PATH_SYMLINK = true; - base.PATH_REMOVE_DIRECTORY = true; - base.PATH_UNLINK_FILE = true; - } - - const result = posix.openatWasi( - self.fd, - sub_path, - .{ .SYMLINK_FOLLOW = !args.no_follow }, - .{ .DIRECTORY = true }, - .{}, - base, - base, - ); - const fd = result catch |err| switch (err) { - error.FileTooBig => unreachable, // can't happen for directories - error.IsDir => unreachable, // we're setting DIRECTORY - error.NoSpaceLeft => unreachable, // not setting CREAT - error.PathAlreadyExists => unreachable, // not setting CREAT - error.FileLocksNotSupported => unreachable, // locking folders is not supported - error.WouldBlock => unreachable, // can't happen for directories - error.FileBusy => unreachable, // can't happen for directories - error.SharingViolation => unreachable, - error.PipeBusy => unreachable, - error.ProcessNotFound => unreachable, - error.AntivirusInterference => unreachable, - - else => |e| return e, - }; - return .{ .fd = fd }; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); }, else => {}, } @@ -1358,12 +1295,12 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr var symlink_flags: posix.O = switch (native_os) { .wasi => .{ .read = true, - .NOFOLLOW = args.no_follow, + .NOFOLLOW = !args.follow_symlinks, .DIRECTORY = true, }, else => .{ .ACCMODE = .RDONLY, - .NOFOLLOW = args.no_follow, + .NOFOLLOW = !args.follow_symlinks, .DIRECTORY = true, .CLOEXEC = true, }, @@ -1384,7 +1321,7 @@ pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenEr w.SYNCHRONIZE | w.FILE_TRAVERSE; const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ - .no_follow = args.no_follow, + .no_follow = !args.follow_symlinks, .create_disposition = w.FILE_OPEN, }) catch |err| switch (err) { error.ReadOnlyFileSystem => unreachable, @@ -1923,7 +1860,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { if (treat_as_dir) { if (stack.unusedCapacitySlice().len >= 1) { var iterable_dir = top.iter.dir.openDir(entry.name, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2019,7 +1956,7 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { handle_entry: while (true) { if (treat_as_dir) { break :iterable_dir parent_dir.openDir(name, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2125,7 +2062,7 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint handle_entry: while (true) { if (treat_as_dir) { const new_dir = dir.openDir(entry.name, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { @@ -2224,7 +2161,7 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File handle_entry: while (true) { if (treat_as_dir) { break :iterable_dir self.openDir(sub_path, .{ - .no_follow = true, + .follow_symlinks = false, .iterate = true, }) catch |err| switch (err) { error.NotDir => { diff --git a/lib/std/tar.zig b/lib/std/tar.zig index e397677cf3..12f9c837a2 100644 --- a/lib/std/tar.zig +++ b/lib/std/tar.zig @@ -977,7 +977,7 @@ test pipeToFileSystem { const data = @embedFile("tar/testdata/example.tar"); var reader: std.Io.Reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); const dir = tmp.dir; @@ -1010,7 +1010,7 @@ test "pipeToFileSystem root_dir" { // with strip_components = 1 { - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; defer diagnostics.deinit(); @@ -1032,7 +1032,7 @@ test "pipeToFileSystem root_dir" { // with strip_components = 0 { reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; defer diagnostics.deinit(); @@ -1084,7 +1084,7 @@ test "pipeToFileSystem strip_components" { const data = @embedFile("tar/testdata/example.tar"); var reader: std.Io.Reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); defer tmp.cleanup(); var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; defer diagnostics.deinit(); @@ -1145,7 +1145,7 @@ test "executable bit" { for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| { var reader: std.Io.Reader = .fixed(data); - var tmp = testing.tmpDir(.{ .no_follow = true }); + var tmp = testing.tmpDir(.{ .follow_symlinks = false }); //defer tmp.cleanup(); pipeToFileSystem(tmp.dir, &reader, .{ From 752d38612f97d7c9aa203615bdde772a71b1330f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 17 Oct 2025 01:14:16 -0700 Subject: [PATCH 137/244] std.Io.Threaded: fix -fsingle-threaded build --- lib/std/Io/Threaded.zig | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 7f138a25df..b1ef09e34d 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -265,6 +265,10 @@ const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); const have_networking = builtin.os.tag != .wasi; const have_flock = @TypeOf(posix.system.flock) != void; const have_sendmmsg = builtin.os.tag == .linux; +const have_futex = switch (builtin.cpu.arch) { + .wasm32, .wasm64 => builtin.cpu.has(.wasm, .atomics), + else => true, +}; const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; @@ -731,6 +735,7 @@ fn checkCancel(t: *Threaded) error{Canceled}!void { } fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { + if (builtin.single_threaded) unreachable; // Interface should have prevented this. const t: *Threaded = @ptrCast(@alignCast(userdata)); if (prev_state == .contended) { try futexWait(t, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); @@ -741,6 +746,7 @@ fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex } fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + if (builtin.single_threaded) unreachable; // Interface should have prevented this. _ = userdata; if (prev_state == .contended) { futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); @@ -751,6 +757,7 @@ fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mute } fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + if (builtin.single_threaded) unreachable; // Interface should have prevented this. _ = userdata; _ = prev_state; if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { @@ -759,6 +766,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut } fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { + if (builtin.single_threaded) unreachable; // Deadlock. const t: *Threaded = @ptrCast(@alignCast(userdata)); const t_io = t.io(); comptime assert(@TypeOf(cond.state) == u64); @@ -789,6 +797,7 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: } fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + if (builtin.single_threaded) unreachable; // Deadlock. const t: *Threaded = @ptrCast(@alignCast(userdata)); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); @@ -833,6 +842,7 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I } fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { + if (builtin.single_threaded) unreachable; // Nothing to wake up. const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; comptime assert(@TypeOf(cond.state) == u64); @@ -4007,6 +4017,32 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca return; } + if (builtin.cpu.arch.isWasm()) { + comptime assert(builtin.cpu.has(.wasm, .atomics)); + try t.checkCancel(); + const timeout: i64 = -1; + const signed_expect: i32 = @bitCast(expect); + const result = asm volatile ( + \\local.get %[ptr] + \\local.get %[expected] + \\local.get %[timeout] + \\memory.atomic.wait32 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [expected] "r" (signed_expect), + [timeout] "r" (timeout), + ); + const is_debug = builtin.mode == .Debug; + switch (result) { + 0 => {}, // ok + 1 => {}, // expected != loaded + 2 => assert(!is_debug), // timeout + else => assert(!is_debug), + } + return; + } + @compileError("TODO"); } @@ -4054,6 +4090,31 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi return; } + if (builtin.cpu.arch.isWasm()) { + comptime assert(builtin.cpu.has(.wasm, .atomics)); + const timeout: i64 = -1; + const signed_expect: i32 = @bitCast(expect); + const result = asm volatile ( + \\local.get %[ptr] + \\local.get %[expected] + \\local.get %[timeout] + \\memory.atomic.wait32 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [expected] "r" (signed_expect), + [timeout] "r" (timeout), + ); + const is_debug = builtin.mode == .Debug; + switch (result) { + 0 => {}, // ok + 1 => {}, // expected != loaded + 2 => assert(!is_debug), // timeout + else => assert(!is_debug), + } + return; + } + @compileError("TODO"); } @@ -4119,6 +4180,22 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { } } + if (builtin.cpu.arch.isWasm()) { + comptime assert(builtin.cpu.has(.wasm, .atomics)); + assert(max_waiters != 0); + const woken_count = asm volatile ( + \\local.get %[ptr] + \\local.get %[waiters] + \\memory.atomic.notify 0 + \\local.set %[ret] + : [ret] "=r" (-> u32), + : [ptr] "r" (&ptr.raw), + [waiters] "r" (max_waiters), + ); + _ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled + return; + } + @compileError("TODO"); } From 43c2ba375db502be3d6ba45cd5e949482e14bfae Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 17 Oct 2025 13:34:46 -0700 Subject: [PATCH 138/244] std: accessZ -> access --- lib/std/Build/Step/Compile.zig | 4 ++-- lib/std/os.zig | 2 +- lib/std/zig/LibCInstallation.zig | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index 0f47a0b647..2188d8bfc7 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -1701,7 +1701,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { // This prevents a warning, that should probably be upgraded to an error in Zig's // CLI parsing code, when the linker sees an -L directory that does not exist. - if (prefix_dir.accessZ("lib", .{})) |_| { + if (prefix_dir.access("lib", .{})) |_| { try zig_args.appendSlice(&.{ "-L", b.pathJoin(&.{ search_prefix, "lib" }), }); @@ -1712,7 +1712,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { }), } - if (prefix_dir.accessZ("include", .{})) |_| { + if (prefix_dir.access("include", .{})) |_| { try zig_args.appendSlice(&.{ "-I", b.pathJoin(&.{ search_prefix, "include" }), }); diff --git a/lib/std/os.zig b/lib/std/os.zig index 7bee7ed104..8a2c606661 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -57,7 +57,7 @@ pub var argv: [][*:0]u8 = if (builtin.link_libc) undefined else switch (native_o }; /// Call from Windows-specific code if you already have a WTF-16LE encoded, null terminated string. -/// Otherwise use `access` or `accessZ`. +/// Otherwise use `access`. pub fn accessW(path: [*:0]const u16) windows.GetFileAttributesError!void { const ret = try windows.GetFileAttributesW(path); if (ret != windows.INVALID_FILE_ATTRIBUTES) { diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig index f6d381be82..2ab4e48570 100644 --- a/lib/std/zig/LibCInstallation.zig +++ b/lib/std/zig/LibCInstallation.zig @@ -329,7 +329,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, args: FindNativeOptions) F defer search_dir.close(); if (self.include_dir == null) { - if (search_dir.accessZ(include_dir_example_file, .{})) |_| { + if (search_dir.access(include_dir_example_file, .{})) |_| { self.include_dir = try allocator.dupeZ(u8, search_path); } else |err| switch (err) { error.FileNotFound => {}, @@ -338,7 +338,7 @@ fn findNativeIncludeDirPosix(self: *LibCInstallation, args: FindNativeOptions) F } if (self.sys_include_dir == null) { - if (search_dir.accessZ(sys_include_dir_example_file, .{})) |_| { + if (search_dir.access(sys_include_dir_example_file, .{})) |_| { self.sys_include_dir = try allocator.dupeZ(u8, search_path); } else |err| switch (err) { error.FileNotFound => {}, @@ -382,7 +382,7 @@ fn findNativeIncludeDirWindows( }; defer dir.close(); - dir.accessZ("stdlib.h", .{}) catch |err| switch (err) { + dir.access("stdlib.h", .{}) catch |err| switch (err) { error.FileNotFound => continue, else => return error.FileSystem, }; @@ -429,7 +429,7 @@ fn findNativeCrtDirWindows( }; defer dir.close(); - dir.accessZ("ucrt.lib", .{}) catch |err| switch (err) { + dir.access("ucrt.lib", .{}) catch |err| switch (err) { error.FileNotFound => continue, else => return error.FileSystem, }; @@ -496,7 +496,7 @@ fn findNativeKernel32LibDir( }; defer dir.close(); - dir.accessZ("kernel32.lib", .{}) catch |err| switch (err) { + dir.access("kernel32.lib", .{}) catch |err| switch (err) { error.FileNotFound => continue, else => return error.FileSystem, }; @@ -531,7 +531,7 @@ fn findNativeMsvcIncludeDir( }; defer dir.close(); - dir.accessZ("vcruntime.h", .{}) catch |err| switch (err) { + dir.access("vcruntime.h", .{}) catch |err| switch (err) { error.FileNotFound => return error.LibCStdLibHeaderNotFound, else => return error.FileSystem, }; From 97b9cc0adfee5b21079fccfde535b43391ac6233 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 18 Oct 2025 02:29:59 -0700 Subject: [PATCH 139/244] aro: avoid asking for the time this value should be calculated earlier and passed in --- lib/compiler/aro/aro/Compilation.zig | 2 +- lib/compiler/aro/aro/Driver.zig | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/compiler/aro/aro/Compilation.zig b/lib/compiler/aro/aro/Compilation.zig index e0b9a508cf..7d04589536 100644 --- a/lib/compiler/aro/aro/Compilation.zig +++ b/lib/compiler/aro/aro/Compilation.zig @@ -114,7 +114,7 @@ pub const Environment = struct { if (parsed > max_timestamp) return error.InvalidEpoch; return .{ .provided = parsed }; } else { - const timestamp = std.math.cast(u64, std.time.timestamp()) orelse return error.InvalidEpoch; + const timestamp = std.math.cast(u64, 0) orelse return error.InvalidEpoch; return .{ .system = std.math.clamp(timestamp, 0, max_timestamp) }; } } diff --git a/lib/compiler/aro/aro/Driver.zig b/lib/compiler/aro/aro/Driver.zig index 5d87268a5b..0b50321fe0 100644 --- a/lib/compiler/aro/aro/Driver.zig +++ b/lib/compiler/aro/aro/Driver.zig @@ -273,6 +273,7 @@ pub fn parseArgs( macro_buf: *std.ArrayList(u8), args: []const []const u8, ) (Compilation.Error || std.Io.Writer.Error)!bool { + const io = d.comp.io; var i: usize = 1; var comment_arg: []const u8 = ""; var hosted: ?bool = null; @@ -772,7 +773,7 @@ pub fn parseArgs( opts.arch_os_abi, @errorName(e), }), }; - d.comp.target = std.zig.system.resolveTargetQuery(query) catch |e| { + d.comp.target = std.zig.system.resolveTargetQuery(io, query) catch |e| { return d.fatal("unable to resolve target: {s}", .{errorDescription(e)}); }; } @@ -916,8 +917,7 @@ pub fn errorDescription(e: anyerror) []const u8 { error.NotDir => "is not a directory", error.NotOpenForReading => "file is not open for reading", error.NotOpenForWriting => "file is not open for writing", - error.InvalidUtf8 => "path is not valid UTF-8", - error.InvalidWtf8 => "path is not valid WTF-8", + error.BadPathName => "bad path name", error.FileBusy => "file is busy", error.NameTooLong => "file name is too long", error.AccessDenied => "access denied", From 2d7d98da0cd54e9dc12c5d4f97cdf2e7dac36446 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 18 Oct 2025 02:30:37 -0700 Subject: [PATCH 140/244] std.fs: use BadPathName rather than InvalidWtf8 on Windows --- lib/compiler/translate-c/main.zig | 6 ++- lib/std/Build/Cache/Path.zig | 10 +++-- lib/std/Thread.zig | 1 + lib/std/debug/SelfInfo/Windows.zig | 19 ++++---- lib/std/fs.zig | 11 +---- lib/std/fs/Dir.zig | 24 +++------- lib/std/fs/test.zig | 4 +- lib/std/os.zig | 3 -- lib/std/os/windows.zig | 6 ++- lib/std/posix.zig | 70 ++++++++++-------------------- lib/std/unicode.zig | 13 +++--- lib/std/zig/system.zig | 2 - 12 files changed, 64 insertions(+), 105 deletions(-) diff --git a/lib/compiler/translate-c/main.zig b/lib/compiler/translate-c/main.zig index 3a3fd7577e..d0e8faf8c2 100644 --- a/lib/compiler/translate-c/main.zig +++ b/lib/compiler/translate-c/main.zig @@ -18,6 +18,10 @@ pub fn main() u8 { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var args = process.argsAlloc(arena) catch { std.debug.print("ran out of memory allocating arguments\n", .{}); if (fast_exit) process.exit(1); @@ -42,7 +46,7 @@ pub fn main() u8 { }; defer diagnostics.deinit(); - var comp = aro.Compilation.initDefault(gpa, arena, &diagnostics, std.fs.cwd()) catch |err| switch (err) { + var comp = aro.Compilation.initDefault(gpa, arena, io, &diagnostics, std.fs.cwd()) catch |err| switch (err) { error.OutOfMemory => { std.debug.print("ran out of memory initializing C compilation\n", .{}); if (fast_exit) process.exit(1); diff --git a/lib/std/Build/Cache/Path.zig b/lib/std/Build/Cache/Path.zig index bf16fc6814..92290cfdf4 100644 --- a/lib/std/Build/Cache/Path.zig +++ b/lib/std/Build/Cache/Path.zig @@ -1,5 +1,7 @@ const Path = @This(); + const std = @import("../../std.zig"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const Allocator = std.mem.Allocator; @@ -119,7 +121,7 @@ pub fn atomicFile( return p.root_dir.handle.atomicFile(joined_path, options); } -pub fn access(p: Path, sub_path: []const u8, flags: fs.File.OpenFlags) !void { +pub fn access(p: Path, sub_path: []const u8, flags: Io.Dir.AccessOptions) !void { var buf: [fs.max_path_bytes]u8 = undefined; const joined_path = if (p.sub_path.len == 0) sub_path else p: { break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{ @@ -151,7 +153,7 @@ pub fn fmtEscapeString(path: Path) std.fmt.Alt(Path, formatEscapeString) { return .{ .data = path }; } -pub fn formatEscapeString(path: Path, writer: *std.Io.Writer) std.Io.Writer.Error!void { +pub fn formatEscapeString(path: Path, writer: *Io.Writer) Io.Writer.Error!void { if (path.root_dir.path) |p| { try std.zig.stringEscape(p, writer); if (path.sub_path.len > 0) try std.zig.stringEscape(fs.path.sep_str, writer); @@ -167,7 +169,7 @@ pub fn fmtEscapeChar(path: Path) std.fmt.Alt(Path, formatEscapeChar) { } /// Deprecated, use double quoted escape to print paths. -pub fn formatEscapeChar(path: Path, writer: *std.Io.Writer) std.Io.Writer.Error!void { +pub fn formatEscapeChar(path: Path, writer: *Io.Writer) Io.Writer.Error!void { if (path.root_dir.path) |p| { for (p) |byte| try std.zig.charEscape(byte, writer); if (path.sub_path.len > 0) try writer.writeByte(fs.path.sep); @@ -177,7 +179,7 @@ pub fn formatEscapeChar(path: Path, writer: *std.Io.Writer) std.Io.Writer.Error! } } -pub fn format(self: Path, writer: *std.Io.Writer) std.Io.Writer.Error!void { +pub fn format(self: Path, writer: *Io.Writer) Io.Writer.Error!void { if (std.fs.path.isAbsolute(self.sub_path)) { try writer.writeAll(self.sub_path); return; diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 9a602fbd6b..b5f950a08a 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -171,6 +171,7 @@ pub const SetNameError = error{ NameTooLong, Unsupported, Unexpected, + InvalidWtf8, } || posix.PrctlError || posix.WriteError || std.fs.File.OpenError || std.fmt.BufPrintError; pub fn setName(self: Thread, name: []const u8) SetNameError!void { diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index ea2fa96199..324b597d97 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -20,11 +20,11 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void { module_name_arena.deinit(); } -pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { +pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol { si.mutex.lock(); defer si.mutex.unlock(); const module = try si.findModule(gpa, address); - const di = try module.getDebugInfo(gpa); + const di = try module.getDebugInfo(gpa, io); return di.getSymbol(gpa, address - module.base_address); } pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]const u8 { @@ -190,6 +190,7 @@ const Module = struct { const DebugInfo = struct { arena: std.heap.ArenaAllocator.State, + io: Io, coff_image_base: u64, mapped_file: ?MappedFile, dwarf: ?Dwarf, @@ -209,9 +210,10 @@ const Module = struct { }; fn deinit(di: *DebugInfo, gpa: Allocator) void { + const io = di.io; if (di.dwarf) |*dwarf| dwarf.deinit(gpa); if (di.pdb) |*pdb| { - pdb.file_reader.file.close(); + pdb.file_reader.file.close(io); pdb.deinit(); } if (di.mapped_file) |*mf| mf.deinit(); @@ -277,11 +279,11 @@ const Module = struct { } }; - fn getDebugInfo(module: *Module, gpa: Allocator) Error!*DebugInfo { - if (module.di == null) module.di = loadDebugInfo(module, gpa); + fn getDebugInfo(module: *Module, gpa: Allocator, io: Io) Error!*DebugInfo { + if (module.di == null) module.di = loadDebugInfo(module, gpa, io); return if (module.di.?) |*di| di else |err| err; } - fn loadDebugInfo(module: *const Module, gpa: Allocator) Error!DebugInfo { + fn loadDebugInfo(module: *const Module, gpa: Allocator, io: Io) Error!DebugInfo { const mapped_ptr: [*]const u8 = @ptrFromInt(module.base_address); const mapped = mapped_ptr[0..module.size]; var coff_obj = coff.Coff.init(mapped, true) catch return error.InvalidDebugInfo; @@ -306,6 +308,7 @@ const Module = struct { ); if (len == 0) return error.MissingDebugInfo; const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) { + error.Canceled => |e| return e, error.Unexpected => |e| return e, error.FileNotFound => return error.MissingDebugInfo, @@ -314,8 +317,6 @@ const Module = struct { error.NotDir, error.SymLinkLoop, error.NameTooLong, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, => return error.InvalidDebugInfo, @@ -435,7 +436,7 @@ const Module = struct { errdefer pdb_file.close(); const pdb_reader = try arena.create(Io.File.Reader); - pdb_reader.* = pdb_file.reader(try arena.alloc(u8, 4096)); + pdb_reader.* = pdb_file.reader(io, try arena.alloc(u8, 4096)); var pdb = Pdb.init(gpa, pdb_reader) catch |err| switch (err) { error.OutOfMemory, error.ReadFailed, error.Unexpected => |e| return e, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 395e18e6e5..7fae26cb62 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -500,7 +500,6 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { var real_path_buf: [max_path_bytes]u8 = undefined; const real_path = std.posix.realpathZ(&symlink_path_buf, &real_path_buf) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }; @@ -511,15 +510,11 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { } switch (native_os) { .linux, .serenity => return posix.readlinkZ("/proc/self/exe", out_buffer) catch |err| switch (err) { - error.InvalidUtf8 => unreachable, // WASI-only - error.InvalidWtf8 => unreachable, // Windows-only error.UnsupportedReparsePointType => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }, .illumos => return posix.readlinkZ("/proc/self/path/a.out", out_buffer) catch |err| switch (err) { - error.InvalidUtf8 => unreachable, // WASI-only - error.InvalidWtf8 => unreachable, // Windows-only error.UnsupportedReparsePointType => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, @@ -548,7 +543,6 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { // argv[0] is a path (relative or absolute): use realpath(3) directly var real_path_buf: [max_path_bytes]u8 = undefined; const real_path = posix.realpathZ(std.os.argv[0], &real_path_buf) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, }; @@ -591,10 +585,7 @@ pub fn selfExePath(out_buffer: []u8) SelfExePathError![]u8 { // that the symlink points to, though, so we need to get the realpath. var pathname_w = try windows.wToPrefixedFileW(null, image_path_name); - const wide_slice = std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data) catch |err| switch (err) { - error.InvalidWtf8 => unreachable, - else => |e| return e, - }; + const wide_slice = try std.fs.cwd().realpathW2(pathname_w.span(), &pathname_w.data); const len = std.unicode.calcWtf8Len(wide_slice); if (len > out_buffer.len) diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 5714d98e39..5187ab69a8 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1472,11 +1472,9 @@ pub const DeleteDirError = error{ NotDir, SystemResources, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, @@ -1577,9 +1575,7 @@ pub fn symLink( // when converting to an NT namespaced path. CreateSymbolicLink in // symLinkW will handle the necessary conversion. var target_path_w: windows.PathSpace = undefined; - if (try std.unicode.checkWtf8ToWtf16LeOverflow(target_path, &target_path_w.data)) { - return error.NameTooLong; - } + try std.unicode.checkWtf8ToWtf16LeOverflow(target_path, &target_path_w.data); target_path_w.len = try std.unicode.wtf8ToWtf16Le(&target_path_w.data, target_path); target_path_w.data[target_path_w.len] = 0; // However, we need to canonicalize any path separators to `\`, since if @@ -1808,11 +1804,9 @@ pub const DeleteTreeError = error{ /// One of the path components was not a directory. /// This error is unreachable if `sub_path` does not contain a path separator. NotDir, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, /// On Windows, file paths cannot contain these characters: /// '/', '*', '?', '"', '<', '>', '|' BadPathName, @@ -1913,8 +1907,6 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -1999,8 +1991,6 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -2112,8 +2102,6 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, @@ -2201,8 +2189,6 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File error.AccessDenied, error.PermissionDenied, - error.InvalidUtf8, - error.InvalidWtf8, error.SymLinkLoop, error.NameTooLong, error.SystemResources, diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index a20be08fcb..75f35a46da 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -2019,8 +2019,8 @@ test "delete a setAsCwd directory on Windows" { test "invalid UTF-8/WTF-8 paths" { const expected_err = switch (native_os) { - .wasi => error.InvalidUtf8, - .windows => error.InvalidWtf8, + .wasi => error.BadPathName, + .windows => error.BadPathName, else => return error.SkipZigTest, }; diff --git a/lib/std/os.zig b/lib/std/os.zig index 8a2c606661..02fddb32b4 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -137,8 +137,6 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. switch (err) { error.NotLink => unreachable, error.BadPathName => unreachable, - error.InvalidUtf8 => unreachable, // WASI-only - error.InvalidWtf8 => unreachable, // Windows-only error.UnsupportedReparsePointType => unreachable, // Windows-only error.NetworkNotFound => unreachable, // Windows-only else => |e| return e, @@ -153,7 +151,6 @@ pub fn getFdPath(fd: std.posix.fd_t, out_buffer: *[max_path_bytes]u8) std.posix. const target = posix.readlinkZ(proc_path, out_buffer) catch |err| switch (err) { error.UnsupportedReparsePointType => unreachable, error.NotLink => unreachable, - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; return target; diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index f36effe380..b4780ed203 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -2425,7 +2425,7 @@ pub fn normalizePath(comptime T: type, path: []T) RemoveDotDirsError!usize { return prefix_len + try removeDotDirsSanitized(T, path[prefix_len..new_len]); } -pub const Wtf8ToPrefixedFileWError = error{InvalidWtf8} || Wtf16ToPrefixedFileWError; +pub const Wtf8ToPrefixedFileWError = Wtf16ToPrefixedFileWError; /// Same as `sliceToPrefixedFileW` but accepts a pointer /// to a null-terminated WTF-8 encoded path. @@ -2438,7 +2438,9 @@ pub fn cStrToPrefixedFileW(dir: ?HANDLE, s: [*:0]const u8) Wtf8ToPrefixedFileWEr /// https://wtf-8.codeberg.page/ pub fn sliceToPrefixedFileW(dir: ?HANDLE, path: []const u8) Wtf8ToPrefixedFileWError!PathSpace { var temp_path: PathSpace = undefined; - temp_path.len = try std.unicode.wtf8ToWtf16Le(&temp_path.data, path); + temp_path.len = std.unicode.wtf8ToWtf16Le(&temp_path.data, path) catch |err| switch (err) { + error.InvalidWtf8 => return error.BadPathName, + }; temp_path.data[temp_path.len] = 0; return wToPrefixedFileW(dir, temp_path.span()); } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 358715a61f..697ba1a59a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -486,8 +486,8 @@ fn fchmodat2(dirfd: fd_t, path: []const u8, mode: mode_t, flags: u32) FChmodAtEr const stat = fstatatZ(pathfd, "", AT.EMPTY_PATH) catch |err| switch (err) { error.NameTooLong => unreachable, error.FileNotFound => unreachable, - error.InvalidUtf8 => unreachable, error.Streaming => unreachable, + error.BadPathName => return error.Unexpected, error.Canceled => return error.Canceled, else => |e| return e, }; @@ -1914,14 +1914,9 @@ pub const SymLinkError = error{ ReadOnlyFileSystem, NotDir, NameTooLong, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, } || UnexpectedError; @@ -2210,14 +2205,10 @@ pub const UnlinkError = error{ SystemResources, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, - - /// On Windows, file paths cannot contain these characters: + /// Windows: file paths cannot contain these characters: /// '/', '*', '?', '"', '<', '>', '|' BadPathName, @@ -2396,11 +2387,9 @@ pub const RenameError = error{ PathAlreadyExists, ReadOnlyFileSystem, RenameAcrossMountPoints, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, NoDevice, SharingViolation, @@ -2839,11 +2828,9 @@ pub const DeleteDirError = error{ NotDir, DirNotEmpty, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, @@ -2916,12 +2903,10 @@ pub const ChangeCurDirError = error{ FileNotFound, SystemResources, NotDir, - BadPathName, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, + BadPathName, } || UnexpectedError; /// Changes the current working directory of the calling process. @@ -2933,9 +2918,7 @@ pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { @compileError("WASI does not support os.chdir"); } else if (native_os == .windows) { var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - if (try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path, &wtf16_dir_path)) { - return error.NameTooLong; - } + try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path, &wtf16_dir_path); const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); return chdirW(wtf16_dir_path[0..len]); } else { @@ -2952,9 +2935,7 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { if (native_os == .windows) { const dir_path_span = mem.span(dir_path); var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - if (try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path_span, &wtf16_dir_path)) { - return error.NameTooLong; - } + try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path_span, &wtf16_dir_path); const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); return chdirW(wtf16_dir_path[0..len]); } else if (native_os == .wasi and !builtin.link_libc) { @@ -3016,11 +2997,9 @@ pub const ReadLinkError = error{ SystemResources, NotLink, NotDir, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, BadPathName, /// Windows-only. This error may occur if the opened reparse point is /// of unsupported type. @@ -4705,22 +4684,20 @@ pub const AccessError = error{ NameTooLong, InputOutput, SystemResources, - BadPathName, FileBusy, SymLinkLoop, ReadOnlyFileSystem, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// WASI: file paths must be valid UTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, + BadPathName, Canceled, } || UnexpectedError; /// check user's permissions for a file /// /// * On Windows, asserts `path` is valid [WTF-8](https://wtf-8.codeberg.page/). -/// * On WASI, invalid UTF-8 passed to `path` causes `error.InvalidUtf8`. +/// * On WASI, invalid UTF-8 passed to `path` causes `error.BadPathName`. /// * On other platforms, `path` is an opaque sequence of bytes with no particular encoding. /// /// On Windows, `mode` is ignored. This is a POSIX API that is only partially supported by @@ -5154,16 +5131,15 @@ pub const RealPathError = error{ SystemResources, NoSpaceLeft, FileSystem, - BadPathName, DeviceBusy, ProcessNotFound, SharingViolation, PipeBusy, - /// Windows-only; file paths provided by the user must be valid WTF-8. + /// Windows: file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ - InvalidWtf8, + BadPathName, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, diff --git a/lib/std/unicode.zig b/lib/std/unicode.zig index 7fbf1094ba..2a6dca0d8a 100644 --- a/lib/std/unicode.zig +++ b/lib/std/unicode.zig @@ -1809,27 +1809,28 @@ pub fn wtf8ToWtf16Le(wtf16le: []u16, wtf8: []const u8) error{InvalidWtf8}!usize return utf8ToUtf16LeImpl(wtf16le, wtf8, .can_encode_surrogate_half); } -fn checkUtf8ToUtf16LeOverflowImpl(utf8: []const u8, utf16le: []const u16, comptime surrogates: Surrogates) !bool { +fn checkUtf8ToUtf16LeOverflowImpl(utf8: []const u8, utf16le: []const u16, comptime surrogates: Surrogates) !void { // Each u8 in UTF-8/WTF-8 correlates to at most one u16 in UTF-16LE/WTF-16LE. - if (utf16le.len >= utf8.len) return false; + if (utf16le.len >= utf8.len) return; const utf16_len = calcUtf16LeLenImpl(utf8, surrogates) catch { return switch (surrogates) { .cannot_encode_surrogate_half => error.InvalidUtf8, .can_encode_surrogate_half => error.InvalidWtf8, }; }; - return utf16_len > utf16le.len; + if (utf16_len > utf16le.len) + return error.NameTooLong; } /// Checks if calling `utf8ToUtf16Le` would overflow. Might fail if utf8 is not /// valid UTF-8. -pub fn checkUtf8ToUtf16LeOverflow(utf8: []const u8, utf16le: []const u16) error{InvalidUtf8}!bool { +pub fn checkUtf8ToUtf16LeOverflow(utf8: []const u8, utf16le: []const u16) error{ InvalidUtf8, NameTooLong }!void { return checkUtf8ToUtf16LeOverflowImpl(utf8, utf16le, .cannot_encode_surrogate_half); } /// Checks if calling `utf8ToUtf16Le` would overflow. Might fail if wtf8 is not /// valid WTF-8. -pub fn checkWtf8ToWtf16LeOverflow(wtf8: []const u8, wtf16le: []const u16) error{InvalidWtf8}!bool { +pub fn checkWtf8ToWtf16LeOverflow(wtf8: []const u8, wtf16le: []const u16) error{ InvalidWtf8, NameTooLong }!void { return checkUtf8ToUtf16LeOverflowImpl(wtf8, wtf16le, .can_encode_surrogate_half); } @@ -2039,7 +2040,7 @@ fn testRoundtripWtf8(wtf8: []const u8) !void { var wtf16_buf: [32]u16 = undefined; const wtf16_len = try wtf8ToWtf16Le(&wtf16_buf, wtf8); try testing.expectEqual(wtf16_len, calcWtf16LeLen(wtf8)); - try testing.expectEqual(false, checkWtf8ToWtf16LeOverflow(wtf8, &wtf16_buf)); + try checkWtf8ToWtf16LeOverflow(wtf8, &wtf16_buf); const wtf16 = wtf16_buf[0..wtf16_len]; var roundtripped_buf: [32]u8 = undefined; diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 6313bff374..a2315b3ab9 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -674,8 +674,6 @@ fn abiAndDynamicLinkerFromFile( var link_buf: [posix.PATH_MAX]u8 = undefined; const link_name = posix.readlink(dl_path, &link_buf) catch |err| switch (err) { error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only error.BadPathName => unreachable, // Windows only error.UnsupportedReparsePointType => unreachable, // Windows only error.NetworkNotFound => unreachable, // Windows only From 21e195a1a96100cd5bcd47b6d9565b2141ad13e1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sat, 18 Oct 2025 07:38:10 -0700 Subject: [PATCH 141/244] std: move some windows path checking logic --- lib/std/fs/Dir.zig | 2 +- lib/std/os/windows.zig | 9 +++++++++ lib/std/posix.zig | 4 ++-- lib/std/unicode.zig | 26 -------------------------- 4 files changed, 12 insertions(+), 29 deletions(-) diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 5187ab69a8..c4cbaf7196 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1575,7 +1575,7 @@ pub fn symLink( // when converting to an NT namespaced path. CreateSymbolicLink in // symLinkW will handle the necessary conversion. var target_path_w: windows.PathSpace = undefined; - try std.unicode.checkWtf8ToWtf16LeOverflow(target_path, &target_path_w.data); + try windows.checkWtf8ToWtf16LeOverflow(target_path, &target_path_w.data); target_path_w.len = try std.unicode.wtf8ToWtf16Le(&target_path_w.data, target_path); target_path_w.data[target_path_w.len] = 0; // However, we need to canonicalize any path separators to `\`, since if diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index b4780ed203..1d49a890e4 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -5739,3 +5739,12 @@ pub fn ProcessBaseAddress(handle: HANDLE) ProcessBaseAddressError!HMODULE { const ppeb: *const PEB = @ptrCast(@alignCast(peb_out.ptr)); return ppeb.ImageBaseAddress; } + +pub fn checkWtf8ToWtf16LeOverflow(wtf8: []const u8, wtf16le: []const u16) error{ BadPathName, NameTooLong }!void { + // Each u8 in UTF-8/WTF-8 correlates to at most one u16 in UTF-16LE/WTF-16LE. + if (wtf16le.len >= wtf8.len) return; + const utf16_len = std.unicode.calcUtf16LeLenImpl(wtf8, .can_encode_surrogate_half) catch + return error.BadPathName; + if (utf16_len > wtf16le.len) + return error.NameTooLong; +} diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 697ba1a59a..a58204e1dc 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -2918,7 +2918,7 @@ pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { @compileError("WASI does not support os.chdir"); } else if (native_os == .windows) { var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path, &wtf16_dir_path); + try windows.checkWtf8ToWtf16LeOverflow(dir_path, &wtf16_dir_path); const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); return chdirW(wtf16_dir_path[0..len]); } else { @@ -2935,7 +2935,7 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { if (native_os == .windows) { const dir_path_span = mem.span(dir_path); var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - try std.unicode.checkWtf8ToWtf16LeOverflow(dir_path_span, &wtf16_dir_path); + try windows.checkWtf8ToWtf16LeOverflow(dir_path_span, &wtf16_dir_path); const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); return chdirW(wtf16_dir_path[0..len]); } else if (native_os == .wasi and !builtin.link_libc) { diff --git a/lib/std/unicode.zig b/lib/std/unicode.zig index 2a6dca0d8a..1aae6d488f 100644 --- a/lib/std/unicode.zig +++ b/lib/std/unicode.zig @@ -1809,31 +1809,6 @@ pub fn wtf8ToWtf16Le(wtf16le: []u16, wtf8: []const u8) error{InvalidWtf8}!usize return utf8ToUtf16LeImpl(wtf16le, wtf8, .can_encode_surrogate_half); } -fn checkUtf8ToUtf16LeOverflowImpl(utf8: []const u8, utf16le: []const u16, comptime surrogates: Surrogates) !void { - // Each u8 in UTF-8/WTF-8 correlates to at most one u16 in UTF-16LE/WTF-16LE. - if (utf16le.len >= utf8.len) return; - const utf16_len = calcUtf16LeLenImpl(utf8, surrogates) catch { - return switch (surrogates) { - .cannot_encode_surrogate_half => error.InvalidUtf8, - .can_encode_surrogate_half => error.InvalidWtf8, - }; - }; - if (utf16_len > utf16le.len) - return error.NameTooLong; -} - -/// Checks if calling `utf8ToUtf16Le` would overflow. Might fail if utf8 is not -/// valid UTF-8. -pub fn checkUtf8ToUtf16LeOverflow(utf8: []const u8, utf16le: []const u16) error{ InvalidUtf8, NameTooLong }!void { - return checkUtf8ToUtf16LeOverflowImpl(utf8, utf16le, .cannot_encode_surrogate_half); -} - -/// Checks if calling `utf8ToUtf16Le` would overflow. Might fail if wtf8 is not -/// valid WTF-8. -pub fn checkWtf8ToWtf16LeOverflow(wtf8: []const u8, wtf16le: []const u16) error{ InvalidWtf8, NameTooLong }!void { - return checkUtf8ToUtf16LeOverflowImpl(wtf8, wtf16le, .can_encode_surrogate_half); -} - /// Surrogate codepoints (U+D800 to U+DFFF) are replaced by the Unicode replacement /// character (U+FFFD). /// All surrogate codepoints and the replacement character are encoded as three @@ -2040,7 +2015,6 @@ fn testRoundtripWtf8(wtf8: []const u8) !void { var wtf16_buf: [32]u16 = undefined; const wtf16_len = try wtf8ToWtf16Le(&wtf16_buf, wtf8); try testing.expectEqual(wtf16_len, calcWtf16LeLen(wtf8)); - try checkWtf8ToWtf16LeOverflow(wtf8, &wtf16_buf); const wtf16 = wtf16_buf[0..wtf16_len]; var roundtripped_buf: [32]u8 = undefined; From 83e4ff6f4c739096806b951e7c6e54399c18d19b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 07:08:13 -0700 Subject: [PATCH 142/244] std.Io: add dirClose --- lib/std/Io.zig | 1 + lib/std/Io/Dir.zig | 4 ++++ lib/std/Io/Threaded.zig | 7 +++++++ lib/std/Io/net.zig | 11 ++--------- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 859568a15a..a6507df618 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -667,6 +667,7 @@ pub const VTable = struct { dirCreateFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, dirOpenFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, dirOpenDir: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.OpenError!Dir, + dirClose: *const fn (?*anyopaque, Dir) void, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, fileClose: *const fn (?*anyopaque, File) void, fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 45833dd7f7..749bea99ba 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -93,6 +93,10 @@ pub fn openDir(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) Ope return io.vtable.dirOpenDir(io.userdata, dir, sub_path, options); } +pub fn close(dir: Dir, io: Io) void { + return io.vtable.dirClose(io.userdata, dir); +} + pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index b1ef09e34d..330cc6725d 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -203,6 +203,7 @@ pub fn io(t: *Threaded) Io { .wasi => dirOpenDirWasi, else => dirOpenDirPosix, }, + .dirClose = dirClose, .fileClose = fileClose, .fileWriteStreaming = fileWriteStreaming, .fileWritePositional = fileWritePositional, @@ -1685,6 +1686,12 @@ fn dirOpenDirPosix( @panic("TODO"); } +fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + posix.close(dir.handle); +} + fn dirOpenDirWasi( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 17d82451c5..f262a0d6b5 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1033,9 +1033,8 @@ pub const Socket = struct { }; /// Leaves `address` in a valid state. - pub fn close(s: *Socket, io: Io) void { + pub fn close(s: *const Socket, io: Io) void { io.vtable.netClose(io.userdata, s.handle); - s.handle = undefined; } pub const SendError = error{ @@ -1163,13 +1162,7 @@ pub const Stream = struct { const max_iovecs_len = 8; - pub fn close(s: *Stream, io: Io) void { - io.vtable.netClose(io.userdata, s.socket.handle); - s.* = undefined; - } - - /// Same as `close` but doesn't try to set `Stream` to `undefined`. - pub fn closeConst(s: *const Stream, io: Io) void { + pub fn close(s: *const Stream, io: Io) void { io.vtable.netClose(io.userdata, s.socket.handle); } From b215f8667a0cc59888926fcfcbb4be4370bd2216 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 07:09:28 -0700 Subject: [PATCH 143/244] std.Io.net.HostName.ResolvConf: ignore nameservers above max --- lib/std/Io/net/HostName.zig | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 4cd0621b97..376f798e3e 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -310,6 +310,8 @@ pub const ResolvConf = struct { search_buffer: [max_len]u8, search_len: usize, + /// According to resolv.conf(5) there is a maximum of 3 nameservers in this + /// file. pub const max_nameservers = 3; /// Returns `error.StreamTooLong` if a line is longer than 512 bytes. @@ -394,9 +396,10 @@ pub const ResolvConf = struct { } fn addNumeric(rc: *ResolvConf, io: Io, name: []const u8, port: u16) !void { - assert(rc.nameservers_len < rc.nameservers_buffer.len); - rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port); - rc.nameservers_len += 1; + if (rc.nameservers_len < rc.nameservers_buffer.len) { + rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port); + rc.nameservers_len += 1; + } } pub fn nameservers(rc: *const ResolvConf) []const IpAddress { From 10b1eef2d3901d17cf8810689a9e1eaf6d7901d9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 14:08:21 -0700 Subject: [PATCH 144/244] std: fix compilation errors on Windows --- lib/compiler/test_runner.zig | 4 +- lib/std/Build/Step.zig | 2 +- lib/std/Io/Threaded.zig | 4 +- lib/std/Io/net/HostName.zig | 2 +- lib/std/Thread.zig | 2 +- lib/std/builtin.zig | 13 ---- lib/std/debug.zig | 53 ++++++++++---- lib/std/debug/SelfInfo/MachO.zig | 4 +- lib/std/debug/SelfInfo/Windows.zig | 3 +- lib/std/fs/Dir.zig | 5 +- lib/std/fs/File.zig | 4 +- lib/std/heap/debug_allocator.zig | 109 ++++++++++++++++++++++++----- lib/std/os/windows.zig | 34 +++++---- lib/std/posix.zig | 9 +-- lib/std/posix/test.zig | 14 ---- lib/std/testing.zig | 6 +- tools/incr-check.zig | 19 +++-- 17 files changed, 186 insertions(+), 101 deletions(-) diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 0d6f451947..fbf37f7ec9 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -148,7 +148,7 @@ fn mainServer() !void { error.SkipZigTest => .skip, else => s: { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpStackTrace(trace.*); } break :s .fail; }, @@ -269,7 +269,7 @@ fn mainTerminal() void { std.debug.print("FAIL ({t})\n", .{err}); } if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpStackTrace(trace.*); } test_node.end(); }, diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index aa922ff37b..d1985739bd 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -332,7 +332,7 @@ pub fn cast(step: *Step, comptime T: type) ?*T { pub fn dump(step: *Step, w: *Io.Writer, tty_config: Io.tty.Config) void { if (step.debug_stack_trace.instruction_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; - std.debug.writeStackTrace(&step.debug_stack_trace, w, tty_config) catch {}; + std.debug.writeStackTrace(step.debug_stack_trace, w, tty_config) catch {}; } else { const field = "debug_stack_frames_count"; comptime assert(@hasField(Build, field)); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 330cc6725d..18e6e72563 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -31,7 +31,7 @@ const max_iovecs_len = 8; const splat_buffer_size = 64; comptime { - assert(max_iovecs_len <= posix.IOV_MAX); + if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX); } const Closure = struct { @@ -91,9 +91,7 @@ pub fn init( /// Statically initialize such that any call to the following functions will /// fail with `error.OutOfMemory`: -/// * `Io.VTable.async` /// * `Io.VTable.concurrent` -/// * `Io.VTable.groupAsync` /// When initialized this way, `deinit` is safe, but unnecessary to call. pub const init_single_threaded: Threaded = .{ .allocator = .failing, diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 376f798e3e..ea1ffc4834 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -221,7 +221,7 @@ pub fn connect( defer { connect_many.cancel(io); if (!saw_end) while (true) switch (connect_many_queue.getOneUncancelable(io)) { - .connection => |loser| if (loser) |s| s.closeConst(io) else |_| continue, + .connection => |loser| if (loser) |s| s.close(io) else |_| continue, .end => break, }; } diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index b5f950a08a..891c4f220e 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -577,7 +577,7 @@ fn callFn(comptime f: anytype, args: anytype) switch (Impl) { @call(.auto, f, args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpStackTrace(trace.*); } }; diff --git a/lib/std/builtin.zig b/lib/std/builtin.zig index c0be44b939..8f974161f8 100644 --- a/lib/std/builtin.zig +++ b/lib/std/builtin.zig @@ -37,19 +37,6 @@ pub const subsystem: ?std.Target.SubSystem = blk: { pub const StackTrace = struct { index: usize, instruction_addresses: []usize, - - pub fn format(st: *const StackTrace, writer: *std.Io.Writer) std.Io.Writer.Error!void { - // TODO: re-evaluate whether to use format() methods at all. - // Until then, avoid an error when using GeneralPurposeAllocator with WebAssembly - // where it tries to call detectTTYConfig here. - if (builtin.os.tag == .freestanding) return; - - // TODO: why on earth are we using stderr's ttyconfig? - // If we want colored output, we should just make a formatter out of `writeStackTrace`. - const tty_config = std.Io.tty.detectConfig(.stderr()); - try writer.writeAll("\n"); - try std.debug.writeStackTrace(st, writer, tty_config); - } }; /// This data structure is used by the Zig language code generation and diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 7e09bfec8a..3d88123c64 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -1,4 +1,7 @@ const std = @import("std.zig"); +const Io = std.Io; +const Writer = std.Io.Writer; +const tty = std.Io.tty; const math = std.math; const mem = std.mem; const posix = std.posix; @@ -7,12 +10,11 @@ const testing = std.testing; const Allocator = mem.Allocator; const File = std.fs.File; const windows = std.os.windows; -const Writer = std.Io.Writer; -const tty = std.Io.tty; const builtin = @import("builtin"); const native_arch = builtin.cpu.arch; const native_os = builtin.os.tag; +const StackTrace = std.builtin.StackTrace; const root = @import("root"); @@ -545,13 +547,13 @@ pub fn defaultPanic( stderr.print("panic: ", .{}) catch break :trace; } else { const current_thread_id = std.Thread.getCurrentId(); - stderr.print("thread {} panic: ", .{current_thread_id}) catch break :trace; + stderr.print("thread {d} panic: ", .{current_thread_id}) catch break :trace; } stderr.print("{s}\n", .{msg}) catch break :trace; if (@errorReturnTrace()) |t| if (t.index > 0) { stderr.writeAll("error return context:\n") catch break :trace; - writeStackTrace(t, stderr, tty_config) catch break :trace; + writeStackTrace(t.*, stderr, tty_config) catch break :trace; stderr.writeAll("\nstack trace:\n") catch break :trace; }; writeCurrentStackTrace(.{ @@ -607,8 +609,8 @@ pub const StackUnwindOptions = struct { /// the given buffer, so `addr_buf` must have a lifetime at least equal to the `StackTrace`. /// /// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it. -pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) std.builtin.StackTrace { - const empty_trace: std.builtin.StackTrace = .{ .index = 0, .instruction_addresses = &.{} }; +pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace { + const empty_trace: StackTrace = .{ .index = 0, .instruction_addresses = &.{} }; if (!std.options.allow_stack_tracing) return empty_trace; var it = StackIterator.init(options.context) catch return empty_trace; defer it.deinit(); @@ -646,6 +648,9 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_config: tty.Config) Writer.Error!void { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + if (!std.options.allow_stack_tracing) { tty_config.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); @@ -730,7 +735,7 @@ pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Wri } // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); + try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); printed_any_frame = true; }, }; @@ -754,14 +759,29 @@ pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { }; } +pub const FormatStackTrace = struct { + stack_trace: StackTrace, + tty_config: tty.Config, + + pub fn format(context: @This(), writer: *Io.Writer) Io.Writer.Error!void { + try writer.writeAll("\n"); + try writeStackTrace(context.stack_trace, writer, context.tty_config); + } +}; + /// Write a previously captured stack trace to `writer`, annotated with source locations. -pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { +pub fn writeStackTrace(st: StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { if (!std.options.allow_stack_tracing) { tty_config.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); tty_config.setColor(writer, .reset) catch {}; return; } + // We use an independent Io implementation here in case there was a problem + // with the application's Io implementation itself. + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace. const n_frames = st.index; @@ -779,7 +799,7 @@ pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_c for (st.instruction_addresses[0..captured_frames]) |ret_addr| { // `ret_addr` is the return address, which is *after* the function call. // Subtract 1 to get an address *in* the function call for a better source location. - try printSourceAtAddress(di_gpa, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); + try printSourceAtAddress(di_gpa, io, di, writer, ret_addr -| StackIterator.ra_call_offset, tty_config); } if (n_frames > captured_frames) { tty_config.setColor(writer, .bold) catch {}; @@ -788,7 +808,7 @@ pub fn writeStackTrace(st: *const std.builtin.StackTrace, writer: *Writer, tty_c } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. -pub fn dumpStackTrace(st: *const std.builtin.StackTrace) void { +pub fn dumpStackTrace(st: StackTrace) void { const tty_config = tty.detectConfig(.stderr()); const stderr = lockStderrWriter(&.{}); defer unlockStderrWriter(); @@ -1075,8 +1095,8 @@ pub inline fn stripInstructionPtrAuthCode(ptr: usize) usize { return ptr; } -fn printSourceAtAddress(gpa: Allocator, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void { - const symbol: Symbol = debug_info.getSymbol(gpa, address) catch |err| switch (err) { +fn printSourceAtAddress(gpa: Allocator, io: Io, debug_info: *SelfInfo, writer: *Writer, address: usize, tty_config: tty.Config) Writer.Error!void { + const symbol: Symbol = debug_info.getSymbol(gpa, io, address) catch |err| switch (err) { error.MissingDebugInfo, error.UnsupportedDebugInfo, error.InvalidDebugInfo, @@ -1581,11 +1601,14 @@ test "manage resources correctly" { } }; const gpa = std.testing.allocator; - var discarding: std.Io.Writer.Discarding = .init(&.{}); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + var discarding: Io.Writer.Discarding = .init(&.{}); var di: SelfInfo = .init; defer di.deinit(gpa); try printSourceAtAddress( gpa, + io, &di, &discarding.writer, S.showMyTrace(), @@ -1659,11 +1682,11 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize stderr.print("{s}:\n", .{t.notes[i]}) catch return; var frames_array_mutable = frames_array; const frames = mem.sliceTo(frames_array_mutable[0..], 0); - const stack_trace: std.builtin.StackTrace = .{ + const stack_trace: StackTrace = .{ .index = frames.len, .instruction_addresses = frames, }; - writeStackTrace(&stack_trace, stderr, tty_config) catch return; + writeStackTrace(stack_trace, stderr, tty_config) catch return; } if (t.index > end) { stderr.print("{d} more traces not shown; consider increasing trace size\n", .{ diff --git a/lib/std/debug/SelfInfo/MachO.zig b/lib/std/debug/SelfInfo/MachO.zig index 8a0d9f0e1d..f7eb4465c5 100644 --- a/lib/std/debug/SelfInfo/MachO.zig +++ b/lib/std/debug/SelfInfo/MachO.zig @@ -30,7 +30,8 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void { si.ofiles.deinit(gpa); } -pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { +pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol { + _ = io; const module = try si.findModule(gpa, address); defer si.mutex.unlock(); @@ -970,6 +971,7 @@ fn loadOFile(gpa: Allocator, o_file_path: []const u8) !OFile { } const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Dwarf = std.debug.Dwarf; const Error = std.debug.SelfInfoError; diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index 324b597d97..a0b26f8ec5 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -474,7 +474,7 @@ const Module = struct { break :pdb pdb; }; errdefer if (opt_pdb) |*pdb| { - pdb.file_reader.file.close(); + pdb.file_reader.file.close(io); pdb.deinit(); }; @@ -484,6 +484,7 @@ const Module = struct { return .{ .arena = arena_instance.state, + .io = io, .coff_image_base = coff_image_base, .mapped_file = mapped_file, .dwarf = opt_dwarf, diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index c4cbaf7196..aa1180a90d 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1062,7 +1062,7 @@ pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenOptio w.SYNCHRONIZE | w.FILE_TRAVERSE | (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); - return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); + return self.makeOpenPathAccessMaskW(sub_path, base_flags, !open_dir_options.follow_symlinks); }, else => { return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { @@ -1575,8 +1575,7 @@ pub fn symLink( // when converting to an NT namespaced path. CreateSymbolicLink in // symLinkW will handle the necessary conversion. var target_path_w: windows.PathSpace = undefined; - try windows.checkWtf8ToWtf16LeOverflow(target_path, &target_path_w.data); - target_path_w.len = try std.unicode.wtf8ToWtf16Le(&target_path_w.data, target_path); + target_path_w.len = try windows.wtf8ToWtf16Le(&target_path_w.data, target_path); target_path_w.data[target_path_w.len] = 0; // However, we need to canonicalize any path separators to `\`, since if // the target path is relative, then it must use `\` as the path separator. diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index ca3fb47a5a..ba0f29fd87 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -564,8 +564,8 @@ pub fn updateTimes( mtime: Io.Timestamp, ) UpdateTimesError!void { if (builtin.os.tag == .windows) { - const atime_ft = windows.nanoSecondsToFileTime(atime.nanoseconds); - const mtime_ft = windows.nanoSecondsToFileTime(mtime.nanoseconds); + const atime_ft = windows.nanoSecondsToFileTime(atime); + const mtime_ft = windows.nanoSecondsToFileTime(mtime); return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft); } const times = [2]posix.timespec{ diff --git a/lib/std/heap/debug_allocator.zig b/lib/std/heap/debug_allocator.zig index 8e66f722c3..4480009781 100644 --- a/lib/std/heap/debug_allocator.zig +++ b/lib/std/heap/debug_allocator.zig @@ -80,15 +80,15 @@ //! //! Resizing and remapping are forwarded directly to the backing allocator, //! except where such operations would change the category from large to small. +const builtin = @import("builtin"); +const StackTrace = std.builtin.StackTrace; const std = @import("std"); -const builtin = @import("builtin"); const log = std.log.scoped(.gpa); const math = std.math; const assert = std.debug.assert; const mem = std.mem; const Allocator = std.mem.Allocator; -const StackTrace = std.builtin.StackTrace; const default_page_size: usize = switch (builtin.os.tag) { // Makes `std.heap.PageAllocator` take the happy path. @@ -421,7 +421,12 @@ pub fn DebugAllocator(comptime config: Config) type { return usedBitsCount(slot_count) * @sizeOf(usize); } - fn detectLeaksInBucket(bucket: *BucketHeader, size_class_index: usize, used_bits_count: usize) usize { + fn detectLeaksInBucket( + bucket: *BucketHeader, + size_class_index: usize, + used_bits_count: usize, + tty_config: std.Io.tty.Config, + ) usize { const size_class = @as(usize, 1) << @as(Log2USize, @intCast(size_class_index)); const slot_count = slot_counts[size_class_index]; var leaks: usize = 0; @@ -436,7 +441,13 @@ pub fn DebugAllocator(comptime config: Config) type { const stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc); const page_addr = @intFromPtr(bucket) & ~(page_size - 1); const addr = page_addr + slot_index * size_class; - log.err("memory address 0x{x} leaked: {f}", .{ addr, stack_trace }); + log.err("memory address 0x{x} leaked: {f}", .{ + addr, + std.debug.FormatStackTrace{ + .stack_trace = stack_trace, + .tty_config = tty_config, + }, + }); leaks += 1; } } @@ -449,12 +460,14 @@ pub fn DebugAllocator(comptime config: Config) type { pub fn detectLeaks(self: *Self) usize { var leaks: usize = 0; + const tty_config = std.Io.tty.detectConfig(.stderr()); + for (self.buckets, 0..) |init_optional_bucket, size_class_index| { var optional_bucket = init_optional_bucket; const slot_count = slot_counts[size_class_index]; const used_bits_count = usedBitsCount(slot_count); while (optional_bucket) |bucket| { - leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count); + leaks += detectLeaksInBucket(bucket, size_class_index, used_bits_count, tty_config); optional_bucket = bucket.prev; } } @@ -464,7 +477,11 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.retain_metadata and large_alloc.freed) continue; const stack_trace = large_alloc.getStackTrace(.alloc); log.err("memory address 0x{x} leaked: {f}", .{ - @intFromPtr(large_alloc.bytes.ptr), stack_trace, + @intFromPtr(large_alloc.bytes.ptr), + std.debug.FormatStackTrace{ + .stack_trace = stack_trace, + .tty_config = tty_config, + }, }); leaks += 1; } @@ -519,8 +536,20 @@ pub fn DebugAllocator(comptime config: Config) type { fn reportDoubleFree(ret_addr: usize, alloc_stack_trace: StackTrace, free_stack_trace: StackTrace) void { var addr_buf: [stack_n]usize = undefined; const second_free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Double free detected. Allocation: {f} First free: {f} Second free: {f}", .{ - alloc_stack_trace, free_stack_trace, second_free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = alloc_stack_trace, + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = second_free_stack_trace, + .tty_config = tty_config, + }, }); } @@ -561,11 +590,18 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.safety and old_mem.len != entry.value_ptr.bytes.len) { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ entry.value_ptr.bytes.len, old_mem.len, - entry.value_ptr.getStackTrace(.alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = entry.value_ptr.getStackTrace(.alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } @@ -667,11 +703,18 @@ pub fn DebugAllocator(comptime config: Config) type { if (config.safety and old_mem.len != entry.value_ptr.bytes.len) { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = ret_addr }, &addr_buf); + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ entry.value_ptr.bytes.len, old_mem.len, - entry.value_ptr.getStackTrace(.alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = entry.value_ptr.getStackTrace(.alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } @@ -892,19 +935,33 @@ pub fn DebugAllocator(comptime config: Config) type { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf); if (old_memory.len != requested_size) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ requested_size, old_memory.len, - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } if (alignment != slot_alignment) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{ slot_alignment.toByteUnits(), alignment.toByteUnits(), - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } } @@ -987,19 +1044,33 @@ pub fn DebugAllocator(comptime config: Config) type { var addr_buf: [stack_n]usize = undefined; const free_stack_trace = std.debug.captureCurrentStackTrace(.{ .first_address = return_address }, &addr_buf); if (memory.len != requested_size) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation size {d} bytes does not match free size {d}. Allocation: {f} Free: {f}", .{ requested_size, memory.len, - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } if (alignment != slot_alignment) { + const tty_config = std.Io.tty.detectConfig(.stderr()); log.err("Allocation alignment {d} does not match free alignment {d}. Allocation: {f} Free: {f}", .{ slot_alignment.toByteUnits(), alignment.toByteUnits(), - bucketStackTrace(bucket, slot_count, slot_index, .alloc), - free_stack_trace, + std.debug.FormatStackTrace{ + .stack_trace = bucketStackTrace(bucket, slot_count, slot_index, .alloc), + .tty_config = tty_config, + }, + std.debug.FormatStackTrace{ + .stack_trace = free_stack_trace, + .tty_config = tty_config, + }, }); } } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 1d49a890e4..fd6e2926c9 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -5,12 +5,14 @@ //! slices as well as APIs which accept null-terminated WTF16LE byte buffers. const builtin = @import("builtin"); +const native_arch = builtin.cpu.arch; + const std = @import("../std.zig"); +const Io = std.Io; const mem = std.mem; const assert = std.debug.assert; const math = std.math; const maxInt = std.math.maxInt; -const native_arch = builtin.cpu.arch; const UnexpectedError = std.posix.UnexpectedError; test { @@ -2219,25 +2221,25 @@ pub fn peb() *PEB { /// Universal Time (UTC). /// This function returns the number of nanoseconds since the canonical epoch, /// which is the POSIX one (Jan 01, 1970 AD). -pub fn fromSysTime(hns: i64) i128 { +pub fn fromSysTime(hns: i64) Io.Timestamp { const adjusted_epoch: i128 = hns + std.time.epoch.windows * (std.time.ns_per_s / 100); - return adjusted_epoch * 100; + return .fromNanoseconds(@intCast(adjusted_epoch * 100)); } -pub fn toSysTime(ns: i128) i64 { - const hns = @divFloor(ns, 100); +pub fn toSysTime(ns: Io.Timestamp) i64 { + const hns = @divFloor(ns.nanoseconds, 100); return @as(i64, @intCast(hns)) - std.time.epoch.windows * (std.time.ns_per_s / 100); } -pub fn fileTimeToNanoSeconds(ft: FILETIME) i128 { +pub fn fileTimeToNanoSeconds(ft: FILETIME) Io.Timestamp { const hns = (@as(i64, ft.dwHighDateTime) << 32) | ft.dwLowDateTime; return fromSysTime(hns); } /// Converts a number of nanoseconds since the POSIX epoch to a Windows FILETIME. -pub fn nanoSecondsToFileTime(ns: i128) FILETIME { +pub fn nanoSecondsToFileTime(ns: Io.Timestamp) FILETIME { const adjusted: u64 = @bitCast(toSysTime(ns)); - return FILETIME{ + return .{ .dwHighDateTime = @as(u32, @truncate(adjusted >> 32)), .dwLowDateTime = @as(u32, @truncate(adjusted)), }; @@ -5740,11 +5742,15 @@ pub fn ProcessBaseAddress(handle: HANDLE) ProcessBaseAddressError!HMODULE { return ppeb.ImageBaseAddress; } -pub fn checkWtf8ToWtf16LeOverflow(wtf8: []const u8, wtf16le: []const u16) error{ BadPathName, NameTooLong }!void { +pub fn wtf8ToWtf16Le(wtf16le: []u16, wtf8: []const u8) error{ BadPathName, NameTooLong }!usize { // Each u8 in UTF-8/WTF-8 correlates to at most one u16 in UTF-16LE/WTF-16LE. - if (wtf16le.len >= wtf8.len) return; - const utf16_len = std.unicode.calcUtf16LeLenImpl(wtf8, .can_encode_surrogate_half) catch - return error.BadPathName; - if (utf16_len > wtf16le.len) - return error.NameTooLong; + if (wtf16le.len < wtf8.len) { + const utf16_len = std.unicode.calcUtf16LeLenImpl(wtf8, .can_encode_surrogate_half) catch + return error.BadPathName; + if (utf16_len > wtf16le.len) + return error.NameTooLong; + } + return std.unicode.wtf8ToWtf16Le(wtf16le, wtf8) catch |err| switch (err) { + error.InvalidWtf8 => return error.BadPathName, + }; } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index a58204e1dc..02dc6c6087 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -821,6 +821,9 @@ pub const ReadError = std.Io.File.ReadStreamingError; /// The corresponding POSIX limit is `maxInt(isize)`. pub fn read(fd: fd_t, buf: []u8) ReadError!usize { if (buf.len == 0) return 0; + if (native_os == .windows) { + return windows.ReadFile(fd, buf, null); + } if (native_os == .wasi and !builtin.link_libc) { const iovs = [1]iovec{iovec{ .base = buf.ptr, @@ -2918,8 +2921,7 @@ pub fn chdir(dir_path: []const u8) ChangeCurDirError!void { @compileError("WASI does not support os.chdir"); } else if (native_os == .windows) { var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - try windows.checkWtf8ToWtf16LeOverflow(dir_path, &wtf16_dir_path); - const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); + const len = try windows.wtf8ToWtf16Le(&wtf16_dir_path, dir_path); return chdirW(wtf16_dir_path[0..len]); } else { const dir_path_c = try toPosixPath(dir_path); @@ -2935,8 +2937,7 @@ pub fn chdirZ(dir_path: [*:0]const u8) ChangeCurDirError!void { if (native_os == .windows) { const dir_path_span = mem.span(dir_path); var wtf16_dir_path: [windows.PATH_MAX_WIDE]u16 = undefined; - try windows.checkWtf8ToWtf16LeOverflow(dir_path_span, &wtf16_dir_path); - const len = try std.unicode.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); + const len = try windows.wtf8ToWtf16Le(&wtf16_dir_path, dir_path_span); return chdirW(wtf16_dir_path[0..len]); } else if (native_os == .wasi and !builtin.link_libc) { return chdir(mem.span(dir_path)); diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index e85f1d7471..3f4b11c1af 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -862,20 +862,6 @@ test "isatty" { try expectEqual(posix.isatty(file.handle), false); } -test "read with empty buffer" { - var tmp = tmpDir(.{}); - defer tmp.cleanup(); - - var file = try tmp.dir.createFile("read_empty", .{ .read = true }); - defer file.close(); - - const bytes = try a.alloc(u8, 0); - defer a.free(bytes); - - const rc = try posix.read(file.handle, bytes); - try expectEqual(rc, 0); -} - test "pread with empty buffer" { var tmp = tmpDir(.{}); defer tmp.cleanup(); diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 02ad99932e..7cef6f9c58 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -1148,6 +1148,7 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime } else |err| switch (err) { error.OutOfMemory => { if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) { + const tty_config = std.Io.tty.detectConfig(.stderr()); print( "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\nallocation that was made to fail: {f}", .{ @@ -1157,7 +1158,10 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime failing_allocator_inst.freed_bytes, failing_allocator_inst.allocations, failing_allocator_inst.deallocations, - failing_allocator_inst.getStackTrace(), + std.debug.FormatStackTrace{ + .stack_trace = failing_allocator_inst.getStackTrace(), + .tty_config = tty_config, + }, }, ); return error.MemoryLeakDetected; diff --git a/tools/incr-check.zig b/tools/incr-check.zig index 183d59bf88..4c5a2d9978 100644 --- a/tools/incr-check.zig +++ b/tools/incr-check.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; @@ -11,6 +12,12 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + const gpa = arena; + + var threaded: Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var opt_zig_exe: ?[]const u8 = null; var opt_input_file_name: ?[]const u8 = null; var opt_lib_dir: ?[]const u8 = null; @@ -53,7 +60,7 @@ pub fn main() !void { const input_file_name = opt_input_file_name orelse fatal("missing input file\n{s}", .{usage}); const input_file_bytes = try std.fs.cwd().readFileAlloc(input_file_name, arena, .limited(std.math.maxInt(u32))); - const case = try Case.parse(arena, input_file_bytes); + const case = try Case.parse(arena, io, input_file_bytes); // Check now: if there are any targets using the `cbe` backend, we need the lib dir. if (opt_lib_dir == null) { @@ -86,7 +93,7 @@ pub fn main() !void { else null; - const host = try std.zig.system.resolveTargetQuery(.{}); + const host = try std.zig.system.resolveTargetQuery(io, .{}); const debug_log_verbose = debug_zcu or debug_dwarf or debug_link; @@ -186,7 +193,7 @@ pub fn main() !void { try child.spawn(); - var poller = std.Io.poll(arena, Eval.StreamEnum, .{ + var poller = Io.poll(arena, Eval.StreamEnum, .{ .stdout = child.stdout.?, .stderr = child.stderr.?, }); @@ -226,7 +233,7 @@ const Eval = struct { cc_child_args: *std.ArrayListUnmanaged([]const u8), const StreamEnum = enum { stdout, stderr }; - const Poller = std.Io.Poller(StreamEnum); + const Poller = Io.Poller(StreamEnum); /// Currently this function assumes the previous updates have already been written. fn write(eval: *Eval, update: Case.Update) void { @@ -647,7 +654,7 @@ const Case = struct { msg: []const u8, }; - fn parse(arena: Allocator, bytes: []const u8) !Case { + fn parse(arena: Allocator, io: Io, bytes: []const u8) !Case { const fatal = std.process.fatal; var targets: std.ArrayListUnmanaged(Target) = .empty; @@ -683,7 +690,7 @@ const Case = struct { }, }) catch fatal("line {d}: invalid target query '{s}'", .{ line_n, query }); - const resolved = try std.zig.system.resolveTargetQuery(parsed_query); + const resolved = try std.zig.system.resolveTargetQuery(io, parsed_query); try targets.append(arena, .{ .query = query, From ed7747e90fcf6c26860788d4b0aafdaa07c8e068 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 20:42:43 -0700 Subject: [PATCH 145/244] std.Io.Threaded: add dirMake for Windows --- lib/std/Io/Threaded.zig | 26 ++++++++++++++++++++++++-- lib/std/posix.zig | 27 +++------------------------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 18e6e72563..d851ce30fe 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -164,7 +164,7 @@ pub fn io(t: *Threaded) Io { .conditionWake = conditionWake, .dirMake = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => dirMakeWindows, .wasi => dirMakeWasi, else => dirMakePosix, }, @@ -968,6 +968,28 @@ fn dirMakeWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: I } } +fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + _ = mode; + const sub_dir_handle = windows.OpenFile(sub_path_w.span(), .{ + .dir = dir.handle, + .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, + .creation = windows.FILE_CREATE, + .filter = .dir_only, + }) catch |err| switch (err) { + error.IsDir => return error.Unexpected, + error.PipeBusy => return error.Unexpected, + error.NoDevice => return error.Unexpected, + error.WouldBlock => return error.Unexpected, + error.AntivirusInterference => return error.Unexpected, + else => |e| return e, + }; + windows.CloseHandle(sub_dir_handle); +} + fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -3164,7 +3186,7 @@ fn netInterfaceNameResolve( if (native_os == .windows) { try t.checkCancel(); - const index = std.os.windows.ws2_32.if_nametoindex(&name.bytes); + const index = windows.ws2_32.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; return .{ .index = index }; } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 02dc6c6087..fd4958c5e8 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -2691,8 +2691,7 @@ pub fn renameatW( /// On other platforms, `sub_dir_path` is an opaque sequence of bytes with no particular encoding. pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirError!void { if (native_os == .windows) { - const sub_dir_path_w = try windows.sliceToPrefixedFileW(dir_fd, sub_dir_path); - return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { @compileError("use std.Io instead"); } else { @@ -2704,10 +2703,9 @@ pub fn mkdirat(dir_fd: fd_t, sub_dir_path: []const u8, mode: mode_t) MakeDirErro /// Same as `mkdirat` except the parameters are null-terminated. pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDirError!void { if (native_os == .windows) { - const sub_dir_path_w = try windows.cStrToPrefixedFileW(dir_fd, sub_dir_path); - return mkdiratW(dir_fd, sub_dir_path_w.span(), mode); + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { - return mkdirat(dir_fd, mem.sliceTo(sub_dir_path, 0), mode); + @compileError("use std.Io instead"); } switch (errno(system.mkdirat(dir_fd, sub_dir_path, mode))) { .SUCCESS => return, @@ -2732,25 +2730,6 @@ pub fn mkdiratZ(dir_fd: fd_t, sub_dir_path: [*:0]const u8, mode: mode_t) MakeDir } } -/// Windows-only. Same as `mkdirat` except the parameter WTF16 LE encoded. -pub fn mkdiratW(dir_fd: fd_t, sub_path_w: []const u16, mode: mode_t) MakeDirError!void { - _ = mode; - const sub_dir_handle = windows.OpenFile(sub_path_w, .{ - .dir = dir_fd, - .access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE, - .creation = windows.FILE_CREATE, - .filter = .dir_only, - }) catch |err| switch (err) { - error.IsDir => return error.Unexpected, - error.PipeBusy => return error.Unexpected, - error.NoDevice => return error.Unexpected, - error.WouldBlock => return error.Unexpected, - error.AntivirusInterference => return error.Unexpected, - else => |e| return e, - }; - windows.CloseHandle(sub_dir_handle); -} - pub const MakeDirError = std.Io.Dir.MakeError; /// Create a directory. From f98352eecf1db704b0e59ced1c6741734c3990ac Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 20:44:06 -0700 Subject: [PATCH 146/244] std.debug.SelfInfo: add missing io parameter to getSymbol --- lib/std/debug/SelfInfo/Elf.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/std/debug/SelfInfo/Elf.zig b/lib/std/debug/SelfInfo/Elf.zig index bf8330d235..21319b01d4 100644 --- a/lib/std/debug/SelfInfo/Elf.zig +++ b/lib/std/debug/SelfInfo/Elf.zig @@ -28,7 +28,8 @@ pub fn deinit(si: *SelfInfo, gpa: Allocator) void { if (si.unwind_cache) |cache| gpa.free(cache); } -pub fn getSymbol(si: *SelfInfo, gpa: Allocator, address: usize) Error!std.debug.Symbol { +pub fn getSymbol(si: *SelfInfo, gpa: Allocator, io: Io, address: usize) Error!std.debug.Symbol { + _ = io; const module = try si.findModule(gpa, address, .exclusive); defer si.rwlock.unlock(); @@ -489,6 +490,7 @@ const DlIterContext = struct { }; const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; const Dwarf = std.debug.Dwarf; const Error = std.debug.SelfInfoError; From 482343f2e253f2078627e696fb98ff0e1d8e82df Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 20:48:24 -0700 Subject: [PATCH 147/244] std.Io.Threaded: implement dirStatPath for Windows --- lib/std/Io/Threaded.zig | 17 ++++++++++++++++- lib/std/fs/Dir.zig | 5 ----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index d851ce30fe..4f2fe5e7b1 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -171,7 +171,7 @@ pub fn io(t: *Threaded) Io { .dirStat = dirStat, .dirStatPath = switch (builtin.os.tag) { .linux => dirStatPathLinux, - .windows => @panic("TODO"), + .windows => dirStatPathWindows, .wasi => dirStatPathWasi, else => dirStatPathPosix, }, @@ -1079,6 +1079,21 @@ fn dirStatPathPosix( } } +fn dirStatPathWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = t.io(); + var file = try dir.openFile(t_io, sub_path, .{ + .follow_symlinks = options.follow_symlinks, + }); + defer file.close(t_io); + return file.stat(t_io); +} + fn dirStatPathWasi( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index aa1180a90d..2eb85928f6 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -2332,11 +2332,6 @@ pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError /// Deprecated in favor of `Io.Dir.statPath`. pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { - if (native_os == .windows) { - var file = try self.openFile(sub_path, .{}); - defer file.close(); - return file.stat(); - } var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{}); From aa6e8eff40bdf35c838c8cd93080f2e9d4fad3b2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 21:12:19 -0700 Subject: [PATCH 148/244] std.Io.Threaded: implement dirAccess for Windows --- lib/std/Io/Threaded.zig | 51 ++++++++++++++++++++++++++++++++++++++--- lib/std/fs.zig | 2 +- lib/std/fs/path.zig | 2 +- lib/std/os/windows.zig | 19 ++++++++++----- lib/std/posix.zig | 4 ++-- lib/std/start.zig | 2 +- 6 files changed, 66 insertions(+), 14 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 4f2fe5e7b1..93d4cc1b42 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -182,7 +182,7 @@ pub fn io(t: *Threaded) Io { else => fileStatPosix, }, .dirAccess = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => dirAccessWindows, .wasi => dirAccessWasi, else => dirAccessPosix, }, @@ -1315,6 +1315,51 @@ fn dirAccessWasi( return error.AccessDenied; } +fn dirAccessWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.AccessOptions, +) Io.Dir.AccessError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + _ = options; // TODO + + const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + if (sub_path_w[0] == '.' and sub_path_w[1] == 0) return; + if (sub_path_w[0] == '.' and sub_path_w[1] == '.' and sub_path_w[2] == 0) return; + + const path_len_bytes = std.math.cast(u16, std.mem.sliceTo(sub_path_w, 0).len * 2) orelse + return error.NameTooLong; + var nt_name: windows.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr = windows.OBJECT_ATTRIBUTES{ + .Length = @sizeOf(windows.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + var basic_info: windows.FILE_BASIC_INFORMATION = undefined; + switch (windows.ntdll.NtQueryAttributesFile(&attr, &basic_info)) { + .SUCCESS => return, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .OBJECT_NAME_INVALID => |err| return windows.statusBug(err), + .INVALID_PARAMETER => |err| return windows.statusBug(err), + .ACCESS_DENIED => return error.AccessDenied, + .OBJECT_PATH_SYNTAX_BAD => |err| return windows.statusBug(err), + else => |rc| return windows.unexpectedStatus(rc), + } +} + fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -1818,7 +1863,7 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File var n: DWORD = undefined; if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) == 0) { switch (windows.GetLastError()) { - .IO_PENDING => unreachable, + .IO_PENDING => |err| return windows.statusBug(err), .OPERATION_ABORTED => continue, .BROKEN_PIPE => return 0, .HANDLE_EOF => return 0, @@ -1935,7 +1980,7 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset } else null; if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, overlapped) == 0) { switch (windows.GetLastError()) { - .IO_PENDING => unreachable, + .IO_PENDING => |err| return windows.statusBug(err), .OPERATION_ABORTED => continue, .BROKEN_PIPE => return 0, .HANDLE_EOF => return 0, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 7fae26cb62..01188e29a3 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -263,7 +263,7 @@ pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) File.O /// Same as `openFileAbsolute` but the path parameter is WTF-16-encoded. pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { - assert(path.isAbsoluteWindowsWTF16(absolute_path_w)); + assert(path.isAbsoluteWindowsWtf16(absolute_path_w)); return cwd().openFileW(absolute_path_w, flags); } diff --git a/lib/std/fs/path.zig b/lib/std/fs/path.zig index ff69c21cdd..966c481c23 100644 --- a/lib/std/fs/path.zig +++ b/lib/std/fs/path.zig @@ -313,7 +313,7 @@ pub fn isAbsoluteWindowsW(path_w: [*:0]const u16) bool { return isAbsoluteWindowsImpl(u16, mem.sliceTo(path_w, 0)); } -pub fn isAbsoluteWindowsWTF16(path: []const u16) bool { +pub fn isAbsoluteWindowsWtf16(path: []const u16) bool { return isAbsoluteWindowsImpl(u16, path); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index fd6e2926c9..f1649fafc1 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -89,7 +89,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN }; var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else options.dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir, .Attributes = if (options.sa) |ptr| blk: { // Note we do not use OBJ_CASE_INSENSITIVE here. const inherit: ULONG = if (ptr.bInheritHandle == TRUE) OBJ_INHERIT else 0; break :blk inherit; @@ -847,7 +847,7 @@ pub fn CreateSymbolicLink( // the C:\ drive. .rooted => break :target_path target_path, // Keep relative paths relative, but anything else needs to get NT-prefixed. - else => if (!std.fs.path.isAbsoluteWindowsWTF16(target_path)) + else => if (!std.fs.path.isAbsoluteWindowsWtf16(target_path)) break :target_path target_path, }, // Already an NT path, no need to do anything to it @@ -856,7 +856,7 @@ pub fn CreateSymbolicLink( } var prefixed_target_path = try wToPrefixedFileW(dir, target_path); // We do this after prefixing to ensure that drive-relative paths are treated as absolute - is_target_absolute = std.fs.path.isAbsoluteWindowsWTF16(prefixed_target_path.span()); + is_target_absolute = std.fs.path.isAbsoluteWindowsWtf16(prefixed_target_path.span()); break :target_path prefixed_target_path.span(); }; @@ -864,7 +864,7 @@ pub fn CreateSymbolicLink( var buffer: [MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; const buf_len = @sizeOf(SYMLINK_DATA) + final_target_path.len * 4; const header_len = @sizeOf(ULONG) + @sizeOf(USHORT) * 2; - const target_is_absolute = std.fs.path.isAbsoluteWindowsWTF16(final_target_path); + const target_is_absolute = std.fs.path.isAbsoluteWindowsWtf16(final_target_path); const symlink_data = SYMLINK_DATA{ .ReparseTag = IO_REPARSE_TAG_SYMLINK, .ReparseDataLength = @intCast(buf_len - header_len), @@ -905,7 +905,7 @@ pub fn ReadLink(dir: ?HANDLE, sub_path_w: []const u16, out_buffer: []u8) ReadLin }; var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir, .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -1035,7 +1035,7 @@ pub fn DeleteFile(sub_path_w: []const u16, options: DeleteFileOptions) DeleteFil var attr = OBJECT_ATTRIBUTES{ .Length = @sizeOf(OBJECT_ATTRIBUTES), - .RootDirectory = if (std.fs.path.isAbsoluteWindowsWTF16(sub_path_w)) null else options.dir, + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else options.dir, .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. .ObjectName = &nt_name, .SecurityDescriptor = null, @@ -2885,6 +2885,13 @@ pub fn unexpectedStatus(status: NTSTATUS) UnexpectedError { return error.Unexpected; } +pub fn statusBug(status: NTSTATUS) UnexpectedError { + switch (builtin.mode) { + .Debug => std.debug.panic("programmer bug caused syscall status: {t}", .{status}), + else => return error.Unexpected, + } +} + pub const Win32Error = @import("windows/win32error.zig").Win32Error; pub const NTSTATUS = @import("windows/ntstatus.zig").NTSTATUS; pub const LANG = @import("windows/lang.zig"); diff --git a/lib/std/posix.zig b/lib/std/posix.zig index fd4958c5e8..a186ac7074 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -2617,7 +2617,7 @@ pub fn renameatW( if (ReplaceIfExists == windows.TRUE) flags |= windows.FILE_RENAME_REPLACE_IF_EXISTS; rename_info.* = .{ .Flags = flags, - .RootDirectory = if (fs.path.isAbsoluteWindowsWTF16(new_path_w)) null else new_dir_fd, + .RootDirectory = if (fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd, .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong .FileName = undefined, }; @@ -2654,7 +2654,7 @@ pub fn renameatW( rename_info.* = .{ .Flags = ReplaceIfExists, - .RootDirectory = if (fs.path.isAbsoluteWindowsWTF16(new_path_w)) null else new_dir_fd, + .RootDirectory = if (fs.path.isAbsoluteWindowsWtf16(new_path_w)) null else new_dir_fd, .FileNameLength = @intCast(new_path_w.len * 2), // already checked error.NameTooLong .FileName = undefined, }; diff --git a/lib/std/start.zig b/lib/std/start.zig index 09c1f3b5c4..4063b2027b 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -708,7 +708,7 @@ pub inline fn callMain() u8 { switch (native_os) { .freestanding, .other => {}, else => if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace); + std.debug.dumpStackTrace(trace.*); }, } return 1; From b561d5f3feda6b1251fc2b8ab2430e78f546577c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 21:27:16 -0700 Subject: [PATCH 149/244] std.Io.Threaded: implement dirCreateFile for Windows --- lib/std/Io/Dir.zig | 7 ++++++ lib/std/Io/Threaded.zig | 50 ++++++++++++++++++++++++++++++++++++++- lib/std/fs.zig | 6 ----- lib/std/fs/Dir.zig | 52 +---------------------------------------- lib/std/posix.zig | 8 ++----- 5 files changed, 59 insertions(+), 64 deletions(-) diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 749bea99ba..966ef77bcb 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -101,6 +101,13 @@ pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) F return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } +/// Creates, opens, or overwrites a file with write access. +/// +/// Allocates a resource to be dellocated with `File.close`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. pub fn createFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { return io.vtable.dirCreateFile(io.userdata, dir, sub_path, flags); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 93d4cc1b42..5fb6b781c1 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -187,7 +187,7 @@ pub fn io(t: *Threaded) Io { else => dirAccessPosix, }, .dirCreateFile = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => dirCreateFileWindows, .wasi => dirCreateFileWasi, else => dirCreateFilePosix, }, @@ -1483,6 +1483,54 @@ fn dirCreateFilePosix( return .{ .handle = fd }; } +fn dirCreateFileWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.CreateFlags, +) Io.File.OpenError!Io.File { + const w = windows; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; + const handle = try w.OpenFile(sub_path_w, .{ + .dir = dir.handle, + .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, + .creation = if (flags.exclusive) + @as(u32, w.FILE_CREATE) + else if (flags.truncate) + @as(u32, w.FILE_OVERWRITE_IF) + else + @as(u32, w.FILE_OPEN_IF), + }); + errdefer w.CloseHandle(handle); + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const range_off: w.LARGE_INTEGER = 0; + const range_len: w.LARGE_INTEGER = 1; + const exclusive = switch (flags.lock) { + .none => return .{ .handle = handle }, + .shared => false, + .exclusive => true, + }; + try w.LockFile( + handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + @intFromBool(flags.lock_nonblocking), + @intFromBool(exclusive), + ); + return .{ .handle = handle }; +} + fn dirCreateFileWasi( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 01188e29a3..ac4d933d32 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -299,12 +299,6 @@ pub fn createFileAbsolute(absolute_path: []const u8, flags: File.CreateFlags) Fi return cwd().createFile(absolute_path, flags); } -/// Same as `createFileAbsolute` but the path parameter is WTF-16 encoded. -pub fn createFileAbsoluteW(absolute_path_w: [*:0]const u16, flags: File.CreateFlags) File.OpenError!File { - assert(path.isAbsoluteWindowsW(absolute_path_w)); - return cwd().createFileW(mem.span(absolute_path_w), flags); -} - /// Delete a file name and possibly the file it refers to, based on an absolute path. /// Asserts that the path is absolute. See `Dir.deleteFile` for a function that /// operates on both absolute and relative paths. diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 2eb85928f6..ecd713c01a 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -900,64 +900,14 @@ pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File return file; } -/// Creates, opens, or overwrites a file with write access. -/// Call `File.close` on the result when done. -/// Asserts that the path parameter has no null bytes. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.createFile`. pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.createFileW(path_w.span(), flags); - } var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags); return .adaptFromNewApi(new_file); } -/// Same as `createFile` but Windows-only and the path parameter is -/// [WTF-16](https://wtf-8.codeberg.page/#potentially-ill-formed-utf-16) encoded. -pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { - const w = windows; - const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; - const file: File = .{ - .handle = try w.OpenFile(sub_path_w, .{ - .dir = self.fd, - .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, - .creation = if (flags.exclusive) - @as(u32, w.FILE_CREATE) - else if (flags.truncate) - @as(u32, w.FILE_OVERWRITE_IF) - else - @as(u32, w.FILE_OPEN_IF), - }), - }; - errdefer file.close(); - var io: w.IO_STATUS_BLOCK = undefined; - const range_off: w.LARGE_INTEGER = 0; - const range_len: w.LARGE_INTEGER = 1; - const exclusive = switch (flags.lock) { - .none => return file, - .shared => false, - .exclusive => true, - }; - try w.LockFile( - file.handle, - null, - null, - null, - &io, - &range_off, - &range_len, - null, - @intFromBool(flags.lock_nonblocking), - @intFromBool(exclusive), - ); - return file; -} - /// Deprecated in favor of `Io.Dir.MakeError`. pub const MakeError = Io.Dir.MakeError; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index a186ac7074..4473faf328 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -4684,9 +4684,7 @@ pub const AccessError = error{ /// Windows. See `fs` for the cross-platform file system API. pub fn access(path: []const u8, mode: u32) AccessError!void { if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(null, path); - _ = try windows.GetFileAttributesW(path_w.span().ptr); - return; + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { @compileError("wasi doesn't support absolute paths"); } @@ -4697,9 +4695,7 @@ pub fn access(path: []const u8, mode: u32) AccessError!void { /// Same as `access` except `path` is null-terminated. pub fn accessZ(path: [*:0]const u8, mode: u32) AccessError!void { if (native_os == .windows) { - const path_w = try windows.cStrToPrefixedFileW(null, path); - _ = try windows.GetFileAttributesW(path_w.span().ptr); - return; + @compileError("use std.Io instead"); } else if (native_os == .wasi and !builtin.link_libc) { return access(mem.sliceTo(path, 0), mode); } From 1c67607397be4efa962d64a1d80b7fd91fe161eb Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 21:46:57 -0700 Subject: [PATCH 150/244] std.Io.Threaded: implement dirOpenFile for Windows --- lib/std/Io/Dir.zig | 9 +++++++ lib/std/Io/Threaded.zig | 46 ++++++++++++++++++++++++++++++++- lib/std/fs.zig | 56 ----------------------------------------- lib/std/fs/Dir.zig | 49 +----------------------------------- 4 files changed, 55 insertions(+), 105 deletions(-) diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 966ef77bcb..ab8f75e541 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -97,6 +97,15 @@ pub fn close(dir: Dir, io: Io) void { return io.vtable.dirClose(io.userdata, dir); } +/// Opens a file for reading or writing, without attempting to create a new file. +/// +/// To create a new file, see `createFile`. +/// +/// Allocates a resource to be released with `File.close`. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { return io.vtable.dirOpenFile(io.userdata, dir, sub_path, flags); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 5fb6b781c1..7f78c617a5 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -192,7 +192,7 @@ pub fn io(t: *Threaded) Io { else => dirCreateFilePosix, }, .dirOpenFile = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => dirOpenFileWindows, .wasi => dirOpenFileWasi, else => dirOpenFilePosix, }, @@ -1731,6 +1731,50 @@ fn dirOpenFilePosix( return .{ .handle = fd }; } +fn dirOpenFileWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const w = windows; + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + const handle = try w.OpenFile(sub_path_w, .{ + .dir = dir.handle, + .access_mask = w.SYNCHRONIZE | + (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | + (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), + .creation = w.FILE_OPEN, + }); + errdefer w.CloseHandle(handle); + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const range_off: w.LARGE_INTEGER = 0; + const range_len: w.LARGE_INTEGER = 1; + const exclusive = switch (flags.lock) { + .none => return .{ .handle = handle }, + .shared => false, + .exclusive => true, + }; + try w.LockFile( + handle, + null, + null, + null, + &io_status_block, + &range_off, + &range_len, + null, + @intFromBool(flags.lock_nonblocking), + @intFromBool(exclusive), + ); + return .{ .handle = handle }; +} + fn dirOpenFileWasi( userdata: ?*anyopaque, dir: Io.Dir, diff --git a/lib/std/fs.zig b/lib/std/fs.zig index ac4d933d32..25018ddaa8 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -138,12 +138,6 @@ pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { test makeDirAbsoluteZ {} -/// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16 LE-encoded string. -pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { - assert(path.isAbsoluteWindowsW(absolute_path_w)); - return posix.mkdirW(mem.span(absolute_path_w), Dir.default_mode); -} - /// Same as `Dir.deleteDir` except the path is absolute. /// On Windows, `dir_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, `dir_path` should be encoded as valid UTF-8. @@ -159,12 +153,6 @@ pub fn deleteDirAbsoluteZ(dir_path: [*:0]const u8) !void { return posix.rmdirZ(dir_path); } -/// Same as `deleteDirAbsolute` except the path parameter is WTF-16 and target OS is assumed Windows. -pub fn deleteDirAbsoluteW(dir_path: [*:0]const u16) !void { - assert(path.isAbsoluteWindowsW(dir_path)); - return posix.rmdirW(mem.span(dir_path)); -} - /// Same as `Dir.rename` except the paths are absolute. /// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). /// On WASI, both paths should be encoded as valid UTF-8. @@ -182,13 +170,6 @@ pub fn renameAbsoluteZ(old_path: [*:0]const u8, new_path: [*:0]const u8) !void { return posix.renameZ(old_path, new_path); } -/// Same as `renameAbsolute` except the path parameters are WTF-16 and target OS is assumed Windows. -pub fn renameAbsoluteW(old_path: [*:0]const u16, new_path: [*:0]const u16) !void { - assert(path.isAbsoluteWindowsW(old_path)); - assert(path.isAbsoluteWindowsW(new_path)); - return posix.renameW(old_path, new_path); -} - /// Same as `Dir.rename`, except `new_sub_path` is relative to `new_dir` pub fn rename(old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) !void { return posix.renameat(old_dir.fd, old_sub_path, new_dir.fd, new_sub_path); @@ -199,12 +180,6 @@ pub fn renameZ(old_dir: Dir, old_sub_path_z: [*:0]const u8, new_dir: Dir, new_su return posix.renameatZ(old_dir.fd, old_sub_path_z, new_dir.fd, new_sub_path_z); } -/// Same as `rename` except the parameters are WTF16LE, NT prefixed. -/// This function is Windows-only. -pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_path_w: []const u16) !void { - return posix.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w, windows.TRUE); -} - /// Returns a handle to the current working directory. It is not opened with iteration capability. /// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior. /// On POSIX targets, this function is comptime-callable. @@ -241,12 +216,6 @@ pub fn openDirAbsoluteZ(absolute_path_c: [*:0]const u8, flags: Dir.OpenOptions) assert(path.isAbsoluteZ(absolute_path_c)); return cwd().openDirZ(absolute_path_c, flags); } -/// Same as `openDirAbsolute` but the path parameter is null-terminated. -pub fn openDirAbsoluteW(absolute_path_c: [*:0]const u16, flags: Dir.OpenOptions) File.OpenError!Dir { - assert(path.isAbsoluteWindowsW(absolute_path_c)); - return cwd().openDirW(absolute_path_c, flags); -} - /// Opens a file for reading or writing, without attempting to create a new file, based on an absolute path. /// Call `File.close` to release the resource. /// Asserts that the path is absolute. See `Dir.openFile` for a function that @@ -261,12 +230,6 @@ pub fn openFileAbsolute(absolute_path: []const u8, flags: File.OpenFlags) File.O return cwd().openFile(absolute_path, flags); } -/// Same as `openFileAbsolute` but the path parameter is WTF-16-encoded. -pub fn openFileAbsoluteW(absolute_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { - assert(path.isAbsoluteWindowsWtf16(absolute_path_w)); - return cwd().openFileW(absolute_path_w, flags); -} - /// Test accessing `path`. /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. /// For example, instead of testing if a file exists and then opening it, just @@ -279,12 +242,6 @@ pub fn accessAbsolute(absolute_path: []const u8, flags: Io.Dir.AccessOptions) Di assert(path.isAbsolute(absolute_path)); try cwd().access(absolute_path, flags); } -/// Same as `accessAbsolute` but the path parameter is WTF-16 encoded. -pub fn accessAbsoluteW(absolute_path: [*:0]const u16, flags: File.OpenFlags) Dir.AccessError!void { - assert(path.isAbsoluteWindowsW(absolute_path)); - try cwd().accessW(absolute_path, flags); -} - /// Creates, opens, or overwrites a file with write access, based on an absolute path. /// Call `File.close` to release the resource. /// Asserts that the path is absolute. See `Dir.createFile` for a function that @@ -311,12 +268,6 @@ pub fn deleteFileAbsolute(absolute_path: []const u8) Dir.DeleteFileError!void { return cwd().deleteFile(absolute_path); } -/// Same as `deleteFileAbsolute` except the parameter is WTF-16 encoded. -pub fn deleteFileAbsoluteW(absolute_path_w: [*:0]const u16) Dir.DeleteFileError!void { - assert(path.isAbsoluteWindowsW(absolute_path_w)); - return cwd().deleteFileW(mem.span(absolute_path_w)); -} - /// Removes a symlink, file, or directory. /// This is equivalent to `Dir.deleteTree` with the base directory. /// Asserts that the path is absolute. See `Dir.deleteTree` for a function that @@ -348,13 +299,6 @@ pub fn readLinkAbsolute(pathname: []const u8, buffer: *[max_path_bytes]u8) ![]u8 return posix.readlink(pathname, buffer); } -/// Windows-only. Same as `readlinkW`, except the path parameter is null-terminated, WTF16 -/// encoded. -pub fn readlinkAbsoluteW(pathname_w: [*:0]const u16, buffer: *[max_path_bytes]u8) ![]u8 { - assert(path.isAbsoluteWindowsW(pathname_w)); - return posix.readlinkW(mem.span(pathname_w), buffer); -} - /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent /// one; the latter case is known as a dangling link. diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index ecd713c01a..1b2182f0f7 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -846,60 +846,13 @@ pub fn close(self: *Dir) void { self.* = undefined; } -/// Opens a file for reading or writing, without attempting to create a new file. -/// To create a new file, see `createFile`. -/// Call `File.close` to release the resource. -/// Asserts that the path parameter has no null bytes. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +/// Deprecated in favor of `Io.Dir.openFile`. pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { - if (native_os == .windows) { - const path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openFileW(path_w.span(), flags); - } var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags)); } -/// Same as `openFile` but Windows-only and the path parameter is -/// [WTF-16](https://wtf-8.codeberg.page/#potentially-ill-formed-utf-16) encoded. -pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { - const w = windows; - const file: File = .{ - .handle = try w.OpenFile(sub_path_w, .{ - .dir = self.fd, - .access_mask = w.SYNCHRONIZE | - (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | - (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), - .creation = w.FILE_OPEN, - }), - }; - errdefer file.close(); - var io: w.IO_STATUS_BLOCK = undefined; - const range_off: w.LARGE_INTEGER = 0; - const range_len: w.LARGE_INTEGER = 1; - const exclusive = switch (flags.lock) { - .none => return file, - .shared => false, - .exclusive => true, - }; - try w.LockFile( - file.handle, - null, - null, - null, - &io, - &range_off, - &range_len, - null, - @intFromBool(flags.lock_nonblocking), - @intFromBool(exclusive), - ); - return file; -} - /// Deprecated in favor of `Io.Dir.createFile`. pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { var threaded: Io.Threaded = .init_single_threaded; From 6d1b2c7f64fd1a1c671f357cff96b3dd39612857 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 22:17:45 -0700 Subject: [PATCH 151/244] std.Io: introduce openSelfExe --- lib/std/Io.zig | 1 + lib/std/Io/Dir.zig | 17 ++++++++++++++++- lib/std/Io/File.zig | 6 ++++++ lib/std/Io/Threaded.zig | 21 +++++++++++++++++++++ lib/std/debug/SelfInfo/Windows.zig | 23 ++++++++++------------- lib/std/fs.zig | 22 +++++++--------------- 6 files changed, 61 insertions(+), 29 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index a6507df618..b16661be97 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -678,6 +678,7 @@ pub const VTable = struct { fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void, fileSeekTo: *const fn (?*anyopaque, File, absolute_offset: u64) File.SeekError!void, + openSelfExe: *const fn (?*anyopaque, File.OpenFlags) File.OpenSelfExeError!File, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index ab8f75e541..7336ab24af 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -1,5 +1,8 @@ const Dir = @This(); +const builtin = @import("builtin"); +const native_os = builtin.os.tag; + const std = @import("../std.zig"); const Io = std.Io; const File = Io.File; @@ -9,8 +12,20 @@ handle: Handle, pub const Mode = Io.File.Mode; pub const default_mode: Mode = 0o755; +/// Returns a handle to the current working directory. +/// +/// It is not opened with iteration capability. Iterating over the result is +/// illegal behavior. +/// +/// Closing the returned `Dir` is checked illegal behavior. +/// +/// On POSIX targets, this function is comptime-callable. pub fn cwd() Dir { - return .{ .handle = std.fs.cwd().fd }; + return switch (native_os) { + .windows => .{ .handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle }, + .wasi => .{ .handle = std.options.wasiCwd() }, + else => .{ .handle = std.posix.AT.FDCWD }, + }; } pub const Handle = std.posix.fd_t; diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 51f3a08df7..29500ffb1d 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -207,6 +207,12 @@ pub fn close(file: File, io: Io) void { return io.vtable.fileClose(io.userdata, file); } +pub const OpenSelfExeError = OpenError || std.fs.SelfExePathError || std.posix.FlockError; + +pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File { + return io.vtable.openSelfExe(io.userdata, flags); +} + pub const ReadStreamingError = error{ InputOutput, SystemResources, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 7f78c617a5..174d4b76f3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -209,6 +209,7 @@ pub fn io(t: *Threaded) Io { .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, + .openSelfExe = openSelfExe, .now = switch (builtin.os.tag) { .windows => nowWindows, @@ -2241,6 +2242,26 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr } } +fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelfExeError!Io.File { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + if (native_os == .linux or native_os == .serenity) { + return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags); + } + if (is_windows) { + // If ImagePathName is a symlink, then it will contain the path of the symlink, + // not the path that the symlink points to. However, because we are opening + // the file, we can let the openFileW call follow the symlink for us. + const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; + const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; + const prefixed_path_w_array = try windows.wToPrefixedFileW(null, image_path_name); + const prefixed_path_w = prefixed_path_w_array.span(); + const cwd_handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle; + + return dirOpenFileWindows(t, .{ .handle = cwd_handle }, prefixed_path_w, flags); + } + @panic("TODO"); +} + fn fileWritePositional( userdata: ?*anyopaque, file: Io.File, diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index a0b26f8ec5..51c41030dc 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -297,17 +297,7 @@ const Module = struct { // a binary is produced with -gdwarf, since the section names are longer than 8 bytes. const mapped_file: ?DebugInfo.MappedFile = mapped: { if (!coff_obj.strtabRequired()) break :mapped null; - var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined; - name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present - const process_handle = windows.GetCurrentProcess(); - const len = windows.kernel32.GetModuleFileNameExW( - process_handle, - module.handle, - name_buffer[4..], - windows.PATH_MAX_WIDE, - ); - if (len == 0) return error.MissingDebugInfo; - const coff_file = fs.openFileAbsoluteW(name_buffer[0 .. len + 4 :0], .{}) catch |err| switch (err) { + const coff_file = Io.File.openSelfExe(io, .{}) catch |err| switch (err) { error.Canceled => |e| return e, error.Unexpected => |e| return e, error.FileNotFound => return error.MissingDebugInfo, @@ -337,9 +327,15 @@ const Module = struct { error.SystemFdQuotaExceeded, error.FileLocksNotSupported, error.FileBusy, + error.InputOutput, + error.NotSupported, + error.FileSystem, + error.NotLink, + error.UnrecognizedVolume, + error.UnknownName, => return error.ReadFailed, }; - errdefer coff_file.close(); + errdefer coff_file.close(io); var section_handle: windows.HANDLE = undefined; const create_section_rc = windows.ntdll.NtCreateSection( §ion_handle, @@ -356,6 +352,7 @@ const Module = struct { errdefer windows.CloseHandle(section_handle); var coff_len: usize = 0; var section_view_ptr: ?[*]const u8 = null; + const process_handle = windows.GetCurrentProcess(); const map_section_rc = windows.ntdll.NtMapViewOfSection( section_handle, process_handle, @@ -373,7 +370,7 @@ const Module = struct { const section_view = section_view_ptr.?[0..coff_len]; coff_obj = coff.Coff.init(section_view, false) catch return error.InvalidDebugInfo; break :mapped .{ - .file = coff_file, + .file = .adaptFromNewApi(coff_file), .section_handle = section_handle, .section_view = section_view, }; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 25018ddaa8..ea39ce8e33 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -180,9 +180,7 @@ pub fn renameZ(old_dir: Dir, old_sub_path_z: [*:0]const u8, new_dir: Dir, new_su return posix.renameatZ(old_dir.fd, old_sub_path_z, new_dir.fd, new_sub_path_z); } -/// Returns a handle to the current working directory. It is not opened with iteration capability. -/// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior. -/// On POSIX targets, this function is comptime-callable. +/// Deprecated in favor of `Io.Dir.cwd`. pub fn cwd() Dir { if (native_os == .windows) { return .{ .fd = windows.peb().ProcessParameters.CurrentDirectory.Handle }; @@ -336,20 +334,14 @@ pub fn symLinkAbsoluteW( return windows.CreateSymbolicLink(null, mem.span(sym_link_path_w), mem.span(target_path_w), flags.is_directory); } -pub const OpenSelfExeError = posix.OpenError || SelfExePathError || posix.FlockError; +pub const OpenSelfExeError = Io.File.OpenSelfExeError; +/// Deprecated in favor of `Io.File.openSelfExe`. pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { - if (native_os == .linux or native_os == .serenity) { - return openFileAbsolute("/proc/self/exe", flags); - } - if (native_os == .windows) { - // If ImagePathName is a symlink, then it will contain the path of the symlink, - // not the path that the symlink points to. However, because we are opening - // the file, we can let the openFileW call follow the symlink for us. - const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; - const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); - return cwd().openFileW(prefixed_path_w.span(), flags); + if (native_os == .linux or native_os == .serenity or native_os == .windows) { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.File.openSelfExe(io, flags)); } // Use of max_path_bytes here is valid as the resulting path is immediately // opened with no modification. From 97bde94e360cc95206edca478a9dbbc5a0e75264 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 22:23:41 -0700 Subject: [PATCH 152/244] compiler: upgrade unit tests to new API --- src/Package/Fetch.zig | 18 +++++++--- src/Package/Fetch/git.zig | 69 +++++++++++++++++++++------------------ 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index 08a49da30a..46be6fa069 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -2069,6 +2069,7 @@ test "tarball with duplicate paths" { // const gpa = std.testing.allocator; + const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2079,7 +2080,7 @@ test "tarball with duplicate paths" { // Run tarball fetch, expect to fail var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try std.testing.expectError(error.FetchFailed, fetch.run()); @@ -2101,6 +2102,7 @@ test "tarball with excluded duplicate paths" { // const gpa = std.testing.allocator; + const io = std.testing.io; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2111,7 +2113,7 @@ test "tarball with excluded duplicate paths" { // Run tarball fetch, should succeed var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -2145,6 +2147,8 @@ test "tarball without root folder" { // const gpa = std.testing.allocator; + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2155,7 +2159,7 @@ test "tarball without root folder" { // Run tarball fetch, should succeed var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -2176,6 +2180,8 @@ test "tarball without root folder" { test "set executable bit based on file content" { if (!std.fs.has_executable_bit) return error.SkipZigTest; const gpa = std.testing.allocator; + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); @@ -2194,7 +2200,7 @@ test "set executable bit based on file content" { // -rwxrwxr-x 17 executables/script var fb: TestFetchBuilder = undefined; - var fetch = try fb.build(gpa, tmp.dir, tarball_path); + var fetch = try fb.build(gpa, io, tmp.dir, tarball_path); defer fb.deinit(); try fetch.run(); @@ -2244,13 +2250,14 @@ const TestFetchBuilder = struct { fn build( self: *TestFetchBuilder, allocator: std.mem.Allocator, + io: Io, cache_parent_dir: std.fs.Dir, path_or_url: []const u8, ) !*Fetch { const cache_dir = try cache_parent_dir.makeOpenPath("zig-global-cache", .{}); try self.thread_pool.init(.{ .allocator = allocator }); - self.http_client = .{ .allocator = allocator }; + self.http_client = .{ .allocator = allocator, .io = io }; self.global_cache_directory = .{ .handle = cache_dir, .path = null }; self.job_queue = .{ @@ -2266,6 +2273,7 @@ const TestFetchBuilder = struct { self.fetch = .{ .arena = std.heap.ArenaAllocator.init(allocator), + .io = io, .location = .{ .path_or_url = path_or_url }, .location_tok = 0, .hash_tok = .none, diff --git a/src/Package/Fetch/git.zig b/src/Package/Fetch/git.zig index df0366c783..1d01b58633 100644 --- a/src/Package/Fetch/git.zig +++ b/src/Package/Fetch/git.zig @@ -5,6 +5,7 @@ //! a package. const std = @import("std"); +const Io = std.Io; const mem = std.mem; const testing = std.testing; const Allocator = mem.Allocator; @@ -67,8 +68,8 @@ pub const Oid = union(Format) { }; const Hashing = union(Format) { - sha1: std.Io.Writer.Hashing(Sha1), - sha256: std.Io.Writer.Hashing(Sha256), + sha1: Io.Writer.Hashing(Sha1), + sha256: Io.Writer.Hashing(Sha256), fn init(oid_format: Format, buffer: []u8) Hashing { return switch (oid_format) { @@ -77,7 +78,7 @@ pub const Oid = union(Format) { }; } - fn writer(h: *@This()) *std.Io.Writer { + fn writer(h: *@This()) *Io.Writer { return switch (h.*) { inline else => |*inner| &inner.writer, }; @@ -100,7 +101,7 @@ pub const Oid = union(Format) { }; } - pub fn readBytes(oid_format: Format, reader: *std.Io.Reader) !Oid { + pub fn readBytes(oid_format: Format, reader: *Io.Reader) !Oid { return switch (oid_format) { inline else => |tag| @unionInit(Oid, @tagName(tag), (try reader.takeArray(tag.byteLength())).*), }; @@ -146,7 +147,7 @@ pub const Oid = union(Format) { } else error.InvalidOid; } - pub fn format(oid: Oid, writer: *std.Io.Writer) std.Io.Writer.Error!void { + pub fn format(oid: Oid, writer: *Io.Writer) Io.Writer.Error!void { try writer.print("{x}", .{oid.slice()}); } @@ -594,7 +595,7 @@ pub const Packet = union(enum) { pub const max_data_length = 65516; /// Reads a packet in pkt-line format. - fn read(reader: *std.Io.Reader) !Packet { + fn read(reader: *Io.Reader) !Packet { const packet: Packet = try .peek(reader); switch (packet) { .data => |data| reader.toss(data.len), @@ -605,7 +606,7 @@ pub const Packet = union(enum) { /// Consumes the header of a pkt-line packet and reads any associated data /// into the reader's buffer, but does not consume the data. - fn peek(reader: *std.Io.Reader) !Packet { + fn peek(reader: *Io.Reader) !Packet { const length = std.fmt.parseUnsigned(u16, try reader.take(4), 16) catch return error.InvalidPacket; switch (length) { 0 => return .flush, @@ -618,7 +619,7 @@ pub const Packet = union(enum) { } /// Writes a packet in pkt-line format. - fn write(packet: Packet, writer: *std.Io.Writer) !void { + fn write(packet: Packet, writer: *Io.Writer) !void { switch (packet) { .flush => try writer.writeAll("0000"), .delimiter => try writer.writeAll("0001"), @@ -812,7 +813,7 @@ pub const Session = struct { const CapabilityIterator = struct { request: std.http.Client.Request, - reader: *std.Io.Reader, + reader: *Io.Reader, decompress: std.http.Decompress, const Capability = struct { @@ -869,7 +870,7 @@ pub const Session = struct { upload_pack_uri.query = null; upload_pack_uri.fragment = null; - var body: std.Io.Writer = .fixed(options.buffer); + var body: Io.Writer = .fixed(options.buffer); try Packet.write(.{ .data = "command=ls-refs\n" }, &body); if (session.supports_agent) { try Packet.write(.{ .data = agent_capability }, &body); @@ -918,7 +919,7 @@ pub const Session = struct { pub const RefIterator = struct { format: Oid.Format, request: std.http.Client.Request, - reader: *std.Io.Reader, + reader: *Io.Reader, decompress: std.http.Decompress, pub const Ref = struct { @@ -986,7 +987,7 @@ pub const Session = struct { upload_pack_uri.query = null; upload_pack_uri.fragment = null; - var body: std.Io.Writer = .fixed(response_buffer); + var body: Io.Writer = .fixed(response_buffer); try Packet.write(.{ .data = "command=fetch\n" }, &body); if (session.supports_agent) { try Packet.write(.{ .data = agent_capability }, &body); @@ -1068,8 +1069,8 @@ pub const Session = struct { pub const FetchStream = struct { request: std.http.Client.Request, - input: *std.Io.Reader, - reader: std.Io.Reader, + input: *Io.Reader, + reader: Io.Reader, err: ?Error = null, remaining_len: usize, decompress: std.http.Decompress, @@ -1094,7 +1095,7 @@ pub const Session = struct { _, }; - pub fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + pub fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize { const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r)); const input = fs.input; if (fs.remaining_len == 0) { @@ -1139,7 +1140,7 @@ const PackHeader = struct { const signature = "PACK"; const supported_version = 2; - fn read(reader: *std.Io.Reader) !PackHeader { + fn read(reader: *Io.Reader) !PackHeader { const actual_signature = reader.take(4) catch |e| switch (e) { error.EndOfStream => return error.InvalidHeader, else => |other| return other, @@ -1202,7 +1203,7 @@ const EntryHeader = union(Type) { }; } - fn read(format: Oid.Format, reader: *std.Io.Reader) !EntryHeader { + fn read(format: Oid.Format, reader: *Io.Reader) !EntryHeader { const InitialByte = packed struct { len: u4, type: u3, has_next: bool }; const initial: InitialByte = @bitCast(reader.takeByte() catch |e| switch (e) { error.EndOfStream => return error.InvalidFormat, @@ -1231,7 +1232,7 @@ const EntryHeader = union(Type) { } }; -fn readOffsetVarInt(r: *std.Io.Reader) !u64 { +fn readOffsetVarInt(r: *Io.Reader) !u64 { const Byte = packed struct { value: u7, has_next: bool }; var b: Byte = @bitCast(try r.takeByte()); var value: u64 = b.value; @@ -1250,7 +1251,7 @@ const IndexHeader = struct { const supported_version = 2; const size = 4 + 4 + @sizeOf([256]u32); - fn read(index_header: *IndexHeader, reader: *std.Io.Reader) !void { + fn read(index_header: *IndexHeader, reader: *Io.Reader) !void { const sig = try reader.take(4); if (!mem.eql(u8, sig, signature)) return error.InvalidHeader; const version = try reader.takeInt(u32, .big); @@ -1324,7 +1325,7 @@ pub fn indexPack( } @memset(fan_out_table[fan_out_index..], count); - var index_hashed_writer = std.Io.Writer.hashed(&index_writer.interface, Oid.Hasher.init(format), &.{}); + var index_hashed_writer = Io.Writer.hashed(&index_writer.interface, Oid.Hasher.init(format), &.{}); const writer = &index_hashed_writer.writer; try writer.writeAll(IndexHeader.signature); try writer.writeInt(u32, IndexHeader.supported_version, .big); @@ -1489,14 +1490,14 @@ fn resolveDeltaChain( const delta_header = try EntryHeader.read(format, &pack.interface); const delta_data = try readObjectRaw(allocator, &pack.interface, delta_header.uncompressedLength()); defer allocator.free(delta_data); - var delta_reader: std.Io.Reader = .fixed(delta_data); + var delta_reader: Io.Reader = .fixed(delta_data); _ = try delta_reader.takeLeb128(u64); // base object size const expanded_size = try delta_reader.takeLeb128(u64); const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge; const expanded_data = try allocator.alloc(u8, expanded_alloc_size); errdefer allocator.free(expanded_data); - var expanded_delta_stream: std.Io.Writer = .fixed(expanded_data); + var expanded_delta_stream: Io.Writer = .fixed(expanded_data); try expandDelta(base_data, &delta_reader, &expanded_delta_stream); if (expanded_delta_stream.end != expanded_size) return error.InvalidObject; @@ -1509,9 +1510,9 @@ fn resolveDeltaChain( /// Reads the complete contents of an object from `reader`. This function may /// read more bytes than required from `reader`, so the reader position after /// returning is not reliable. -fn readObjectRaw(allocator: Allocator, reader: *std.Io.Reader, size: u64) ![]u8 { +fn readObjectRaw(allocator: Allocator, reader: *Io.Reader, size: u64) ![]u8 { const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge; - var aw: std.Io.Writer.Allocating = .init(allocator); + var aw: Io.Writer.Allocating = .init(allocator); try aw.ensureTotalCapacity(alloc_size + std.compress.flate.max_window_len); defer aw.deinit(); var decompress: std.compress.flate.Decompress = .init(reader, .zlib, &.{}); @@ -1523,7 +1524,7 @@ fn readObjectRaw(allocator: Allocator, reader: *std.Io.Reader, size: u64) ![]u8 /// /// The format of the delta data is documented in /// [pack-format](https://git-scm.com/docs/pack-format). -fn expandDelta(base_object: []const u8, delta_reader: *std.Io.Reader, writer: *std.Io.Writer) !void { +fn expandDelta(base_object: []const u8, delta_reader: *Io.Reader, writer: *Io.Writer) !void { while (true) { const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.takeByte() catch |e| switch (e) { error.EndOfStream => return, @@ -1576,7 +1577,7 @@ fn expandDelta(base_object: []const u8, delta_reader: *std.Io.Reader, writer: *s /// - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb` /// - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a` /// 4. `git checkout $commit` -fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void { +fn runRepositoryTest(io: Io, comptime format: Oid.Format, head_commit: []const u8) !void { const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack"); var git_dir = testing.tmpDir(.{}); @@ -1586,7 +1587,7 @@ fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void try pack_file.writeAll(testrepo_pack); var pack_file_buffer: [2000]u8 = undefined; - var pack_file_reader = pack_file.reader(&pack_file_buffer); + var pack_file_reader = pack_file.reader(io, &pack_file_buffer); var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true }); defer index_file.close(); @@ -1608,7 +1609,7 @@ fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void try testing.expectEqualSlices(u8, testrepo_idx, index_file_data); } - var index_file_reader = index_file.reader(&index_file_buffer); + var index_file_reader = index_file.reader(io, &index_file_buffer); var repository: Repository = undefined; try repository.init(testing.allocator, format, &pack_file_reader, &index_file_reader); defer repository.deinit(); @@ -1687,11 +1688,11 @@ fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void const skip_checksums = true; test "SHA-1 packfile indexing and checkout" { - try runRepositoryTest(.sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb"); + try runRepositoryTest(std.testing.io, .sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb"); } test "SHA-256 packfile indexing and checkout" { - try runRepositoryTest(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"); + try runRepositoryTest(std.testing.io, .sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"); } /// Checks out a commit of a packfile. Intended for experimenting with and @@ -1699,6 +1700,10 @@ test "SHA-256 packfile indexing and checkout" { pub fn main() !void { const allocator = std.heap.smp_allocator; + var threaded: Io.Threaded = .init(allocator); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len != 5) { @@ -1710,7 +1715,7 @@ pub fn main() !void { var pack_file = try std.fs.cwd().openFile(args[2], .{}); defer pack_file.close(); var pack_file_buffer: [4096]u8 = undefined; - var pack_file_reader = pack_file.reader(&pack_file_buffer); + var pack_file_reader = pack_file.reader(io, &pack_file_buffer); const commit = try Oid.parse(format, args[3]); var worktree = try std.fs.cwd().makeOpenPath(args[4], .{}); @@ -1727,7 +1732,7 @@ pub fn main() !void { try indexPack(allocator, format, &pack_file_reader, &index_file_writer); std.debug.print("Starting checkout...\n", .{}); - var index_file_reader = index_file.reader(&index_file_buffer); + var index_file_reader = index_file.reader(io, &index_file_buffer); var repository: Repository = undefined; try repository.init(allocator, format, &pack_file_reader, &index_file_reader); defer repository.deinit(); From 894cb5a1fc51e457065d2ec8820863a00e7d96e4 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 22:31:52 -0700 Subject: [PATCH 153/244] std.posix: untangle getRandomBytesDevURandom from Io.Reader --- lib/std/posix.zig | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 4473faf328..d7bfc0d03c 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -666,18 +666,22 @@ pub fn getrandom(buffer: []u8) GetRandomError!void { return getRandomBytesDevURandom(buffer); } -fn getRandomBytesDevURandom(buf: []u8) !void { +fn getRandomBytesDevURandom(buf: []u8) GetRandomError!void { const fd = try openZ("/dev/urandom", .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); defer close(fd); - const st = try fstat(fd); + const st = fstat(fd) catch |err| switch (err) { + error.Streaming => return error.NoDevice, + else => |e| return e, + }; if (!S.ISCHR(st.mode)) { return error.NoDevice; } - const file: fs.File = .{ .handle = fd }; - var file_reader = file.readerStreaming(&.{}); - file_reader.interface.readSliceAll(buf) catch return error.Unexpected; + var i: usize = 0; + while (i < buf.len) { + i += read(fd, buf[i..]) catch return error.Unexpected; + } } /// Causes abnormal process termination. From 71c86e1d28b0205a3417f93a41b794f656625355 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 22:36:14 -0700 Subject: [PATCH 154/244] std.posix: fix compilation on wasm32-freestanding --- lib/std/posix.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/posix.zig b/lib/std/posix.zig index d7bfc0d03c..aa26041c03 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -52,6 +52,8 @@ else switch (native_os) { pub const fd_t = void; pub const uid_t = void; pub const gid_t = void; + pub const mode_t = void; + pub const ino_t = void; }, }; From dc6a4f3bf1262b46a46e0e23f5c09a93c7c780c5 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 19 Oct 2025 23:30:39 -0700 Subject: [PATCH 155/244] std.Io: add dirMakePath and dirMakeOpenPath --- lib/std/Io.zig | 4 +- lib/std/Io/Dir.zig | 14 +++ lib/std/Io/Threaded.zig | 241 ++++++++++++++++++++++++++++++++++++++-- lib/std/fs/Dir.zig | 171 ++-------------------------- 4 files changed, 259 insertions(+), 171 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index b16661be97..a3cb2cf29f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -660,7 +660,9 @@ pub const VTable = struct { conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, - dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, + dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void, + dirMakePath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.Mode) Dir.MakeError!void, + dirMakeOpenPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir, dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, dirAccess: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.AccessOptions) Dir.AccessError!void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 7336ab24af..ab130dca8e 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -348,6 +348,20 @@ pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!Make } } +pub const MakeOpenPathError = MakeError || OpenError || StatPathError; + +/// Performs the equivalent of `makePath` followed by `openDir`, atomically if possible. +/// +/// When this operation is canceled, it may leave the file system in a +/// partially modified state. +/// +/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// On WASI, `sub_path` should be encoded as valid UTF-8. +/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. +pub fn makeOpenPath(dir: Dir, io: Io, sub_path: []const u8, options: OpenOptions) MakeOpenPathError!Dir { + return io.vtable.dirMakeOpenPath(io.userdata, dir, sub_path, options); +} + pub const Stat = File.Stat; pub const StatError = File.StatError; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 174d4b76f3..6097fc6df3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -168,6 +168,15 @@ pub fn io(t: *Threaded) Io { .wasi => dirMakeWasi, else => dirMakePosix, }, + .dirMakePath = switch (builtin.os.tag) { + .windows => dirMakePathWindows, + else => dirMakePathPosix, + }, + .dirMakeOpenPath = switch (builtin.os.tag) { + .windows => dirMakeOpenPathWindows, + .wasi => dirMakeOpenPathWasi, + else => dirMakeOpenPathPosix, + }, .dirStat = dirStat, .dirStatPath = switch (builtin.os.tag) { .linux => dirStatPathLinux, @@ -197,7 +206,7 @@ pub fn io(t: *Threaded) Io { else => dirOpenFilePosix, }, .dirOpenDir = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => dirOpenDirWindows, .wasi => dirOpenDirWasi, else => dirOpenDirPosix, }, @@ -991,6 +1000,153 @@ fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode windows.CloseHandle(sub_dir_handle); } +fn dirMakePathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + +fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + +fn dirMakeOpenPathPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = t.io(); + return dir.openDir(t_io, sub_path, options) catch |err| switch (err) { + error.FileNotFound => { + try dir.makePath(t_io, sub_path); + return dir.openDir(t_io, sub_path, options); + }, + else => |e| return e, + }; +} + +fn dirMakeOpenPathWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const w = windows; + const access_mask = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE | + (if (options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); + + var it = try std.fs.path.componentIterator(sub_path); + // If there are no components in the path, then create a dummy component with the full path. + var component: std.fs.path.NativeComponentIterator.Component = it.last() orelse .{ + .name = "", + .path = sub_path, + }; + + while (true) { + try t.checkCancel(); + + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, component.path); + const sub_path_w = sub_path_w_array.span(); + const is_last = it.peekNext() == null; + const create_disposition: u32 = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE; + + var result: Io.Dir = .{ .handle = undefined }; + + const path_len_bytes: u16 = @intCast(sub_path_w.len * 2); + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtCreateFile( + &result.handle, + access_mask, + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + create_disposition, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => { + component = it.next() orelse return result; + w.CloseHandle(result.handle); + continue; + }, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_COLLISION => { + assert(!is_last); + // stat the file and return an error if it's not a directory + // this is important because otherwise a dangling symlink + // could cause an infinite loop + check_dir: { + // workaround for windows, see https://github.com/ziglang/zig/issues/16738 + const fstat = dir.statPath(t.io(), component.path, .{ + .follow_symlinks = options.follow_symlinks, + }) catch |stat_err| switch (stat_err) { + error.IsDir => break :check_dir, + else => |e| return e, + }; + if (fstat.kind != .directory) return error.NotDir; + } + + component = it.next().?; + continue; + }, + + .OBJECT_NAME_NOT_FOUND, + .OBJECT_PATH_NOT_FOUND, + => { + component = it.previous() orelse return error.FileNotFound; + continue; + }, + + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => |err| return w.statusBug(err), + else => return w.unexpectedStatus(rc), + } + } +} + +fn dirMakeOpenPathWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeOpenPathError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} + fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -1859,6 +2015,75 @@ fn dirOpenDirPosix( @panic("TODO"); } +fn dirOpenDirWindows( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const w = windows; + const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w = sub_path_w_array.span(); + + // TODO remove some of these flags if options.access_sub_paths is false + const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | + w.SYNCHRONIZE | w.FILE_TRAVERSE; + const access_mask: u32 = if (options.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; + + const path_len_bytes: u16 = @intCast(sub_path_w.len * 2); + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle, + .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; + const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; + var io_status_block: w.IO_STATUS_BLOCK = undefined; + var result: Io.Dir = .{ .handle = undefined }; + const rc = w.ntdll.NtCreateFile( + &result.handle, + access_mask, + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, + null, + 0, + ); + + switch (rc) { + .SUCCESS => return result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_NAME_COLLISION => |err| return w.statusBug(err), + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + // and the directory is trying to be opened for iteration. + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => |err| return w.statusBug(err), + else => return w.unexpectedStatus(rc), + } +} + +const MakeOpenDirAccessMaskWOptions = struct { + no_follow: bool, + create_disposition: u32, +}; + fn dirClose(userdata: ?*anyopaque, dir: Io.Dir) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -2304,17 +2529,17 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestam const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; switch (clock) { - .realtime => { + .real => { // RtlGetSystemTimePrecise() has a granularity of 100 nanoseconds // and uses the NTFS/Windows epoch, which is 1601-01-01. return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 }; }, - .monotonic, .uptime => { + .awake, .boot => { // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. - return .{ .timestamp = windows.QueryPerformanceCounter() }; + return .{ .nanoseconds = windows.QueryPerformanceCounter() }; }, - .process_cputime_id, - .thread_cputime_id, + .cpu_process, + .cpu_thread, => return error.UnsupportedClock, } } @@ -2360,9 +2585,9 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); const ms = ms: { - const duration_and_clock = (try timeout.toDurationFromNow(t.io())) orelse + const d = (try timeout.toDurationFromNow(t.io())) orelse break :ms std.math.maxInt(windows.DWORD); - break :ms std.math.lossyCast(windows.DWORD, duration_and_clock.duration.toMilliseconds()); + break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds()); }; windows.kernel32.Sleep(ms); } diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 1b2182f0f7..1f179241b0 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -898,85 +898,11 @@ pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathSta return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path); } -/// Windows only. Calls makeOpenDirAccessMaskW iteratively to make an entire path -/// (i.e. creating any parent directories that do not exist). -/// Opens the dir if the path already exists and is a directory. -/// This function is not atomic, and if it returns an error, the file system may -/// have been modified regardless. -/// `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) (MakeError || OpenError || StatFileError)!Dir { - const w = windows; - var it = try fs.path.componentIterator(sub_path); - // If there are no components in the path, then create a dummy component with the full path. - var component = it.last() orelse fs.path.NativeComponentIterator.Component{ - .name = "", - .path = sub_path, - }; - - while (true) { - const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); - const is_last = it.peekNext() == null; - var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ - .no_follow = no_follow, - .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, - }) catch |err| switch (err) { - error.FileNotFound => |e| { - component = it.previous() orelse return e; - continue; - }, - error.PathAlreadyExists => result: { - assert(!is_last); - // stat the file and return an error if it's not a directory - // this is important because otherwise a dangling symlink - // could cause an infinite loop - check_dir: { - // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = self.statFile(component.path) catch |stat_err| switch (stat_err) { - error.IsDir => break :check_dir, - else => |e| return e, - }; - if (fstat.kind != .directory) return error.NotDir; - } - break :result null; - }, - else => |e| return e, - }; - - component = it.next() orelse return result.?; - - // Don't leak the intermediate file handles - if (result) |*dir| { - dir.close(); - } - } -} - -/// This function performs `makePath`, followed by `openDir`. -/// If supported by the OS, this operation is atomic. It is not atomic on -/// all operating systems. -/// On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `sub_path` should be encoded as valid UTF-8. -/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenOptions) (MakeError || OpenError || StatFileError)!Dir { - return switch (native_os) { - .windows => { - const w = windows; - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE | - (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); - - return self.makeOpenPathAccessMaskW(sub_path, base_flags, !open_dir_options.follow_symlinks); - }, - else => { - return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { - error.FileNotFound => { - try self.makePath(sub_path); - return self.openDir(sub_path, open_dir_options); - }, - else => |e| return e, - }; - }, - }; +/// Deprecated in favor of `Io.Dir.makeOpenPath`. +pub fn makeOpenPath(dir: Dir, sub_path: []const u8, options: OpenOptions) Io.Dir.MakeOpenPathError!Dir { + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.makeOpenPath(dir.adaptToNewApi(), io, sub_path, options)); } pub const RealPathError = posix.RealPathError || error{Canceled}; @@ -1145,8 +1071,9 @@ pub const OpenOptions = Io.Dir.OpenOptions; pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir { switch (native_os) { .windows => { - const sub_path_w = try windows.sliceToPrefixedFileW(self.fd, sub_path); - return self.openDirW(sub_path_w.span().ptr, args); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); }, .wasi => if (!builtin.link_libc) { var threaded: Io.Threaded = .init_single_threaded; @@ -1163,8 +1090,7 @@ pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenError!Dir { switch (native_os) { .windows => { - const sub_path_w = try windows.cStrToPrefixedFileW(self.fd, sub_path_c); - return self.openDirW(sub_path_w.span().ptr, args); + @compileError("use std.Io instead"); }, // Use the libc API when libc is linked because it implements things // such as opening absolute directory paths. @@ -1215,28 +1141,6 @@ pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenErr return self.openDirFlagsZ(sub_path_c, symlink_flags); } -/// Same as `openDir` except the path parameter is WTF-16 LE encoded, NT-prefixed. -/// This function asserts the target OS is Windows. -pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenOptions) OpenError!Dir { - const w = windows; - // TODO remove some of these flags if args.access_sub_paths is false - const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | - w.SYNCHRONIZE | w.FILE_TRAVERSE; - const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; - const dir = self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ - .no_follow = !args.follow_symlinks, - .create_disposition = w.FILE_OPEN, - }) catch |err| switch (err) { - error.ReadOnlyFileSystem => unreachable, - error.DiskQuota => unreachable, - error.NoSpaceLeft => unreachable, - error.PathAlreadyExists => unreachable, - error.LinkQuotaExceeded => unreachable, - else => |e| return e, - }; - return dir; -} - /// Asserts `flags` has `DIRECTORY` set. fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError!Dir { assert(flags.DIRECTORY); @@ -1257,63 +1161,6 @@ fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError return Dir{ .fd = fd }; } -const MakeOpenDirAccessMaskWOptions = struct { - no_follow: bool, - create_disposition: u32, -}; - -fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) (MakeError || OpenError)!Dir { - const w = windows; - - var result = Dir{ - .fd = undefined, - }; - - const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); - var nt_name = w.UNICODE_STRING{ - .Length = path_len_bytes, - .MaximumLength = path_len_bytes, - .Buffer = @constCast(sub_path_w), - }; - var attr = w.OBJECT_ATTRIBUTES{ - .Length = @sizeOf(w.OBJECT_ATTRIBUTES), - .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, - .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. - .ObjectName = &nt_name, - .SecurityDescriptor = null, - .SecurityQualityOfService = null, - }; - const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; - var io: w.IO_STATUS_BLOCK = undefined; - const rc = w.ntdll.NtCreateFile( - &result.fd, - access_mask, - &attr, - &io, - null, - w.FILE_ATTRIBUTE_NORMAL, - w.FILE_SHARE_READ | w.FILE_SHARE_WRITE | w.FILE_SHARE_DELETE, - flags.create_disposition, - w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, - null, - 0, - ); - - switch (rc) { - .SUCCESS => return result, - .OBJECT_NAME_INVALID => return error.BadPathName, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_A_DIRECTORY => return error.NotDir, - // This can happen if the directory has 'List folder contents' permission set to 'Deny' - // and the directory is trying to be opened for iteration. - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_PARAMETER => unreachable, - else => return w.unexpectedStatus(rc), - } -} - pub const DeleteFileError = posix.UnlinkError; /// Delete a file name and possibly the file it refers to, based on an open directory handle. From 62c0496d0a3bed811174080f651408c89bdce0c9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 20 Oct 2025 06:13:39 -0700 Subject: [PATCH 156/244] std.Io.Threaded: implement dirOpenDir --- lib/std/Io/Threaded.zig | 120 +++++++++++++++++++++++++++++++++------- lib/std/fs/Dir.zig | 93 +------------------------------ 2 files changed, 103 insertions(+), 110 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 6097fc6df3..6121878f2b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -208,6 +208,7 @@ pub fn io(t: *Threaded) Io { .dirOpenDir = switch (builtin.os.tag) { .windows => dirOpenDirWindows, .wasi => dirOpenDirWasi, + .haiku => dirOpenDirHaiku, else => dirOpenDirPosix, }, .dirClose = dirClose, @@ -1784,22 +1785,20 @@ fn dirOpenFilePosix( 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. - if (have_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; - }, - } - } + // atomically. Note that the NONBLOCK flag is removed after the openat() + // call is successful. + if (have_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 fd: posix.fd_t = while (true) { try t.checkCancel(); const rc = openat_sym(dir.handle, sub_path_posix, os_flags, @as(posix.mode_t, 0)); @@ -2008,11 +2007,92 @@ fn dirOpenDirPosix( ) Io.Dir.OpenError!Io.Dir { const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; - _ = dir; - _ = sub_path; + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var flags: posix.O = switch (native_os) { + .wasi => .{ + .read = true, + .NOFOLLOW = !options.follow_symlinks, + .DIRECTORY = true, + }, + else => .{ + .ACCMODE = .RDONLY, + .NOFOLLOW = !options.follow_symlinks, + .DIRECTORY = true, + .CLOEXEC = true, + }, + }; + + if (@hasField(posix.O, "PATH") and !options.iterate) + flags.PATH = true; + + while (true) { + try t.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, flags, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => return .{ .handle = @intCast(rc) }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .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, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + .NXIO => return error.NoDevice, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirOpenDirHaiku( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.OpenOptions, +) Io.Dir.OpenError!Io.Dir { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + _ = options; - @panic("TODO"); + + while (true) { + try t.checkCancel(); + const rc = posix.system._kern_open_dir(dir.handle, sub_path_posix); + if (rc >= 0) return .{ .handle = rc }; + switch (@as(posix.E, @enumFromInt(rc))) { + .INTR => continue, + .CANCELED => return error.Canceled, + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .ACCES => return error.AccessDenied, + .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, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .BUSY => return error.DeviceBusy, + else => |err| return posix.unexpectedErrno(err), + } + } } fn dirOpenDirWindows( diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 1f179241b0..15f3a6c45a 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -1069,96 +1069,9 @@ pub const OpenOptions = Io.Dir.OpenOptions; /// Deprecated in favor of `Io.Dir.openDir`. pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir { - switch (native_os) { - .windows => { - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); - return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); - }, - .wasi => if (!builtin.link_libc) { - var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); - return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); - }, - else => {}, - } - const sub_path_c = try posix.toPosixPath(sub_path); - return self.openDirZ(&sub_path_c, args); -} - -/// Same as `openDir` except the parameter is null-terminated. -pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenOptions) OpenError!Dir { - switch (native_os) { - .windows => { - @compileError("use std.Io instead"); - }, - // Use the libc API when libc is linked because it implements things - // such as opening absolute directory paths. - .wasi => if (!builtin.link_libc) { - return openDir(self, mem.sliceTo(sub_path_c, 0), args); - }, - .haiku => { - const rc = posix.system._kern_open_dir(self.fd, sub_path_c); - if (rc >= 0) return .{ .fd = rc }; - switch (@as(posix.E, @enumFromInt(rc))) { - .FAULT => unreachable, - .INVAL => unreachable, - .BADF => unreachable, - .ACCES => return error.AccessDenied, - .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, - .NOTDIR => return error.NotDir, - .PERM => return error.PermissionDenied, - .BUSY => return error.DeviceBusy, - else => |err| return posix.unexpectedErrno(err), - } - }, - else => {}, - } - - var symlink_flags: posix.O = switch (native_os) { - .wasi => .{ - .read = true, - .NOFOLLOW = !args.follow_symlinks, - .DIRECTORY = true, - }, - else => .{ - .ACCMODE = .RDONLY, - .NOFOLLOW = !args.follow_symlinks, - .DIRECTORY = true, - .CLOEXEC = true, - }, - }; - - if (@hasField(posix.O, "PATH") and !args.iterate) - symlink_flags.PATH = true; - - return self.openDirFlagsZ(sub_path_c, symlink_flags); -} - -/// Asserts `flags` has `DIRECTORY` set. -fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError!Dir { - assert(flags.DIRECTORY); - const fd = posix.openatZ(self.fd, sub_path_c, flags, 0) catch |err| switch (err) { - error.FileTooBig => unreachable, // can't happen for directories - error.IsDir => unreachable, // we're setting DIRECTORY - error.NoSpaceLeft => unreachable, // not setting CREAT - error.PathAlreadyExists => unreachable, // not setting CREAT - error.FileLocksNotSupported => unreachable, // locking folders is not supported - error.WouldBlock => unreachable, // can't happen for directories - error.FileBusy => unreachable, // can't happen for directories - error.SharingViolation => unreachable, // can't happen for directories - error.PipeBusy => unreachable, // can't happen for directories - error.AntivirusInterference => unreachable, // can't happen for directories - error.ProcessNotFound => unreachable, // can't happen for directories - else => |e| return e, - }; - return Dir{ .fd = fd }; + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); } pub const DeleteFileError = posix.UnlinkError; From 34891b528e11afe1a1818a18d8ae01035542bb27 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 20 Oct 2025 11:12:08 -0700 Subject: [PATCH 157/244] std.Io.Threaded: implement netListen for Windows --- lib/std/Io/Threaded.zig | 295 ++++++++++++++++++++++++++--- lib/std/os/windows.zig | 152 --------------- lib/std/os/windows/test.zig | 25 --- lib/std/os/windows/ws2_32.zig | 287 ++++++++++------------------ lib/std/posix.zig | 340 +++++++++++----------------------- lib/std/posix/test.zig | 19 -- 6 files changed, 479 insertions(+), 639 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 6121878f2b..e03440e9d3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const native_os = builtin.os.tag; const is_windows = native_os == .windows; const windows = std.os.windows; +const ws2_32 = std.os.windows.ws2_32; const std = @import("../std.zig"); const Io = std.Io; @@ -24,6 +25,7 @@ threads: std.ArrayListUnmanaged(std.Thread), stack_size: usize, cpu_count: std.Thread.CpuCountError!usize, concurrent_count: usize, +wsa: if (is_windows) Wsa else struct {} = .{}, threadlocal var current_closure: ?*Closure = null; @@ -105,6 +107,9 @@ pub fn deinit(t: *Threaded) void { const gpa = t.allocator; t.join(); t.threads.deinit(gpa); + if (is_windows and t.wsa.status == .initialized) { + if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected(); + } t.* = undefined; } @@ -234,7 +239,7 @@ pub fn io(t: *Threaded) Io { }, .netListenIp = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => netListenIpWindows, else => netListenIpPosix, }, .netListenUnix = netListenUnix, @@ -2797,6 +2802,116 @@ fn netListenIpPosix( }; } +fn netListenIpWindows( + userdata: ?*anyopaque, + address: IpAddress, + options: IpAddress.ListenOptions, +) IpAddress.ListenError!net.Server { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(&address); + const mode = posixSocketMode(options.mode); + const protocol = posixProtocol(options.protocol); + + const socket_handle = while (true) { + try t.checkCancel(); + const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; + const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags); + if (rc != ws2_32.INVALID_SOCKET) break rc; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENOBUFS => return error.SystemResources, + .EPROTONOSUPPORT => return error.ProtocolUnsupportedBySystem, + else => |err| return windows.unexpectedWSAError(err), + } + }; + errdefer closeSocketWindows(socket_handle); + + if (options.reuse_address) + try setSocketOptionWsa(t, socket_handle, posix.SOL.SOCKET, posix.SO.REUSEADDR, 1); + + var storage: WsaAddress = undefined; + var addr_len = addressToWsa(&address, &storage); + + while (true) { + try t.checkCancel(); + const rc = ws2_32.bind(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .ENOBUFS => return error.SystemResources, + .ENETDOWN => return error.NetworkDown, + else => |err| return windows.unexpectedWSAError(err), + } + } + + while (true) { + try t.checkCancel(); + const rc = ws2_32.listen(socket_handle, options.kernel_backlog); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EADDRINUSE => return error.AddressInUse, + .EISCONN => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EMFILE, .ENOBUFS => return error.SystemResources, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EOPNOTSUPP => |err| return wsaErrorBug(err), + .EINPROGRESS => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } + + while (true) { + try t.checkCancel(); + const rc = ws2_32.getsockname(socket_handle, &storage.any, &addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } + + return .{ + .socket = .{ + .handle = socket_handle, + .address = addressFromWsa(&storage), + }, + }; +} + fn netListenUnix( userdata: ?*anyopaque, address: *const net.UnixAddress, @@ -2971,7 +3086,7 @@ fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, opti .CANCELED => return error.Canceled, .BADF => |err| return errnoBug(err), // File descriptor used after closed. - .NOTSOCK => |err| return errnoBug(err), // always a race condition + .NOTSOCK => |err| return errnoBug(err), .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), else => |err| return posix.unexpectedErrno(err), @@ -2979,6 +3094,27 @@ fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, opti } } +fn setSocketOptionWsa(t: *Threaded, socket: Io.net.Socket.Handle, level: i32, opt_name: u32, option: u32) !void { + const o: []const u8 = @ptrCast(&option); + const rc = ws2_32.setsockopt(socket, level, @bitCast(opt_name), o.ptr, @intCast(o.len)); + while (true) { + if (rc != ws2_32.SOCKET_ERROR) return; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + fn netConnectIpPosix( userdata: ?*anyopaque, address: *const IpAddress, @@ -3263,25 +3399,31 @@ fn netSendOne( try t.checkCancel(); const rc = posix.system.sendmsg(handle, &msg, flags); if (is_windows) { - if (rc == windows.ws2_32.SOCKET_ERROR) { - switch (windows.ws2_32.WSAGetLastError()) { - .WSAEACCES => return error.AccessDenied, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageOversize, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, - .WSAEDESTADDRREQ => unreachable, // A destination address is required. - .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. - .WSAEHOSTUNREACH => return error.NetworkUnreachable, - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + if (rc == ws2_32.SOCKET_ERROR) { + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EACCES => return error.AccessDenied, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EMSGSIZE => return error.MessageOversize, + .ENOBUFS => return error.SystemResources, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EDESTADDRREQ => unreachable, // A destination address is required. + .EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .EHOSTUNREACH => return error.NetworkUnreachable, + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENETUNREACH => return error.NetworkUnreachable, + .ENOTCONN => return error.SocketUnconnected, + .ESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. + .NOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. else => |err| return windows.unexpectedWSAError(err), } } else { @@ -3613,7 +3755,7 @@ fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; switch (native_os) { - .windows => windows.closesocket(handle) catch recoverableOsBugDetected(), + .windows => closeSocketWindows(handle) catch recoverableOsBugDetected(), else => posix.close(handle), } } @@ -3664,7 +3806,7 @@ fn netInterfaceNameResolve( if (native_os == .windows) { try t.checkCancel(); - const index = windows.ws2_32.if_nametoindex(&name.bytes); + const index = ws2_32.if_nametoindex(&name.bytes); if (index == 0) return error.InterfaceNotFound; return .{ .index = index }; } @@ -3881,6 +4023,13 @@ const UnixAddress = extern union { un: posix.sockaddr.un, }; +const WsaAddress = extern union { + any: ws2_32.sockaddr, + in: ws2_32.sockaddr.in, + in6: ws2_32.sockaddr.in6, + un: ws2_32.sockaddr.un, +}; + fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t { return switch (a.*) { .ip4 => posix.AF.INET, @@ -3896,6 +4045,14 @@ fn addressFromPosix(posix_address: *const PosixAddress) IpAddress { }; } +fn addressFromWsa(wsa_address: *const WsaAddress) IpAddress { + return switch (wsa_address.any.family) { + posix.AF.INET => .{ .ip4 = address4FromWsa(&wsa_address.in) }, + posix.AF.INET6 => .{ .ip6 = address6FromWsa(&wsa_address.in6) }, + else => .{ .ip4 = .loopback(0) }, + }; +} + fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t { return switch (a.*) { .ip4 => |ip4| { @@ -3909,6 +4066,19 @@ fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t { }; } +fn addressToWsa(a: *const IpAddress, storage: *WsaAddress) i32 { + return switch (a.*) { + .ip4 => |ip4| { + storage.in = address4ToPosix(ip4); + return @sizeOf(posix.sockaddr.in); + }, + .ip6 => |*ip6| { + storage.in6 = address6ToPosix(ip6); + return @sizeOf(posix.sockaddr.in6); + }, + }; +} + fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.socklen_t { @memcpy(storage.un.path[0..a.path.len], a.path); storage.un.family = posix.AF.UNIX; @@ -3932,6 +4102,22 @@ fn address6FromPosix(in6: *const posix.sockaddr.in6) net.Ip6Address { }; } +fn address4FromWsa(in: *const ws2_32.sockaddr.in) net.Ip4Address { + return .{ + .port = std.mem.bigToNative(u16, in.port), + .bytes = @bitCast(in.addr), + }; +} + +fn address6FromWsa(in6: *const ws2_32.sockaddr.in6) net.Ip6Address { + return .{ + .port = std.mem.bigToNative(u16, in6.port), + .bytes = in6.addr, + .flow = in6.flowinfo, + .interface = .{ .index = in6.scope_id }, + }; +} + fn address4ToPosix(a: net.Ip4Address) posix.sockaddr.in { return .{ .port = std.mem.nativeToBig(u16, a.port), @@ -3955,6 +4141,13 @@ fn errnoBug(err: posix.E) Io.UnexpectedError { } } +fn wsaErrorBug(err: ws2_32.WinsockError) Io.UnexpectedError { + switch (builtin.mode) { + .Debug => std.debug.panic("programmer bug caused syscall error: {t}", .{err}), + else => return error.Unexpected, + } +} + fn posixSocketMode(mode: net.Socket.Mode) u32 { return switch (mode) { .stream => posix.SOCK.STREAM, @@ -4814,3 +5007,59 @@ pub const ResetEvent = enum(u32) { @atomicStore(ResetEvent, re, .unset, .monotonic); } }; + +fn closeSocketWindows(s: ws2_32.SOCKET) void { + const rc = ws2_32.closesocket(s); + if (builtin.mode == .Debug) switch (rc) { + 0 => {}, + ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { + else => unreachable, + }, + else => unreachable, + }; +} + +const Wsa = struct { + status: Status = .uninitialized, + mutex: Io.Mutex = .init, + init_error: ?Wsa.InitError = null, + + const Status = enum { uninitialized, initialized, failure }; + + const InitError = error{ + ProcessFdQuotaExceeded, + NetworkDown, + VersionUnsupported, + BlockingOperationInProgress, + } || Io.UnexpectedError; +}; + +fn initializeWsa(t: *Threaded) error{NetworkDown}!void { + const t_io = t.io(); + const wsa = &t.wsa; + wsa.mutex.lockUncancelable(t_io); + defer wsa.mutex.unlock(t_io); + switch (wsa.status) { + .uninitialized => { + var wsa_data: ws2_32.WSADATA = undefined; + const minor_version = 2; + const major_version = 2; + switch (ws2_32.WSAStartup((@as(windows.WORD, minor_version) << 8) | major_version, &wsa_data)) { + 0 => { + wsa.status = .initialized; + return; + }, + else => |err_int| switch (@as(ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(err_int))))) { + .SYSNOTREADY => wsa.init_error = error.NetworkDown, + .VERNOTSUPPORTED => wsa.init_error = error.VersionUnsupported, + .EINPROGRESS => wsa.init_error = error.BlockingOperationInProgress, + .EPROCLIM => wsa.init_error = error.ProcessFdQuotaExceeded, + else => |err| wsa.init_error = windows.unexpectedWSAError(err), + }, + } + }, + .initialized => return, + .failure => {}, + } + return error.NetworkDown; +} diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index f1649fafc1..7609612c26 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1574,131 +1574,11 @@ pub fn GetFileAttributesW(lpFileName: [*:0]const u16) GetFileAttributesError!DWO return rc; } -pub fn WSAStartup(majorVersion: u8, minorVersion: u8) !ws2_32.WSADATA { - var wsadata: ws2_32.WSADATA = undefined; - return switch (ws2_32.WSAStartup((@as(WORD, minorVersion) << 8) | majorVersion, &wsadata)) { - 0 => wsadata, - else => |err_int| switch (@as(ws2_32.WinsockError, @enumFromInt(@as(u16, @intCast(err_int))))) { - .WSASYSNOTREADY => return error.SystemNotAvailable, - .WSAVERNOTSUPPORTED => return error.VersionNotSupported, - .WSAEINPROGRESS => return error.BlockingOperationInProgress, - .WSAEPROCLIM => return error.ProcessFdQuotaExceeded, - else => |err| return unexpectedWSAError(err), - }, - }; -} - -pub fn WSACleanup() !void { - return switch (ws2_32.WSACleanup()) { - 0 => {}, - ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => return error.NotInitialized, - .WSAENETDOWN => return error.NetworkNotAvailable, - .WSAEINPROGRESS => return error.BlockingOperationInProgress, - else => |err| return unexpectedWSAError(err), - }, - else => unreachable, - }; -} - -var wsa_startup_mutex: std.Thread.Mutex = .{}; - -pub fn callWSAStartup() !void { - wsa_startup_mutex.lock(); - defer wsa_startup_mutex.unlock(); - - // Here we could use a flag to prevent multiple threads to prevent - // multiple calls to WSAStartup, but it doesn't matter. We're globally - // leaking the resource intentionally, and the mutex already prevents - // data races within the WSAStartup function. - _ = WSAStartup(2, 2) catch |err| switch (err) { - error.SystemNotAvailable => return error.SystemResources, - error.VersionNotSupported => return error.Unexpected, - error.BlockingOperationInProgress => return error.Unexpected, - error.ProcessFdQuotaExceeded => return error.ProcessFdQuotaExceeded, - error.Unexpected => return error.Unexpected, - }; -} - -/// Microsoft requires WSAStartup to be called to initialize, or else -/// WSASocketW will return WSANOTINITIALISED. -/// Since this is a standard library, we do not have the luxury of -/// putting initialization code anywhere, because we would not want -/// to pay the cost of calling WSAStartup if there ended up being no -/// networking. Also, if Zig code is used as a library, Zig is not in -/// charge of the start code, and we couldn't put in any initialization -/// code even if we wanted to. -/// The documentation for WSAStartup mentions that there must be a -/// matching WSACleanup call. It is not possible for the Zig Standard -/// Library to honor this for the same reason - there is nowhere to put -/// deinitialization code. -/// So, API users of the zig std lib have two options: -/// * (recommended) The simple, cross-platform way: just call `WSASocketW` -/// and don't worry about it. Zig will call WSAStartup() in a thread-safe -/// manner and never deinitialize networking. This is ideal for an -/// application which has the capability to do networking. -/// * The getting-your-hands-dirty way: call `WSAStartup()` before doing -/// networking, so that the error handling code for WSANOTINITIALISED never -/// gets run, which then allows the application or library to call `WSACleanup()`. -/// This could make sense for a library, which has init and deinit -/// functions for the whole library's lifetime. -pub fn WSASocketW( - af: i32, - socket_type: i32, - protocol: i32, - protocolInfo: ?*ws2_32.WSAPROTOCOL_INFOW, - g: ws2_32.GROUP, - dwFlags: DWORD, -) !ws2_32.SOCKET { - var first = true; - while (true) { - const rc = ws2_32.WSASocketW(af, socket_type, protocol, protocolInfo, g, dwFlags); - if (rc == ws2_32.INVALID_SOCKET) { - switch (ws2_32.WSAGetLastError()) { - .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, - .WSAEMFILE => return error.ProcessFdQuotaExceeded, - .WSAENOBUFS => return error.SystemResources, - .WSAEPROTONOSUPPORT => return error.ProtocolNotSupported, - .WSANOTINITIALISED => { - if (!first) return error.Unexpected; - first = false; - try callWSAStartup(); - continue; - }, - else => |err| return unexpectedWSAError(err), - } - } - return rc; - } -} - -pub fn bind(s: ws2_32.SOCKET, name: *const ws2_32.sockaddr, namelen: ws2_32.socklen_t) i32 { - return ws2_32.bind(s, name, @as(i32, @intCast(namelen))); -} - -pub fn listen(s: ws2_32.SOCKET, backlog: u31) i32 { - return ws2_32.listen(s, backlog); -} - -pub fn closesocket(s: ws2_32.SOCKET) !void { - switch (ws2_32.closesocket(s)) { - 0 => {}, - ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { - else => |err| return unexpectedWSAError(err), - }, - else => unreachable, - } -} - pub fn accept(s: ws2_32.SOCKET, name: ?*ws2_32.sockaddr, namelen: ?*ws2_32.socklen_t) ws2_32.SOCKET { assert((name == null) == (namelen == null)); return ws2_32.accept(s, name, @as(?*i32, @ptrCast(namelen))); } -pub fn getsockname(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { - return ws2_32.getsockname(s, name, @as(*i32, @ptrCast(namelen))); -} - pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen))); } @@ -2816,38 +2696,6 @@ inline fn MAKELANGID(p: c_ushort, s: c_ushort) LANGID { return (s << 10) | p; } -/// Loads a Winsock extension function in runtime specified by a GUID. -pub fn loadWinsockExtensionFunction(comptime T: type, sock: ws2_32.SOCKET, guid: GUID) !T { - var function: T = undefined; - var num_bytes: DWORD = undefined; - - const rc = ws2_32.WSAIoctl( - sock, - ws2_32.SIO_GET_EXTENSION_FUNCTION_POINTER, - &guid, - @sizeOf(GUID), - @as(?*anyopaque, @ptrFromInt(@intFromPtr(&function))), - @sizeOf(T), - &num_bytes, - null, - null, - ); - - if (rc == ws2_32.SOCKET_ERROR) { - return switch (ws2_32.WSAGetLastError()) { - .WSAEOPNOTSUPP => error.OperationNotSupported, - .WSAENOTSOCK => error.FileDescriptorNotASocket, - else => |err| unexpectedWSAError(err), - }; - } - - if (num_bytes != @sizeOf(T)) { - return error.ShortRead; - } - - return function; -} - /// Call this when you made a windows DLL call or something that does SetLastError /// and you get an unexpected error. pub fn unexpectedError(err: Win32Error) UnexpectedError { diff --git a/lib/std/os/windows/test.zig b/lib/std/os/windows/test.zig index b78e4c323a..2c2c113d18 100644 --- a/lib/std/os/windows/test.zig +++ b/lib/std/os/windows/test.zig @@ -237,28 +237,3 @@ test "removeDotDirs" { try testRemoveDotDirs("a\\b\\..\\", "a\\"); try testRemoveDotDirs("a\\b\\..\\c", "a\\c"); } - -test "loadWinsockExtensionFunction" { - _ = try windows.WSAStartup(2, 2); - defer windows.WSACleanup() catch unreachable; - - const LPFN_CONNECTEX = *const fn ( - Socket: windows.ws2_32.SOCKET, - SockAddr: *const windows.ws2_32.sockaddr, - SockLen: std.posix.socklen_t, - SendBuf: ?*const anyopaque, - SendBufLen: windows.DWORD, - BytesSent: *windows.DWORD, - Overlapped: *windows.OVERLAPPED, - ) callconv(.winapi) windows.BOOL; - - _ = windows.loadWinsockExtensionFunction( - LPFN_CONNECTEX, - try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0), - windows.ws2_32.WSAID_CONNECTEX, - ) catch |err| switch (err) { - error.OperationNotSupported => unreachable, - error.ShortRead => unreachable, - else => |e| return e, - }; -} diff --git a/lib/std/os/windows/ws2_32.zig b/lib/std/os/windows/ws2_32.zig index 83194425fa..64f5f57d45 100644 --- a/lib/std/os/windows/ws2_32.zig +++ b/lib/std/os/windows/ws2_32.zig @@ -1271,130 +1271,105 @@ pub const timeval = extern struct { usec: LONG, }; -// https://docs.microsoft.com/en-au/windows/win32/winsock/windows-sockets-error-codes-2 +/// https://docs.microsoft.com/en-au/windows/win32/winsock/windows-sockets-error-codes-2 pub const WinsockError = enum(u16) { /// Specified event object handle is invalid. /// An application attempts to use an event object, but the specified handle is not valid. - WSA_INVALID_HANDLE = 6, - + INVALID_HANDLE = 6, /// Insufficient memory available. /// An application used a Windows Sockets function that directly maps to a Windows function. /// The Windows function is indicating a lack of required memory resources. - WSA_NOT_ENOUGH_MEMORY = 8, - + NOT_ENOUGH_MEMORY = 8, /// One or more parameters are invalid. /// An application used a Windows Sockets function which directly maps to a Windows function. /// The Windows function is indicating a problem with one or more parameters. - WSA_INVALID_PARAMETER = 87, - + INVALID_PARAMETER = 87, /// Overlapped operation aborted. /// An overlapped operation was canceled due to the closure of the socket, or the execution of the SIO_FLUSH command in WSAIoctl. - WSA_OPERATION_ABORTED = 995, - + OPERATION_ABORTED = 995, /// Overlapped I/O event object not in signaled state. /// The application has tried to determine the status of an overlapped operation which is not yet completed. /// Applications that use WSAGetOverlappedResult (with the fWait flag set to FALSE) in a polling mode to determine when an overlapped operation has completed, get this error code until the operation is complete. - WSA_IO_INCOMPLETE = 996, - + IO_INCOMPLETE = 996, /// The application has initiated an overlapped operation that cannot be completed immediately. /// A completion indication will be given later when the operation has been completed. - WSA_IO_PENDING = 997, - + IO_PENDING = 997, /// Interrupted function call. /// A blocking operation was interrupted by a call to WSACancelBlockingCall. - WSAEINTR = 10004, - + EINTR = 10004, /// File handle is not valid. /// The file handle supplied is not valid. - WSAEBADF = 10009, - + EBADF = 10009, /// Permission denied. /// An attempt was made to access a socket in a way forbidden by its access permissions. /// An example is using a broadcast address for sendto without broadcast permission being set using setsockopt(SO.BROADCAST). /// Another possible reason for the WSAEACCES error is that when the bind function is called (on Windows NT 4.0 with SP4 and later), another application, service, or kernel mode driver is bound to the same address with exclusive access. /// Such exclusive access is a new feature of Windows NT 4.0 with SP4 and later, and is implemented by using the SO.EXCLUSIVEADDRUSE option. - WSAEACCES = 10013, - + EACCES = 10013, /// Bad address. /// The system detected an invalid pointer address in attempting to use a pointer argument of a call. /// This error occurs if an application passes an invalid pointer value, or if the length of the buffer is too small. /// For instance, if the length of an argument, which is a sockaddr structure, is smaller than the sizeof(sockaddr). - WSAEFAULT = 10014, - + EFAULT = 10014, /// Invalid argument. /// Some invalid argument was supplied (for example, specifying an invalid level to the setsockopt function). /// In some instances, it also refers to the current state of the socket—for instance, calling accept on a socket that is not listening. - WSAEINVAL = 10022, - + EINVAL = 10022, /// Too many open files. /// Too many open sockets. Each implementation may have a maximum number of socket handles available, either globally, per process, or per thread. - WSAEMFILE = 10024, - + EMFILE = 10024, /// Resource temporarily unavailable. /// This error is returned from operations on nonblocking sockets that cannot be completed immediately, for example recv when no data is queued to be read from the socket. /// It is a nonfatal error, and the operation should be retried later. /// It is normal for WSAEWOULDBLOCK to be reported as the result from calling connect on a nonblocking SOCK.STREAM socket, since some time must elapse for the connection to be established. - WSAEWOULDBLOCK = 10035, - + EWOULDBLOCK = 10035, /// Operation now in progress. /// A blocking operation is currently executing. /// Windows Sockets only allows a single blocking operation—per- task or thread—to be outstanding, and if any other function call is made (whether or not it references that or any other socket) the function fails with the WSAEINPROGRESS error. - WSAEINPROGRESS = 10036, - + EINPROGRESS = 10036, /// Operation already in progress. /// An operation was attempted on a nonblocking socket with an operation already in progress—that is, calling connect a second time on a nonblocking socket that is already connecting, or canceling an asynchronous request (WSAAsyncGetXbyY) that has already been canceled or completed. - WSAEALREADY = 10037, - + EALREADY = 10037, /// Socket operation on nonsocket. /// An operation was attempted on something that is not a socket. /// Either the socket handle parameter did not reference a valid socket, or for select, a member of an fd_set was not valid. - WSAENOTSOCK = 10038, - + ENOTSOCK = 10038, /// Destination address required. /// A required address was omitted from an operation on a socket. /// For example, this error is returned if sendto is called with the remote address of ADDR_ANY. - WSAEDESTADDRREQ = 10039, - + EDESTADDRREQ = 10039, /// Message too long. /// A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram was smaller than the datagram itself. - WSAEMSGSIZE = 10040, - + EMSGSIZE = 10040, /// Protocol wrong type for socket. /// A protocol was specified in the socket function call that does not support the semantics of the socket type requested. /// For example, the ARPA Internet UDP protocol cannot be specified with a socket type of SOCK.STREAM. - WSAEPROTOTYPE = 10041, - + EPROTOTYPE = 10041, /// Bad protocol option. /// An unknown, invalid or unsupported option or level was specified in a getsockopt or setsockopt call. - WSAENOPROTOOPT = 10042, - + ENOPROTOOPT = 10042, /// Protocol not supported. /// The requested protocol has not been configured into the system, or no implementation for it exists. /// For example, a socket call requests a SOCK.DGRAM socket, but specifies a stream protocol. - WSAEPROTONOSUPPORT = 10043, - + EPROTONOSUPPORT = 10043, /// Socket type not supported. /// The support for the specified socket type does not exist in this address family. /// For example, the optional type SOCK.RAW might be selected in a socket call, and the implementation does not support SOCK.RAW sockets at all. - WSAESOCKTNOSUPPORT = 10044, - + ESOCKTNOSUPPORT = 10044, /// Operation not supported. /// The attempted operation is not supported for the type of object referenced. /// Usually this occurs when a socket descriptor to a socket that cannot support this operation is trying to accept a connection on a datagram socket. - WSAEOPNOTSUPP = 10045, - + EOPNOTSUPP = 10045, /// Protocol family not supported. /// The protocol family has not been configured into the system or no implementation for it exists. /// This message has a slightly different meaning from WSAEAFNOSUPPORT. /// However, it is interchangeable in most cases, and all Windows Sockets functions that return one of these messages also specify WSAEAFNOSUPPORT. - WSAEPFNOSUPPORT = 10046, - + EPFNOSUPPORT = 10046, /// Address family not supported by protocol family. /// An address incompatible with the requested protocol was used. /// All sockets are created with an associated address family (that is, AF.INET for Internet Protocols) and a generic protocol type (that is, SOCK.STREAM). /// This error is returned if an incorrect protocol is explicitly requested in the socket call, or if an address of the wrong family is used for a socket, for example, in sendto. - WSAEAFNOSUPPORT = 10047, - + EAFNOSUPPORT = 10047, /// Address already in use. /// Typically, only one usage of each socket address (protocol/IP address/port) is permitted. /// This error occurs if an application attempts to bind a socket to an IP address/port that has already been used for an existing socket, or a socket that was not closed properly, or one that is still in the process of closing. @@ -1402,115 +1377,91 @@ pub const WinsockError = enum(u16) { /// Client applications usually need not call bind at all—connect chooses an unused port automatically. /// When bind is called with a wildcard address (involving ADDR_ANY), a WSAEADDRINUSE error could be delayed until the specific address is committed. /// This could happen with a call to another function later, including connect, listen, WSAConnect, or WSAJoinLeaf. - WSAEADDRINUSE = 10048, - + EADDRINUSE = 10048, /// Cannot assign requested address. /// The requested address is not valid in its context. /// This normally results from an attempt to bind to an address that is not valid for the local computer. /// This can also result from connect, sendto, WSAConnect, WSAJoinLeaf, or WSASendTo when the remote address or port is not valid for a remote computer (for example, address or port 0). - WSAEADDRNOTAVAIL = 10049, - + EADDRNOTAVAIL = 10049, /// Network is down. /// A socket operation encountered a dead network. /// This could indicate a serious failure of the network system (that is, the protocol stack that the Windows Sockets DLL runs over), the network interface, or the local network itself. - WSAENETDOWN = 10050, - + ENETDOWN = 10050, /// Network is unreachable. /// A socket operation was attempted to an unreachable network. /// This usually means the local software knows no route to reach the remote host. - WSAENETUNREACH = 10051, - + ENETUNREACH = 10051, /// Network dropped connection on reset. /// The connection has been broken due to keep-alive activity detecting a failure while the operation was in progress. /// It can also be returned by setsockopt if an attempt is made to set SO.KEEPALIVE on a connection that has already failed. - WSAENETRESET = 10052, - + ENETRESET = 10052, /// Software caused connection abort. /// An established connection was aborted by the software in your host computer, possibly due to a data transmission time-out or protocol error. - WSAECONNABORTED = 10053, - + ECONNABORTED = 10053, /// Connection reset by peer. /// An existing connection was forcibly closed by the remote host. /// This normally results if the peer application on the remote host is suddenly stopped, the host is rebooted, the host or remote network interface is disabled, or the remote host uses a hard close (see setsockopt for more information on the SO.LINGER option on the remote socket). /// This error may also result if a connection was broken due to keep-alive activity detecting a failure while one or more operations are in progress. /// Operations that were in progress fail with WSAENETRESET. Subsequent operations fail with WSAECONNRESET. - WSAECONNRESET = 10054, - + ECONNRESET = 10054, /// No buffer space available. /// An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full. - WSAENOBUFS = 10055, - + ENOBUFS = 10055, /// Socket is already connected. /// A connect request was made on an already-connected socket. /// Some implementations also return this error if sendto is called on a connected SOCK.DGRAM socket (for SOCK.STREAM sockets, the to parameter in sendto is ignored) although other implementations treat this as a legal occurrence. - WSAEISCONN = 10056, - + EISCONN = 10056, /// Socket is not connected. /// A request to send or receive data was disallowed because the socket is not connected and (when sending on a datagram socket using sendto) no address was supplied. /// Any other type of operation might also return this error—for example, setsockopt setting SO.KEEPALIVE if the connection has been reset. - WSAENOTCONN = 10057, - + ENOTCONN = 10057, /// Cannot send after socket shutdown. /// A request to send or receive data was disallowed because the socket had already been shut down in that direction with a previous shutdown call. /// By calling shutdown a partial close of a socket is requested, which is a signal that sending or receiving, or both have been discontinued. - WSAESHUTDOWN = 10058, - + ESHUTDOWN = 10058, /// Too many references. /// Too many references to some kernel object. - WSAETOOMANYREFS = 10059, - + ETOOMANYREFS = 10059, /// Connection timed out. /// A connection attempt failed because the connected party did not properly respond after a period of time, or the established connection failed because the connected host has failed to respond. - WSAETIMEDOUT = 10060, - + ETIMEDOUT = 10060, /// Connection refused. /// No connection could be made because the target computer actively refused it. /// This usually results from trying to connect to a service that is inactive on the foreign host—that is, one with no server application running. - WSAECONNREFUSED = 10061, - + ECONNREFUSED = 10061, /// Cannot translate name. /// Cannot translate a name. - WSAELOOP = 10062, - + ELOOP = 10062, /// Name too long. /// A name component or a name was too long. - WSAENAMETOOLONG = 10063, - + ENAMETOOLONG = 10063, /// Host is down. /// A socket operation failed because the destination host is down. A socket operation encountered a dead host. /// Networking activity on the local host has not been initiated. /// These conditions are more likely to be indicated by the error WSAETIMEDOUT. - WSAEHOSTDOWN = 10064, - + EHOSTDOWN = 10064, /// No route to host. /// A socket operation was attempted to an unreachable host. See WSAENETUNREACH. - WSAEHOSTUNREACH = 10065, - + EHOSTUNREACH = 10065, /// Directory not empty. /// Cannot remove a directory that is not empty. - WSAENOTEMPTY = 10066, - + ENOTEMPTY = 10066, /// Too many processes. /// A Windows Sockets implementation may have a limit on the number of applications that can use it simultaneously. /// WSAStartup may fail with this error if the limit has been reached. - WSAEPROCLIM = 10067, - + EPROCLIM = 10067, /// User quota exceeded. /// Ran out of user quota. - WSAEUSERS = 10068, - + EUSERS = 10068, /// Disk quota exceeded. /// Ran out of disk quota. - WSAEDQUOT = 10069, - + EDQUOT = 10069, /// Stale file handle reference. /// The file handle reference is no longer available. - WSAESTALE = 10070, - + ESTALE = 10070, /// Item is remote. /// The item is not available locally. - WSAEREMOTE = 10071, - + EREMOTE = 10071, /// Network subsystem is unavailable. /// This error is returned by WSAStartup if the Windows Sockets implementation cannot function at this time because the underlying system it uses to provide network services is currently unavailable. /// Users should check: @@ -1518,47 +1469,38 @@ pub const WinsockError = enum(u16) { /// - That they are not trying to use more than one Windows Sockets implementation simultaneously. /// - If there is more than one Winsock DLL on your system, be sure the first one in the path is appropriate for the network subsystem currently loaded. /// - The Windows Sockets implementation documentation to be sure all necessary components are currently installed and configured correctly. - WSASYSNOTREADY = 10091, - + SYSNOTREADY = 10091, /// Winsock.dll version out of range. /// The current Windows Sockets implementation does not support the Windows Sockets specification version requested by the application. /// Check that no old Windows Sockets DLL files are being accessed. - WSAVERNOTSUPPORTED = 10092, - + VERNOTSUPPORTED = 10092, /// Successful WSAStartup not yet performed. /// Either the application has not called WSAStartup or WSAStartup failed. /// The application may be accessing a socket that the current active task does not own (that is, trying to share a socket between tasks), or WSACleanup has been called too many times. - WSANOTINITIALISED = 10093, - + NOTINITIALISED = 10093, /// Graceful shutdown in progress. /// Returned by WSARecv and WSARecvFrom to indicate that the remote party has initiated a graceful shutdown sequence. - WSAEDISCON = 10101, - + EDISCON = 10101, /// No more results. /// No more results can be returned by the WSALookupServiceNext function. - WSAENOMORE = 10102, - + ENOMORE = 10102, /// Call has been canceled. /// A call to the WSALookupServiceEnd function was made while this call was still processing. The call has been canceled. - WSAECANCELLED = 10103, - + ECANCELLED = 10103, /// Procedure call table is invalid. /// The service provider procedure call table is invalid. /// A service provider returned a bogus procedure table to Ws2_32.dll. /// This is usually caused by one or more of the function pointers being NULL. - WSAEINVALIDPROCTABLE = 10104, - + EINVALIDPROCTABLE = 10104, /// Service provider is invalid. /// The requested service provider is invalid. /// This error is returned by the WSCGetProviderInfo and WSCGetProviderInfo32 functions if the protocol entry specified could not be found. /// This error is also returned if the service provider returned a version number other than 2.0. - WSAEINVALIDPROVIDER = 10105, - + EINVALIDPROVIDER = 10105, /// Service provider failed to initialize. /// The requested service provider could not be loaded or initialized. /// This error is returned if either a service provider's DLL could not be loaded (LoadLibrary failed) or the provider's WSPStartup or NSPStartup function failed. - WSAEPROVIDERFAILEDINIT = 10106, - + EPROVIDERFAILEDINIT = 10106, /// System call failure. /// A system call that should never fail has failed. /// This is a generic error code, returned under various conditions. @@ -1566,157 +1508,120 @@ pub const WinsockError = enum(u16) { /// For example, if a call to WaitForMultipleEvents fails or one of the registry functions fails trying to manipulate the protocol/namespace catalogs. /// Returned when a provider does not return SUCCESS and does not provide an extended error code. /// Can indicate a service provider implementation error. - WSASYSCALLFAILURE = 10107, - + SYSCALLFAILURE = 10107, /// Service not found. /// No such service is known. The service cannot be found in the specified name space. - WSASERVICE_NOT_FOUND = 10108, - + SERVICE_NOT_FOUND = 10108, /// Class type not found. /// The specified class was not found. - WSATYPE_NOT_FOUND = 10109, - + TYPE_NOT_FOUND = 10109, /// No more results. /// No more results can be returned by the WSALookupServiceNext function. - WSA_E_NO_MORE = 10110, - + E_NO_MORE = 10110, /// Call was canceled. /// A call to the WSALookupServiceEnd function was made while this call was still processing. The call has been canceled. - WSA_E_CANCELLED = 10111, - + E_CANCELLED = 10111, /// Database query was refused. /// A database query failed because it was actively refused. - WSAEREFUSED = 10112, - + EREFUSED = 10112, /// Host not found. /// No such host is known. The name is not an official host name or alias, or it cannot be found in the database(s) being queried. /// This error may also be returned for protocol and service queries, and means that the specified name could not be found in the relevant database. - WSAHOST_NOT_FOUND = 11001, - + HOST_NOT_FOUND = 11001, /// Nonauthoritative host not found. /// This is usually a temporary error during host name resolution and means that the local server did not receive a response from an authoritative server. A retry at some time later may be successful. - WSATRY_AGAIN = 11002, - + TRY_AGAIN = 11002, /// This is a nonrecoverable error. /// This indicates that some sort of nonrecoverable error occurred during a database lookup. /// This may be because the database files (for example, BSD-compatible HOSTS, SERVICES, or PROTOCOLS files) could not be found, or a DNS request was returned by the server with a severe error. - WSANO_RECOVERY = 11003, - + NO_RECOVERY = 11003, /// Valid name, no data record of requested type. /// The requested name is valid and was found in the database, but it does not have the correct associated data being resolved for. /// The usual example for this is a host name-to-address translation attempt (using gethostbyname or WSAAsyncGetHostByName) which uses the DNS (Domain Name Server). /// An MX record is returned but no A record—indicating the host itself exists, but is not directly reachable. - WSANO_DATA = 11004, - + NO_DATA = 11004, /// QoS receivers. /// At least one QoS reserve has arrived. - WSA_QOS_RECEIVERS = 11005, - + QOS_RECEIVERS = 11005, /// QoS senders. /// At least one QoS send path has arrived. - WSA_QOS_SENDERS = 11006, - + QOS_SENDERS = 11006, /// No QoS senders. /// There are no QoS senders. - WSA_QOS_NO_SENDERS = 11007, - + QOS_NO_SENDERS = 11007, /// QoS no receivers. /// There are no QoS receivers. - WSA_QOS_NO_RECEIVERS = 11008, - + QOS_NO_RECEIVERS = 11008, /// QoS request confirmed. /// The QoS reserve request has been confirmed. - WSA_QOS_REQUEST_CONFIRMED = 11009, - + QOS_REQUEST_CONFIRMED = 11009, /// QoS admission error. /// A QoS error occurred due to lack of resources. - WSA_QOS_ADMISSION_FAILURE = 11010, - + QOS_ADMISSION_FAILURE = 11010, /// QoS policy failure. /// The QoS request was rejected because the policy system couldn't allocate the requested resource within the existing policy. - WSA_QOS_POLICY_FAILURE = 11011, - + QOS_POLICY_FAILURE = 11011, /// QoS bad style. /// An unknown or conflicting QoS style was encountered. - WSA_QOS_BAD_STYLE = 11012, - + QOS_BAD_STYLE = 11012, /// QoS bad object. /// A problem was encountered with some part of the filterspec or the provider-specific buffer in general. - WSA_QOS_BAD_OBJECT = 11013, - + QOS_BAD_OBJECT = 11013, /// QoS traffic control error. /// An error with the underlying traffic control (TC) API as the generic QoS request was converted for local enforcement by the TC API. /// This could be due to an out of memory error or to an internal QoS provider error. - WSA_QOS_TRAFFIC_CTRL_ERROR = 11014, - + QOS_TRAFFIC_CTRL_ERROR = 11014, /// QoS generic error. /// A general QoS error. - WSA_QOS_GENERIC_ERROR = 11015, - + QOS_GENERIC_ERROR = 11015, /// QoS service type error. /// An invalid or unrecognized service type was found in the QoS flowspec. - WSA_QOS_ESERVICETYPE = 11016, - + QOS_ESERVICETYPE = 11016, /// QoS flowspec error. /// An invalid or inconsistent flowspec was found in the QOS structure. - WSA_QOS_EFLOWSPEC = 11017, - + QOS_EFLOWSPEC = 11017, /// Invalid QoS provider buffer. /// An invalid QoS provider-specific buffer. - WSA_QOS_EPROVSPECBUF = 11018, - + QOS_EPROVSPECBUF = 11018, /// Invalid QoS filter style. /// An invalid QoS filter style was used. - WSA_QOS_EFILTERSTYLE = 11019, - + QOS_EFILTERSTYLE = 11019, /// Invalid QoS filter type. /// An invalid QoS filter type was used. - WSA_QOS_EFILTERTYPE = 11020, - + QOS_EFILTERTYPE = 11020, /// Incorrect QoS filter count. /// An incorrect number of QoS FILTERSPECs were specified in the FLOWDESCRIPTOR. - WSA_QOS_EFILTERCOUNT = 11021, - + QOS_EFILTERCOUNT = 11021, /// Invalid QoS object length. /// An object with an invalid ObjectLength field was specified in the QoS provider-specific buffer. - WSA_QOS_EOBJLENGTH = 11022, - + QOS_EOBJLENGTH = 11022, /// Incorrect QoS flow count. /// An incorrect number of flow descriptors was specified in the QoS structure. - WSA_QOS_EFLOWCOUNT = 11023, - + QOS_EFLOWCOUNT = 11023, /// Unrecognized QoS object. /// An unrecognized object was found in the QoS provider-specific buffer. - WSA_QOS_EUNKOWNPSOBJ = 11024, - + QOS_EUNKOWNPSOBJ = 11024, /// Invalid QoS policy object. /// An invalid policy object was found in the QoS provider-specific buffer. - WSA_QOS_EPOLICYOBJ = 11025, - + QOS_EPOLICYOBJ = 11025, /// Invalid QoS flow descriptor. /// An invalid QoS flow descriptor was found in the flow descriptor list. - WSA_QOS_EFLOWDESC = 11026, - + QOS_EFLOWDESC = 11026, /// Invalid QoS provider-specific flowspec. /// An invalid or inconsistent flowspec was found in the QoS provider-specific buffer. - WSA_QOS_EPSFLOWSPEC = 11027, - + QOS_EPSFLOWSPEC = 11027, /// Invalid QoS provider-specific filterspec. /// An invalid FILTERSPEC was found in the QoS provider-specific buffer. - WSA_QOS_EPSFILTERSPEC = 11028, - + QOS_EPSFILTERSPEC = 11028, /// Invalid QoS shape discard mode object. /// An invalid shape discard mode object was found in the QoS provider-specific buffer. - WSA_QOS_ESDMODEOBJ = 11029, - + QOS_ESDMODEOBJ = 11029, /// Invalid QoS shaping rate object. /// An invalid shaping rate object was found in the QoS provider-specific buffer. - WSA_QOS_ESHAPERATEOBJ = 11030, - + QOS_ESHAPERATEOBJ = 11030, /// Reserved policy QoS element type. /// A reserved policy element was found in the QoS provider-specific buffer. - WSA_QOS_RESERVED_PETYPE = 11031, - + QOS_RESERVED_PETYPE = 11031, _, }; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index aa26041c03..04e949036e 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3290,33 +3290,6 @@ pub const SocketError = error{ } || UnexpectedError; pub fn socket(domain: u32, socket_type: u32, protocol: u32) SocketError!socket_t { - if (native_os == .windows) { - // These flags are not actually part of the Windows API, instead they are converted here for compatibility - const filtered_sock_type = socket_type & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC); - var flags: u32 = windows.ws2_32.WSA_FLAG_OVERLAPPED; - if ((socket_type & SOCK.CLOEXEC) != 0) flags |= windows.ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; - - const rc = try windows.WSASocketW( - @bitCast(domain), - @bitCast(filtered_sock_type), - @bitCast(protocol), - null, - 0, - flags, - ); - errdefer windows.closesocket(rc) catch unreachable; - if ((socket_type & SOCK.NONBLOCK) != 0) { - var mode: c_ulong = 1; // nonblocking - if (windows.ws2_32.SOCKET_ERROR == windows.ws2_32.ioctlsocket(rc, windows.ws2_32.FIONBIO, &mode)) { - switch (windows.ws2_32.WSAGetLastError()) { - // have not identified any error codes that should be handled yet - else => unreachable, - } - } - } - return rc; - } - const have_sock_flags = !builtin.target.os.tag.isDarwin() and native_os != .haiku; const filtered_sock_type = if (!have_sock_flags) socket_type & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC) @@ -3411,14 +3384,14 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { .both => windows.ws2_32.SD_BOTH, }); if (0 != result) switch (windows.ws2_32.WSAGetLastError()) { - .WSAECONNABORTED => return error.ConnectionAborted, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEINPROGRESS => return error.BlockingOperationInProgress, - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAENOTSOCK => unreachable, - .WSANOTINITIALISED => unreachable, + .ECONNABORTED => return error.ConnectionAborted, + .ECONNRESET => return error.ConnectionResetByPeer, + .EINPROGRESS => return error.BlockingOperationInProgress, + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENOTCONN => return error.SocketUnconnected, + .ENOTSOCK => unreachable, + .NOTINITIALISED => unreachable, else => |err| return windows.unexpectedWSAError(err), }; } else { @@ -3440,70 +3413,17 @@ pub fn shutdown(sock: socket_t, how: ShutdownHow) ShutdownError!void { } pub const BindError = error{ - /// The address is protected, and the user is not the superuser. - /// For UNIX domain sockets: Search permission is denied on a component - /// of the path prefix. - AccessDenied, - - /// The given address is already in use, or in the case of Internet domain sockets, - /// The port number was specified as zero in the socket - /// address structure, but, upon attempting to bind to an ephemeral port, it was - /// determined that all port numbers in the ephemeral port range are currently in - /// use. See the discussion of /proc/sys/net/ipv4/ip_local_port_range ip(7). - AddressInUse, - - /// A nonexistent interface was requested or the requested address was not local. - AddressNotAvailable, - - /// The address is not valid for the address family of socket. - AddressFamilyUnsupported, - - /// Too many symbolic links were encountered in resolving addr. SymLinkLoop, - - /// addr is too long. NameTooLong, - - /// A component in the directory prefix of the socket pathname does not exist. FileNotFound, - - /// Insufficient kernel memory was available. - SystemResources, - - /// A component of the path prefix is not a directory. NotDir, - - /// The socket inode would reside on a read-only filesystem. ReadOnlyFileSystem, + AccessDenied, +} || std.Io.net.IpAddress.BindError; - /// The network subsystem has failed. - NetworkDown, - - FileDescriptorNotASocket, - - AlreadyBound, -} || UnexpectedError; - -/// addr is `*const T` where T is one of the sockaddr pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!void { if (native_os == .windows) { - const rc = windows.bind(sock, addr, len); - if (rc == windows.ws2_32.SOCKET_ERROR) { - switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAEACCES => return error.AccessDenied, - .WSAEADDRINUSE => return error.AddressInUse, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEFAULT => unreachable, // invalid pointers - .WSAEINVAL => return error.AlreadyBound, - .WSAENOBUFS => return error.SystemResources, - .WSAENETDOWN => return error.NetworkDown, - else => |err| return windows.unexpectedWSAError(err), - } - unreachable; - } - return; + @compileError("use std.Io instead"); } else { const rc = system.bind(sock, addr, len); switch (errno(rc)) { @@ -3514,7 +3434,7 @@ pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!voi .INVAL => unreachable, // invalid parameters .NOTSOCK => unreachable, // invalid `sockfd` .AFNOSUPPORT => return error.AddressFamilyUnsupported, - .ADDRNOTAVAIL => return error.AddressNotAvailable, + .ADDRNOTAVAIL => return error.AddressUnavailable, .FAULT => unreachable, // invalid `addr` pointer .LOOP => return error.SymLinkLoop, .NAMETOOLONG => return error.NameTooLong, @@ -3529,51 +3449,13 @@ pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!voi } pub const ListenError = error{ - /// Another socket is already listening on the same port. - /// For Internet domain sockets, the socket referred to by sockfd had not previously - /// been bound to an address and, upon attempting to bind it to an ephemeral port, it - /// was determined that all port numbers in the ephemeral port range are currently in - /// use. See the discussion of /proc/sys/net/ipv4/ip_local_port_range in ip(7). - AddressInUse, - - /// The file descriptor sockfd does not refer to a socket. FileDescriptorNotASocket, - - /// The socket is not of a type that supports the listen() operation. OperationNotSupported, - - /// The network subsystem has failed. - NetworkDown, - - /// Ran out of system resources - /// On Windows it can either run out of socket descriptors or buffer space - SystemResources, - - /// Already connected - AlreadyConnected, - - /// Socket has not been bound yet - SocketNotBound, -} || UnexpectedError; +} || std.Io.net.IpAddress.ListenError || std.Io.net.UnixAddress.ListenError; pub fn listen(sock: socket_t, backlog: u31) ListenError!void { if (native_os == .windows) { - const rc = windows.listen(sock, backlog); - if (rc == windows.ws2_32.SOCKET_ERROR) { - switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAENETDOWN => return error.NetworkDown, - .WSAEADDRINUSE => return error.AddressInUse, - .WSAEISCONN => return error.AlreadyConnected, - .WSAEINVAL => return error.SocketNotBound, - .WSAEMFILE, .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEOPNOTSUPP => return error.OperationNotSupported, - .WSAEINPROGRESS => unreachable, - else => |err| return windows.unexpectedWSAError(err), - } - } - return; + @compileError("use std.Io instead"); } else { const rc = system.listen(sock, backlog); switch (errno(rc)) { @@ -3630,16 +3512,16 @@ pub fn accept( if (native_os == .windows) { if (rc == windows.ws2_32.INVALID_SOCKET) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, // not initialized WSA - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEFAULT => unreachable, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotListening, - .WSAEMFILE => return error.ProcessFdQuotaExceeded, - .WSAENETDOWN => return error.NetworkDown, - .WSAENOBUFS => return error.FileDescriptorNotASocket, - .WSAEOPNOTSUPP => return error.OperationNotSupported, - .WSAEWOULDBLOCK => return error.WouldBlock, + .NOTINITIALISED => unreachable, // not initialized WSA + .ECONNRESET => return error.ConnectionResetByPeer, + .EFAULT => unreachable, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotListening, + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENETDOWN => return error.NetworkDown, + .ENOBUFS => return error.FileDescriptorNotASocket, + .EOPNOTSUPP => return error.OperationNotSupported, + .EWOULDBLOCK => return error.WouldBlock, else => |err| return windows.unexpectedWSAError(err), } } else { @@ -3706,9 +3588,9 @@ fn setSockFlags(sock: socket_t, flags: u32) !void { var mode: c_ulong = 1; if (windows.ws2_32.ioctlsocket(sock, windows.ws2_32.FIONBIO, &mode) == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENOTSOCK => return error.FileDescriptorNotASocket, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), } @@ -3861,11 +3743,11 @@ pub fn getsockname(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock const rc = windows.getsockname(sock, addr, addrlen); if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAEFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotBound, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .EFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotBound, else => |err| return windows.unexpectedWSAError(err), } } @@ -3890,11 +3772,11 @@ pub fn getpeername(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock const rc = windows.getpeername(sock, addr, addrlen); if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAEFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotBound, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .EFAULT => unreachable, // addr or addrlen have invalid pointers or addrlen points to an incorrect value + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotBound, else => |err| return windows.unexpectedWSAError(err), } } @@ -3932,7 +3814,7 @@ pub const ConnectError = error{ /// address and, upon attempting to bind it to an ephemeral port, it was determined that all port numbers /// in the ephemeral port range are currently in use. See the discussion of /// /proc/sys/net/ipv4/ip_local_port_range in ip(7). - AddressNotAvailable, + AddressUnavailable, /// The passed address didn't have the correct address family in its sa_family field. AddressFamilyUnsupported, @@ -3975,22 +3857,22 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne const rc = windows.ws2_32.connect(sock, sock_addr, @intCast(len)); if (rc == 0) return; switch (windows.ws2_32.WSAGetLastError()) { - .WSAEADDRINUSE => return error.AddressInUse, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNREFUSED => return error.ConnectionRefused, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAETIMEDOUT => return error.Timeout, - .WSAEHOSTUNREACH, // TODO: should we return NetworkUnreachable in this case as well? - .WSAENETUNREACH, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNREFUSED => return error.ConnectionRefused, + .ECONNRESET => return error.ConnectionResetByPeer, + .ETIMEDOUT => return error.Timeout, + .EHOSTUNREACH, // TODO: should we return NetworkUnreachable in this case as well? + .ENETUNREACH, => return error.NetworkUnreachable, - .WSAEFAULT => unreachable, - .WSAEINVAL => unreachable, - .WSAEISCONN => return error.AlreadyConnected, - .WSAENOTSOCK => unreachable, - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSAEACCES => unreachable, - .WSAENOBUFS => return error.SystemResources, - .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EFAULT => unreachable, + .EINVAL => unreachable, + .EISCONN => return error.AlreadyConnected, + .ENOTSOCK => unreachable, + .EWOULDBLOCK => return error.WouldBlock, + .EACCES => unreachable, + .ENOBUFS => return error.SystemResources, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, else => |err| return windows.unexpectedWSAError(err), } return; @@ -4002,7 +3884,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, .ADDRINUSE => return error.AddressInUse, - .ADDRNOTAVAIL => return error.AddressNotAvailable, + .ADDRNOTAVAIL => return error.AddressUnavailable, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, .ALREADY => return error.ConnectionPending, @@ -4064,7 +3946,7 @@ pub fn getsockoptError(sockfd: fd_t) ConnectError!void { .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, .ADDRINUSE => return error.AddressInUse, - .ADDRNOTAVAIL => return error.AddressNotAvailable, + .ADDRNOTAVAIL => return error.AddressUnavailable, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN => return error.SystemResources, .ALREADY => return error.ConnectionPending, @@ -5686,7 +5568,7 @@ pub const SendMsgError = SendError || error{ /// The socket is not connected (connection-oriented sockets only). SocketUnconnected, - AddressNotAvailable, + AddressUnavailable, }; pub fn sendmsg( @@ -5701,25 +5583,25 @@ pub fn sendmsg( if (native_os == .windows) { if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSAEACCES => return error.AccessDenied, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageOversize, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, - .WSAEDESTADDRREQ => unreachable, // A destination address is required. - .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. - .WSAEHOSTUNREACH => return error.NetworkUnreachable, - // TODO: WSAEINPROGRESS, WSAEINTR - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + .EACCES => return error.AccessDenied, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EMSGSIZE => return error.MessageOversize, + .ENOBUFS => return error.SystemResources, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EDESTADDRREQ => unreachable, // A destination address is required. + .EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .EHOSTUNREACH => return error.NetworkUnreachable, + // TODO: EINPROGRESS, EINTR + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENETUNREACH => return error.NetworkUnreachable, + .ENOTCONN => return error.SocketUnconnected, + .ESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. + .EWOULDBLOCK => return error.WouldBlock, + .NOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. else => |err| return windows.unexpectedWSAError(err), } } else { @@ -5804,25 +5686,25 @@ pub fn sendto( if (native_os == .windows) { switch (windows.sendto(sockfd, buf.ptr, buf.len, flags, dest_addr, addrlen)) { windows.ws2_32.SOCKET_ERROR => switch (windows.ws2_32.WSAGetLastError()) { - .WSAEACCES => return error.AccessDenied, - .WSAEADDRNOTAVAIL => return error.AddressNotAvailable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEMSGSIZE => return error.MessageOversize, - .WSAENOBUFS => return error.SystemResources, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEAFNOSUPPORT => return error.AddressFamilyUnsupported, - .WSAEDESTADDRREQ => unreachable, // A destination address is required. - .WSAEFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. - .WSAEHOSTUNREACH => return error.NetworkUnreachable, - // TODO: WSAEINPROGRESS, WSAEINTR - .WSAEINVAL => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAENETRESET => return error.ConnectionResetByPeer, - .WSAENETUNREACH => return error.NetworkUnreachable, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSANOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + .EACCES => return error.AccessDenied, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EMSGSIZE => return error.MessageOversize, + .ENOBUFS => return error.SystemResources, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EDESTADDRREQ => unreachable, // A destination address is required. + .EFAULT => unreachable, // The lpBuffers, lpTo, lpOverlapped, lpNumberOfBytesSent, or lpCompletionRoutine parameters are not part of the user address space, or the lpTo parameter is too small. + .EHOSTUNREACH => return error.NetworkUnreachable, + // TODO: EINPROGRESS, EINTR + .EINVAL => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENETUNREACH => return error.NetworkUnreachable, + .ENOTCONN => return error.SocketUnconnected, + .ESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. + .EWOULDBLOCK => return error.WouldBlock, + .NOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. else => |err| return windows.unexpectedWSAError(err), }, else => |rc| return @intCast(rc), @@ -5896,7 +5778,7 @@ pub fn send( error.FileNotFound => unreachable, error.NotDir => unreachable, error.NetworkUnreachable => unreachable, - error.AddressNotAvailable => unreachable, + error.AddressUnavailable => unreachable, error.SocketUnconnected => unreachable, error.UnreachableAddress => unreachable, else => |e| return e, @@ -6007,9 +5889,9 @@ pub fn poll(fds: []pollfd, timeout: i32) PollError!usize { if (native_os == .windows) { switch (windows.poll(fds.ptr, @intCast(fds.len), timeout)) { windows.ws2_32.SOCKET_ERROR => switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAENOBUFS => return error.SystemResources, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .ENOBUFS => return error.SystemResources, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), }, @@ -6107,14 +5989,14 @@ pub fn recvfrom( if (native_os == .windows) { if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAECONNRESET => return error.ConnectionResetByPeer, - .WSAEINVAL => return error.SocketNotBound, - .WSAEMSGSIZE => return error.MessageOversize, - .WSAENETDOWN => return error.NetworkDown, - .WSAENOTCONN => return error.SocketUnconnected, - .WSAEWOULDBLOCK => return error.WouldBlock, - .WSAETIMEDOUT => return error.Timeout, + .NOTINITIALISED => unreachable, + .ECONNRESET => return error.ConnectionResetByPeer, + .EINVAL => return error.SocketNotBound, + .EMSGSIZE => return error.MessageOversize, + .ENETDOWN => return error.NetworkDown, + .ENOTCONN => return error.SocketUnconnected, + .EWOULDBLOCK => return error.WouldBlock, + .ETIMEDOUT => return error.Timeout, // TODO: handle more errors else => |err| return windows.unexpectedWSAError(err), } @@ -6220,11 +6102,11 @@ pub fn setsockopt(fd: socket_t, level: i32, optname: u32, opt: []const u8) SetSo const rc = windows.ws2_32.setsockopt(fd, level, @intCast(optname), opt.ptr, @intCast(opt.len)); if (rc == windows.ws2_32.SOCKET_ERROR) { switch (windows.ws2_32.WSAGetLastError()) { - .WSANOTINITIALISED => unreachable, - .WSAENETDOWN => return error.NetworkDown, - .WSAEFAULT => unreachable, - .WSAENOTSOCK => return error.FileDescriptorNotASocket, - .WSAEINVAL => return error.SocketNotBound, + .NOTINITIALISED => unreachable, + .ENETDOWN => return error.NetworkDown, + .EFAULT => unreachable, + .ENOTSOCK => return error.FileDescriptorNotASocket, + .EINVAL => return error.SocketNotBound, else => |err| return windows.unexpectedWSAError(err), } } diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 3f4b11c1af..3e077fe300 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -520,25 +520,6 @@ test "getrlimit and setrlimit" { } } -test "shutdown socket" { - if (native_os == .wasi) - return error.SkipZigTest; - if (native_os == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (native_os == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - const sock = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0); - posix.shutdown(sock, .both) catch |err| switch (err) { - error.SocketUnconnected => {}, - else => |e| return e, - }; - std.posix.close(sock); -} - test "sigrtmin/max" { if (native_os == .wasi or native_os == .windows or native_os == .macos) { return error.SkipZigTest; From 0b5179a231c3f639f2fe3f2cafffa8b2cfd02482 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 20 Oct 2025 22:50:30 -0700 Subject: [PATCH 158/244] std.Io.Threaded: implement netAccept for Windows --- lib/std/Io/Threaded.zig | 34 +++++++++++++++++++++++++++++++++- lib/std/os/windows.zig | 5 ----- lib/std/posix.zig | 41 +---------------------------------------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index e03440e9d3..b96136d151 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -244,7 +244,7 @@ pub fn io(t: *Threaded) Io { }, .netListenUnix = netListenUnix, .netAccept = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => netAcceptWindows, else => netAcceptPosix, }, .netBindIp = switch (builtin.os.tag) { @@ -3288,6 +3288,38 @@ fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Serve } }; } +fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + var storage: WsaAddress = undefined; + var addr_len: i32 = @sizeOf(WsaAddress); + while (true) { + try t.checkCancel(); + const rc = ws2_32.accept(listen_handle, &storage.any, &addr_len); + if (rc != windows.ws2_32.INVALID_SOCKET) return .{ .socket = .{ + .handle = rc, + .address = addressFromWsa(&storage), + } }; + switch (windows.ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ECONNRESET => return error.ConnectionAborted, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENETDOWN => return error.NetworkDown, + .ENOBUFS => return error.SystemResources, + .EOPNOTSUPP => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 7609612c26..ad02698354 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -1574,11 +1574,6 @@ pub fn GetFileAttributesW(lpFileName: [*:0]const u16) GetFileAttributesError!DWO return rc; } -pub fn accept(s: ws2_32.SOCKET, name: ?*ws2_32.sockaddr, namelen: ?*ws2_32.socklen_t) ws2_32.SOCKET { - assert((name == null) == (namelen == null)); - return ws2_32.accept(s, name, @as(?*i32, @ptrCast(namelen))); -} - pub fn getpeername(s: ws2_32.SOCKET, name: *ws2_32.sockaddr, namelen: *ws2_32.socklen_t) i32 { return ws2_32.getpeername(s, name, @as(*i32, @ptrCast(namelen))); } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 04e949036e..dd0ad16695 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3471,31 +3471,10 @@ pub fn listen(sock: socket_t, backlog: u31) ListenError!void { pub const AcceptError = std.Io.net.Server.AcceptError; -/// Accept a connection on a socket. -/// If `sockfd` is opened in non blocking mode, the function will -/// return error.WouldBlock when EAGAIN is received. pub fn accept( - /// This argument is a socket that has been created with `socket`, bound to a local address - /// with `bind`, and is listening for connections after a `listen`. sock: socket_t, - /// This argument is a pointer to a sockaddr structure. This structure is filled in with the - /// address of the peer socket, as known to the communications layer. The exact format of the - /// address returned addr is determined by the socket's address family (see `socket` and the - /// respective protocol man pages). addr: ?*sockaddr, - /// This argument is a value-result argument: the caller must initialize it to contain the - /// size (in bytes) of the structure pointed to by addr; on return it will contain the actual size - /// of the peer address. - /// - /// The returned address is truncated if the buffer provided is too small; in this case, `addr_size` - /// will return a value greater than was supplied to the call. addr_size: ?*socklen_t, - /// The following values can be bitwise ORed in flags to obtain different behavior: - /// * `SOCK.NONBLOCK` - Set the `NONBLOCK` file status flag on the open file description (see `open`) - /// referred to by the new file descriptor. Using this flag saves extra calls to `fcntl` to achieve - /// the same result. - /// * `SOCK.CLOEXEC` - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor. See the - /// description of the `CLOEXEC` flag in `open` for reasons why this may be useful. flags: u32, ) AcceptError!socket_t { const have_accept4 = !(builtin.target.os.tag.isDarwin() or native_os == .windows or native_os == .haiku); @@ -3504,29 +3483,11 @@ pub fn accept( const accepted_sock: socket_t = while (true) { const rc = if (have_accept4) system.accept4(sock, addr, addr_size, flags) - else if (native_os == .windows) - windows.accept(sock, addr, addr_size) else system.accept(sock, addr, addr_size); if (native_os == .windows) { - if (rc == windows.ws2_32.INVALID_SOCKET) { - switch (windows.ws2_32.WSAGetLastError()) { - .NOTINITIALISED => unreachable, // not initialized WSA - .ECONNRESET => return error.ConnectionResetByPeer, - .EFAULT => unreachable, - .ENOTSOCK => return error.FileDescriptorNotASocket, - .EINVAL => return error.SocketNotListening, - .EMFILE => return error.ProcessFdQuotaExceeded, - .ENETDOWN => return error.NetworkDown, - .ENOBUFS => return error.FileDescriptorNotASocket, - .EOPNOTSUPP => return error.OperationNotSupported, - .EWOULDBLOCK => return error.WouldBlock, - else => |err| return windows.unexpectedWSAError(err), - } - } else { - break rc; - } + @compileError("use std.Io instead"); } else { switch (errno(rc)) { .SUCCESS => break @intCast(rc), From 76107e9e655378b7d62c1d5d93ef9e17d241975f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 20 Oct 2025 22:59:05 -0700 Subject: [PATCH 159/244] std.Io.Threaded: implement netBindIp for Windows --- lib/std/Io/Threaded.zig | 84 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index b96136d151..4b942e2379 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -248,7 +248,7 @@ pub fn io(t: *Threaded) Io { else => netAcceptPosix, }, .netBindIp = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => netBindIpWindows, else => netBindIpPosix, }, .netConnectIp = switch (builtin.os.tag) { @@ -2828,7 +2828,7 @@ fn netListenIpWindows( .EAFNOSUPPORT => return error.AddressFamilyUnsupported, .EMFILE => return error.ProcessFdQuotaExceeded, .ENOBUFS => return error.SystemResources, - .EPROTONOSUPPORT => return error.ProtocolUnsupportedBySystem, + .EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, else => |err| return windows.unexpectedWSAError(err), } }; @@ -3176,6 +3176,86 @@ fn netBindIpPosix( }; } +fn netBindIpWindows( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.BindOptions, +) IpAddress.BindError!net.Socket { + if (!have_networking) return error.NetworkDown; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const mode = posixSocketMode(options.mode); + const protocol = posixProtocol(options.protocol); + const socket_handle = while (true) { + try t.checkCancel(); + const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; + const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags); + if (rc != ws2_32.INVALID_SOCKET) break rc; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENOBUFS => return error.SystemResources, + .EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + else => |err| return windows.unexpectedWSAError(err), + } + }; + errdefer closeSocketWindows(socket_handle); + var storage: WsaAddress = undefined; + var addr_len = addressToWsa(address, &storage); + + while (true) { + try t.checkCancel(); + const rc = ws2_32.bind(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .ENOBUFS => return error.SystemResources, + .ENETDOWN => return error.NetworkDown, + else => |err| return windows.unexpectedWSAError(err), + } + } + + while (true) { + try t.checkCancel(); + const rc = ws2_32.getsockname(socket_handle, &storage.any, &addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } + + return .{ + .handle = socket_handle, + .address = addressFromWsa(&storage), + }; +} + fn openSocketPosix( t: *Threaded, family: posix.sa_family_t, From 4174ac18e9563c79f723e51db6c3abbb4eb3f73a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 20 Oct 2025 23:25:57 -0700 Subject: [PATCH 160/244] resinator: update for new Io APIs --- lib/compiler/resinator/compile.zig | 23 +++-- lib/compiler/resinator/cvtres.zig | 15 ++- lib/compiler/resinator/errors.zig | 36 +++++-- lib/compiler/resinator/main.zig | 161 ++++++++++++++++------------- lib/compiler/resinator/utils.zig | 6 +- 5 files changed, 147 insertions(+), 94 deletions(-) diff --git a/lib/compiler/resinator/compile.zig b/lib/compiler/resinator/compile.zig index fbb3797fde..df1bbbcddf 100644 --- a/lib/compiler/resinator/compile.zig +++ b/lib/compiler/resinator/compile.zig @@ -1,6 +1,12 @@ -const std = @import("std"); const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + +const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; +const WORD = std.os.windows.WORD; +const DWORD = std.os.windows.DWORD; + const Node = @import("ast.zig").Node; const lex = @import("lex.zig"); const Parser = @import("parse.zig").Parser; @@ -17,8 +23,6 @@ const res = @import("res.zig"); const ico = @import("ico.zig"); const ani = @import("ani.zig"); const bmp = @import("bmp.zig"); -const WORD = std.os.windows.WORD; -const DWORD = std.os.windows.DWORD; const utils = @import("utils.zig"); const NameOrOrdinal = res.NameOrOrdinal; const SupportedCodePage = @import("code_pages.zig").SupportedCodePage; @@ -28,7 +32,6 @@ const windows1252 = @import("windows1252.zig"); const lang = @import("lang.zig"); const code_pages = @import("code_pages.zig"); const errors = @import("errors.zig"); -const native_endian = builtin.cpu.arch.endian(); pub const CompileOptions = struct { cwd: std.fs.Dir, @@ -77,7 +80,7 @@ pub const Dependencies = struct { } }; -pub fn compile(allocator: Allocator, source: []const u8, writer: *std.Io.Writer, options: CompileOptions) !void { +pub fn compile(allocator: Allocator, io: Io, source: []const u8, writer: *std.Io.Writer, options: CompileOptions) !void { var lexer = lex.Lexer.init(source, .{ .default_code_page = options.default_code_page, .source_mappings = options.source_mappings, @@ -166,10 +169,11 @@ pub fn compile(allocator: Allocator, source: []const u8, writer: *std.Io.Writer, defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - var compiler = Compiler{ + var compiler: Compiler = .{ .source = source, .arena = arena, .allocator = allocator, + .io = io, .cwd = options.cwd, .diagnostics = options.diagnostics, .dependencies = options.dependencies, @@ -191,6 +195,7 @@ pub const Compiler = struct { source: []const u8, arena: Allocator, allocator: Allocator, + io: Io, cwd: std.fs.Dir, state: State = .{}, diagnostics: *Diagnostics, @@ -409,7 +414,7 @@ pub const Compiler = struct { } } - var first_error: ?std.fs.File.OpenError = null; + var first_error: ?(std.fs.File.OpenError || std.fs.File.StatError) = null; for (self.search_dirs) |search_dir| { if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| { errdefer file.close(); @@ -496,6 +501,8 @@ pub const Compiler = struct { } pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: *std.Io.Writer) !void { + const io = self.io; + // Init header with data size zero for now, will need to fill it in later var header = try self.resourceHeader(node.id, node.type, .{}); defer header.deinit(self.allocator); @@ -582,7 +589,7 @@ pub const Compiler = struct { }; defer file_handle.close(); var file_buffer: [2048]u8 = undefined; - var file_reader = file_handle.reader(&file_buffer); + var file_reader = file_handle.reader(io, &file_buffer); if (maybe_predefined_type) |predefined_type| { switch (predefined_type) { diff --git a/lib/compiler/resinator/cvtres.zig b/lib/compiler/resinator/cvtres.zig index 50d2c6e96a..26b6620af2 100644 --- a/lib/compiler/resinator/cvtres.zig +++ b/lib/compiler/resinator/cvtres.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const Io = std.Io; const Allocator = std.mem.Allocator; + const res = @import("res.zig"); const NameOrOrdinal = res.NameOrOrdinal; const MemoryFlags = res.MemoryFlags; @@ -169,8 +171,7 @@ pub fn parseNameOrOrdinal(allocator: Allocator, reader: *std.Io.Reader) !NameOrO pub const CoffOptions = struct { target: std.coff.IMAGE.FILE.MACHINE = .AMD64, - /// If true, zeroes will be written to all timestamp fields - reproducible: bool = true, + timestamp: i64 = 0, /// If true, the MEM_WRITE flag will not be set in the .rsrc section header read_only: bool = false, /// If non-null, a symbol with this name and storage class EXTERNAL will be added to the symbol table. @@ -188,7 +189,13 @@ pub const Diagnostics = union { overflow_resource: usize, }; -pub fn writeCoff(allocator: Allocator, writer: *std.Io.Writer, resources: []const Resource, options: CoffOptions, diagnostics: ?*Diagnostics) !void { +pub fn writeCoff( + allocator: Allocator, + writer: *std.Io.Writer, + resources: []const Resource, + options: CoffOptions, + diagnostics: ?*Diagnostics, +) !void { var resource_tree = ResourceTree.init(allocator, options); defer resource_tree.deinit(); @@ -215,7 +222,7 @@ pub fn writeCoff(allocator: Allocator, writer: *std.Io.Writer, resources: []cons const pointer_to_rsrc02_data = pointer_to_relocations + relocations_len; const pointer_to_symbol_table = pointer_to_rsrc02_data + lengths.rsrc02; - const timestamp: i64 = if (options.reproducible) 0 else std.time.timestamp(); + const timestamp: i64 = options.timestamp; const size_of_optional_header = 0; const machine_type: std.coff.IMAGE.FILE.MACHINE = options.target; const flags = std.coff.Header.Flags{ diff --git a/lib/compiler/resinator/errors.zig b/lib/compiler/resinator/errors.zig index 1d9cbf4c5b..aad74a3ca3 100644 --- a/lib/compiler/resinator/errors.zig +++ b/lib/compiler/resinator/errors.zig @@ -1,5 +1,11 @@ +const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + const Token = @import("lex.zig").Token; const SourceMappings = @import("source_mapping.zig").SourceMappings; const utils = @import("utils.zig"); @@ -11,19 +17,19 @@ const parse = @import("parse.zig"); const lang = @import("lang.zig"); const code_pages = @import("code_pages.zig"); const SupportedCodePage = code_pages.SupportedCodePage; -const builtin = @import("builtin"); -const native_endian = builtin.cpu.arch.endian(); pub const Diagnostics = struct { errors: std.ArrayList(ErrorDetails) = .empty, /// Append-only, cannot handle removing strings. /// Expects to own all strings within the list. strings: std.ArrayList([]const u8) = .empty, - allocator: std.mem.Allocator, + allocator: Allocator, + io: Io, - pub fn init(allocator: std.mem.Allocator) Diagnostics { + pub fn init(allocator: Allocator, io: Io) Diagnostics { return .{ .allocator = allocator, + .io = io, }; } @@ -62,10 +68,11 @@ pub const Diagnostics = struct { } pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.Io.tty.Config, source_mappings: ?SourceMappings) void { + const io = self.io; const stderr = std.debug.lockStderrWriter(&.{}); defer std.debug.unlockStderrWriter(); for (self.errors.items) |err_details| { - renderErrorMessage(stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return; + renderErrorMessage(io, stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return; } } @@ -167,9 +174,9 @@ pub const ErrorDetails = struct { filename_string_index: FilenameStringIndex, pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum)); - pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError); + pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError || std.fs.File.StatError); - pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum { + pub fn enumFromError(err: (std.fs.File.OpenError || std.fs.File.StatError)) FileOpenErrorEnum { return switch (err) { inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)), }; @@ -894,7 +901,16 @@ fn cellCount(code_page: SupportedCodePage, source: []const u8, start_index: usiz const truncated_str = "<...truncated...>"; -pub fn renderErrorMessage(writer: *std.Io.Writer, tty_config: std.Io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void { +pub fn renderErrorMessage( + io: Io, + writer: *std.Io.Writer, + tty_config: std.Io.tty.Config, + cwd: std.fs.Dir, + err_details: ErrorDetails, + source: []const u8, + strings: []const []const u8, + source_mappings: ?SourceMappings, +) !void { if (err_details.type == .hint) return; const source_line_start = err_details.token.getLineStartForErrorDisplay(source); @@ -989,6 +1005,7 @@ pub fn renderErrorMessage(writer: *std.Io.Writer, tty_config: std.Io.tty.Config, var initial_lines_err: ?anyerror = null; var file_reader_buf: [max_source_line_bytes * 2]u8 = undefined; var corresponding_lines: ?CorrespondingLines = CorrespondingLines.init( + io, cwd, err_details, source_line_for_display.line, @@ -1084,6 +1101,7 @@ const CorrespondingLines = struct { code_page: SupportedCodePage, pub fn init( + io: Io, cwd: std.fs.Dir, err_details: ErrorDetails, line_for_comparison: []const u8, @@ -1108,7 +1126,7 @@ const CorrespondingLines = struct { .code_page = err_details.code_page, .file_reader = undefined, }; - corresponding_lines.file_reader = corresponding_lines.file.reader(file_reader_buf); + corresponding_lines.file_reader = corresponding_lines.file.reader(io, file_reader_buf); errdefer corresponding_lines.deinit(); try corresponding_lines.writeLineFromStreamVerbatim( diff --git a/lib/compiler/resinator/main.zig b/lib/compiler/resinator/main.zig index ce8532ae9f..108b914377 100644 --- a/lib/compiler/resinator/main.zig +++ b/lib/compiler/resinator/main.zig @@ -1,5 +1,9 @@ -const std = @import("std"); const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + const removeComments = @import("comments.zig").removeComments; const parseAndRemoveLineCommands = @import("source_mapping.zig").parseAndRemoveLineCommands; const compile = @import("compile.zig").compile; @@ -16,19 +20,18 @@ const aro = @import("aro"); const compiler_util = @import("../util.zig"); pub fn main() !void { - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; - defer std.debug.assert(gpa.deinit() == .ok); - const allocator = gpa.allocator(); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer std.debug.assert(debug_allocator.deinit() == .ok); + const gpa = debug_allocator.allocator(); - var arena_state = std.heap.ArenaAllocator.init(allocator); + var arena_state = std.heap.ArenaAllocator.init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); const stderr = std.fs.File.stderr(); const stderr_config = std.Io.tty.detectConfig(stderr); - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const args = try std.process.argsAlloc(arena); if (args.len < 2) { try renderErrorMessage(std.debug.lockStderrWriter(&.{}), stderr_config, .err, "expected zig lib dir as first argument", .{}); @@ -59,11 +62,11 @@ pub fn main() !void { }; var options = options: { - var cli_diagnostics = cli.Diagnostics.init(allocator); + var cli_diagnostics = cli.Diagnostics.init(gpa); defer cli_diagnostics.deinit(); - var options = cli.parse(allocator, cli_args, &cli_diagnostics) catch |err| switch (err) { + var options = cli.parse(gpa, cli_args, &cli_diagnostics) catch |err| switch (err) { error.ParseError => { - try error_handler.emitCliDiagnostics(allocator, cli_args, &cli_diagnostics); + try error_handler.emitCliDiagnostics(gpa, cli_args, &cli_diagnostics); std.process.exit(1); }, else => |e| return e, @@ -84,6 +87,10 @@ pub fn main() !void { }; defer options.deinit(); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + if (options.print_help_and_exit) { try cli.writeUsage(stdout, "zig rc"); try stdout.flush(); @@ -99,12 +106,13 @@ pub fn main() !void { try stdout.flush(); } - var dependencies = Dependencies.init(allocator); + var dependencies = Dependencies.init(gpa); defer dependencies.deinit(); const maybe_dependencies: ?*Dependencies = if (options.depfile_path != null) &dependencies else null; var include_paths = LazyIncludePaths{ .arena = arena, + .io = io, .auto_includes_option = options.auto_includes, .zig_lib_dir = zig_lib_dir, .target_machine_type = options.coff_options.target, @@ -112,12 +120,12 @@ pub fn main() !void { const full_input = full_input: { if (options.input_format == .rc and options.preprocess != .no) { - var preprocessed_buf: std.Io.Writer.Allocating = .init(allocator); + var preprocessed_buf: std.Io.Writer.Allocating = .init(gpa); errdefer preprocessed_buf.deinit(); // We're going to throw away everything except the final preprocessed output anyway, // so we can use a scoped arena for everything else. - var aro_arena_state = std.heap.ArenaAllocator.init(allocator); + var aro_arena_state = std.heap.ArenaAllocator.init(gpa); defer aro_arena_state.deinit(); const aro_arena = aro_arena_state.allocator(); @@ -129,12 +137,12 @@ pub fn main() !void { .color = stderr_config, } } }, true => .{ .output = .{ .to_list = .{ - .arena = .init(allocator), + .arena = .init(gpa), } } }, }; defer diagnostics.deinit(); - var comp = aro.Compilation.init(aro_arena, aro_arena, &diagnostics, std.fs.cwd()); + var comp = aro.Compilation.init(aro_arena, aro_arena, io, &diagnostics, std.fs.cwd()); defer comp.deinit(); var argv: std.ArrayList([]const u8) = .empty; @@ -159,20 +167,20 @@ pub fn main() !void { preprocess.preprocess(&comp, &preprocessed_buf.writer, argv.items, maybe_dependencies) catch |err| switch (err) { error.GeneratedSourceError => { - try error_handler.emitAroDiagnostics(allocator, "failed during preprocessor setup (this is always a bug)", &comp); + try error_handler.emitAroDiagnostics(gpa, "failed during preprocessor setup (this is always a bug)", &comp); std.process.exit(1); }, // ArgError can occur if e.g. the .rc file is not found error.ArgError, error.PreprocessError => { - try error_handler.emitAroDiagnostics(allocator, "failed during preprocessing", &comp); + try error_handler.emitAroDiagnostics(gpa, "failed during preprocessing", &comp); std.process.exit(1); }, error.FileTooBig => { - try error_handler.emitMessage(allocator, .err, "failed during preprocessing: maximum file size exceeded", .{}); + try error_handler.emitMessage(gpa, .err, "failed during preprocessing: maximum file size exceeded", .{}); std.process.exit(1); }, error.WriteFailed => { - try error_handler.emitMessage(allocator, .err, "failed during preprocessing: error writing the preprocessed output", .{}); + try error_handler.emitMessage(gpa, .err, "failed during preprocessing: error writing the preprocessed output", .{}); std.process.exit(1); }, error.OutOfMemory => |e| return e, @@ -182,22 +190,22 @@ pub fn main() !void { } else { switch (options.input_source) { .stdio => |file| { - var file_reader = file.reader(&.{}); - break :full_input file_reader.interface.allocRemaining(allocator, .unlimited) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); + var file_reader = file.reader(io, &.{}); + break :full_input file_reader.interface.allocRemaining(gpa, .unlimited) catch |err| { + try error_handler.emitMessage(gpa, .err, "unable to read input from stdin: {s}", .{@errorName(err)}); std.process.exit(1); }; }, .filename => |input_filename| { - break :full_input std.fs.cwd().readFileAlloc(input_filename, allocator, .unlimited) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); + break :full_input std.fs.cwd().readFileAlloc(input_filename, gpa, .unlimited) catch |err| { + try error_handler.emitMessage(gpa, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) }); std.process.exit(1); }; }, } } }; - defer allocator.free(full_input); + defer gpa.free(full_input); if (options.preprocess == .only) { switch (options.output_source) { @@ -221,55 +229,55 @@ pub fn main() !void { } else if (options.input_format == .res) IoStream.fromIoSource(options.input_source, .input) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) }); std.process.exit(1); } else IoStream.fromIoSource(options.output_source, .output) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); std.process.exit(1); }; - defer res_stream.deinit(allocator); + defer res_stream.deinit(gpa); const res_data = res_data: { if (options.input_format != .res) { // Note: We still want to run this when no-preprocess is set because: // 1. We want to print accurate line numbers after removing multiline comments // 2. We want to be able to handle an already-preprocessed input with #line commands in it - var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) { + var mapping_results = parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) { error.InvalidLineCommand => { // TODO: Maybe output the invalid line command - try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{}); + try error_handler.emitMessage(gpa, .err, "invalid line command in the preprocessed source", .{}); if (options.preprocess == .no) { - try error_handler.emitMessage(allocator, .note, "line commands must be of the format: #line \"\"", .{}); + try error_handler.emitMessage(gpa, .note, "line commands must be of the format: #line \"\"", .{}); } else { - try error_handler.emitMessage(allocator, .note, "this is likely to be a bug, please report it", .{}); + try error_handler.emitMessage(gpa, .note, "this is likely to be a bug, please report it", .{}); } std.process.exit(1); }, error.LineNumberOverflow => { // TODO: Better error message - try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); + try error_handler.emitMessage(gpa, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)}); std.process.exit(1); }, error.OutOfMemory => |e| return e, }; - defer mapping_results.mappings.deinit(allocator); + defer mapping_results.mappings.deinit(gpa); const default_code_page = options.default_code_page orelse .windows1252; const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page); const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings); - var diagnostics = Diagnostics.init(allocator); + var diagnostics = Diagnostics.init(gpa, io); defer diagnostics.deinit(); var output_buffer: [4096]u8 = undefined; - var res_stream_writer = res_stream.source.writer(allocator, &output_buffer); + var res_stream_writer = res_stream.source.writer(gpa, &output_buffer); defer res_stream_writer.deinit(&res_stream.source); const output_buffered_stream = res_stream_writer.interface(); - compile(allocator, final_input, output_buffered_stream, .{ + compile(gpa, io, final_input, output_buffered_stream, .{ .cwd = std.fs.cwd(), .diagnostics = &diagnostics, .source_mappings = &mapping_results.mappings, @@ -287,7 +295,7 @@ pub fn main() !void { .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page, }) catch |err| switch (err) { error.ParseError, error.CompileError => { - try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); + try error_handler.emitDiagnostics(gpa, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings); // Delete the output file on error res_stream.cleanupAfterError(); std.process.exit(1); @@ -305,7 +313,7 @@ pub fn main() !void { // write the depfile if (options.depfile_path) |depfile_path| { var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) }); std.process.exit(1); }; defer depfile.close(); @@ -332,41 +340,41 @@ pub fn main() !void { if (options.output_format != .coff) return; - break :res_data res_stream.source.readAll(allocator) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); + break :res_data res_stream.source.readAll(gpa, io) catch |err| { + try error_handler.emitMessage(gpa, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); std.process.exit(1); }; }; // No need to keep the res_data around after parsing the resources from it - defer res_data.deinit(allocator); + defer res_data.deinit(gpa); std.debug.assert(options.output_format == .coff); // TODO: Maybe use a buffered file reader instead of reading file into memory -> fbs var res_reader: std.Io.Reader = .fixed(res_data.bytes); - break :resources cvtres.parseRes(allocator, &res_reader, .{ .max_size = res_data.bytes.len }) catch |err| { + break :resources cvtres.parseRes(gpa, &res_reader, .{ .max_size = res_data.bytes.len }) catch |err| { // TODO: Better errors - try error_handler.emitMessage(allocator, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) }); std.process.exit(1); }; }; defer resources.deinit(); var coff_stream = IoStream.fromIoSource(options.output_source, .output) catch |err| { - try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) }); std.process.exit(1); }; - defer coff_stream.deinit(allocator); + defer coff_stream.deinit(gpa); var coff_output_buffer: [4096]u8 = undefined; - var coff_output_buffered_stream = coff_stream.source.writer(allocator, &coff_output_buffer); + var coff_output_buffered_stream = coff_stream.source.writer(gpa, &coff_output_buffer); var cvtres_diagnostics: cvtres.Diagnostics = .{ .none = {} }; - cvtres.writeCoff(allocator, coff_output_buffered_stream.interface(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| { + cvtres.writeCoff(gpa, coff_output_buffered_stream.interface(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| { switch (err) { error.DuplicateResource => { const duplicate_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(allocator, .err, "duplicate resource [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, .err, "duplicate resource [id: {f}, type: {f}, language: {f}]", .{ duplicate_resource.name_value, fmtResourceType(duplicate_resource.type_value), duplicate_resource.language, @@ -374,8 +382,8 @@ pub fn main() !void { }, error.ResourceDataTooLong => { const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(allocator, .err, "resource has a data length that is too large to be written into a coff section", .{}); - try error_handler.emitMessage(allocator, .note, "the resource with the invalid size is [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, .err, "resource has a data length that is too large to be written into a coff section", .{}); + try error_handler.emitMessage(gpa, .note, "the resource with the invalid size is [id: {f}, type: {f}, language: {f}]", .{ overflow_resource.name_value, fmtResourceType(overflow_resource.type_value), overflow_resource.language, @@ -383,15 +391,15 @@ pub fn main() !void { }, error.TotalResourceDataTooLong => { const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource]; - try error_handler.emitMessage(allocator, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); - try error_handler.emitMessage(allocator, .note, "size overflow occurred when attempting to write this resource: [id: {f}, type: {f}, language: {f}]", .{ + try error_handler.emitMessage(gpa, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{}); + try error_handler.emitMessage(gpa, .note, "size overflow occurred when attempting to write this resource: [id: {f}, type: {f}, language: {f}]", .{ overflow_resource.name_value, fmtResourceType(overflow_resource.type_value), overflow_resource.language, }); }, else => { - try error_handler.emitMessage(allocator, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); + try error_handler.emitMessage(gpa, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) }); }, } // Delete the output file on error @@ -423,7 +431,7 @@ const IoStream = struct { }; } - pub fn deinit(self: *IoStream, allocator: std.mem.Allocator) void { + pub fn deinit(self: *IoStream, allocator: Allocator) void { self.source.deinit(allocator); } @@ -458,7 +466,7 @@ const IoStream = struct { } } - pub fn deinit(self: *Source, allocator: std.mem.Allocator) void { + pub fn deinit(self: *Source, allocator: Allocator) void { switch (self.*) { .file => |file| file.close(), .stdio => {}, @@ -471,18 +479,18 @@ const IoStream = struct { bytes: []const u8, needs_free: bool, - pub fn deinit(self: Data, allocator: std.mem.Allocator) void { + pub fn deinit(self: Data, allocator: Allocator) void { if (self.needs_free) { allocator.free(self.bytes); } } }; - pub fn readAll(self: Source, allocator: std.mem.Allocator) !Data { + pub fn readAll(self: Source, allocator: Allocator, io: Io) !Data { return switch (self) { inline .file, .stdio => |file| .{ .bytes = b: { - var file_reader = file.reader(&.{}); + var file_reader = file.reader(io, &.{}); break :b try file_reader.interface.allocRemaining(allocator, .unlimited); }, .needs_free = true, @@ -496,7 +504,7 @@ const IoStream = struct { file: std.fs.File.Writer, allocating: std.Io.Writer.Allocating, - pub const Error = std.mem.Allocator.Error || std.fs.File.WriteError; + pub const Error = Allocator.Error || std.fs.File.WriteError; pub fn interface(this: *@This()) *std.Io.Writer { return switch (this.*) { @@ -514,7 +522,7 @@ const IoStream = struct { } }; - pub fn writer(source: *Source, allocator: std.mem.Allocator, buffer: []u8) Writer { + pub fn writer(source: *Source, allocator: Allocator, buffer: []u8) Writer { return switch (source.*) { .file, .stdio => |file| .{ .file = file.writer(buffer) }, .memory => |*list| .{ .allocating = .fromArrayList(allocator, list) }, @@ -525,17 +533,20 @@ const IoStream = struct { }; const LazyIncludePaths = struct { - arena: std.mem.Allocator, + arena: Allocator, + io: Io, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8, target_machine_type: std.coff.IMAGE.FILE.MACHINE, resolved_include_paths: ?[]const []const u8 = null, pub fn get(self: *LazyIncludePaths, error_handler: *ErrorHandler) ![]const []const u8 { + const io = self.io; + if (self.resolved_include_paths) |include_paths| return include_paths; - return getIncludePaths(self.arena, self.auto_includes_option, self.zig_lib_dir, self.target_machine_type) catch |err| switch (err) { + return getIncludePaths(self.arena, io, self.auto_includes_option, self.zig_lib_dir, self.target_machine_type) catch |err| switch (err) { error.OutOfMemory => |e| return e, else => |e| { switch (e) { @@ -556,7 +567,13 @@ const LazyIncludePaths = struct { } }; -fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8, target_machine_type: std.coff.IMAGE.FILE.MACHINE) ![]const []const u8 { +fn getIncludePaths( + arena: Allocator, + io: Io, + auto_includes_option: cli.Options.AutoIncludes, + zig_lib_dir: []const u8, + target_machine_type: std.coff.IMAGE.FILE.MACHINE, +) ![]const []const u8 { if (auto_includes_option == .none) return &[_][]const u8{}; const includes_arch: std.Target.Cpu.Arch = switch (target_machine_type) { @@ -626,7 +643,7 @@ fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.A .cpu_arch = includes_arch, .abi = .gnu, }; - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); const is_native_abi = target_query.isNativeAbi(); const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, &target, is_native_abi, true, null) catch |err| switch (err) { error.OutOfMemory => |e| return e, @@ -647,7 +664,7 @@ const ErrorHandler = union(enum) { pub fn emitCliDiagnostics( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, args: []const []const u8, diagnostics: *cli.Diagnostics, ) !void { @@ -666,7 +683,7 @@ const ErrorHandler = union(enum) { pub fn emitAroDiagnostics( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, fail_msg: []const u8, comp: *aro.Compilation, ) !void { @@ -692,7 +709,7 @@ const ErrorHandler = union(enum) { pub fn emitDiagnostics( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, cwd: std.fs.Dir, source: []const u8, diagnostics: *Diagnostics, @@ -713,7 +730,7 @@ const ErrorHandler = union(enum) { pub fn emitMessage( self: *ErrorHandler, - allocator: std.mem.Allocator, + allocator: Allocator, msg_type: @import("utils.zig").ErrorMessageType, comptime format: []const u8, args: anytype, @@ -738,7 +755,7 @@ const ErrorHandler = union(enum) { }; fn cliDiagnosticsToErrorBundle( - gpa: std.mem.Allocator, + gpa: Allocator, diagnostics: *cli.Diagnostics, ) !ErrorBundle { @branchHint(.cold); @@ -783,7 +800,7 @@ fn cliDiagnosticsToErrorBundle( } fn diagnosticsToErrorBundle( - gpa: std.mem.Allocator, + gpa: Allocator, source: []const u8, diagnostics: *Diagnostics, mappings: SourceMappings, @@ -870,7 +887,7 @@ fn diagnosticsToErrorBundle( return try bundle.toOwnedBundle(""); } -fn errorStringToErrorBundle(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) !ErrorBundle { +fn errorStringToErrorBundle(allocator: Allocator, comptime format: []const u8, args: anytype) !ErrorBundle { @branchHint(.cold); var bundle: ErrorBundle.Wip = undefined; try bundle.init(allocator); diff --git a/lib/compiler/resinator/utils.zig b/lib/compiler/resinator/utils.zig index b535ab9c71..021b8cf4de 100644 --- a/lib/compiler/resinator/utils.zig +++ b/lib/compiler/resinator/utils.zig @@ -26,7 +26,11 @@ pub const UncheckedSliceWriter = struct { /// Cross-platform 'std.fs.Dir.openFile' wrapper that will always return IsDir if /// a directory is attempted to be opened. /// TODO: Remove once https://github.com/ziglang/zig/issues/5732 is addressed. -pub fn openFileNotDir(cwd: std.fs.Dir, path: []const u8, flags: std.fs.File.OpenFlags) std.fs.File.OpenError!std.fs.File { +pub fn openFileNotDir( + cwd: std.fs.Dir, + path: []const u8, + flags: std.fs.File.OpenFlags, +) (std.fs.File.OpenError || std.fs.File.StatError)!std.fs.File { const file = try cwd.openFile(path, flags); errdefer file.close(); // https://github.com/ziglang/zig/issues/5732 From 67df66c26cdf63a637c9ac86b64f447d2053b6b3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 20 Oct 2025 23:26:14 -0700 Subject: [PATCH 161/244] update some tests and tools for new Io APIs --- test/stack_traces.zig | 20 ++++++++++---------- test/standalone/coff_dwarf/main.zig | 6 +++++- test/standalone/libfuzzer/main.zig | 6 +++++- tools/fetch_them_macos_headers.zig | 6 +++++- tools/gen_macos_headers_c.zig | 2 +- tools/generate_c_size_and_align_checks.zig | 6 +++++- tools/migrate_langref.zig | 8 +++++++- 7 files changed, 38 insertions(+), 16 deletions(-) diff --git a/test/stack_traces.zig b/test/stack_traces.zig index d0f1acc08b..c8245f6820 100644 --- a/test/stack_traces.zig +++ b/test/stack_traces.zig @@ -116,12 +116,12 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { .source = \\pub fn main() void { \\ var stack_trace_buf: [8]usize = undefined; - \\ dumpIt(&captureIt(&stack_trace_buf)); + \\ dumpIt(captureIt(&stack_trace_buf)); \\} \\fn captureIt(buf: []usize) std.builtin.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\fn dumpIt(st: std.builtin.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} \\fn captureItInner(buf: []usize) std.builtin.StackTrace { @@ -140,8 +140,8 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ return captureItInner(buf); \\ ^ \\source.zig:3:22: [address] in main - \\ dumpIt(&captureIt(&stack_trace_buf)); - \\ ^ + \\ dumpIt(captureIt(&stack_trace_buf)); + \\ ^ \\ , .expect_strip = @@ -157,12 +157,12 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { .source = \\pub fn main() void { \\ var stack_trace_buf: [8]usize = undefined; - \\ dumpIt(&captureIt(&stack_trace_buf)); + \\ dumpIt(captureIt(&stack_trace_buf)); \\} \\fn captureIt(buf: []usize) std.builtin.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\fn dumpIt(st: std.builtin.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} \\fn captureItInner(buf: []usize) std.builtin.StackTrace { @@ -186,12 +186,12 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ t.join(); \\} \\fn threadMain(stack_trace_buf: []usize) void { - \\ dumpIt(&captureIt(stack_trace_buf)); + \\ dumpIt(captureIt(stack_trace_buf)); \\} \\fn captureIt(buf: []usize) std.builtin.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: *const std.builtin.StackTrace) void { + \\fn dumpIt(st: std.builtin.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} \\fn captureItInner(buf: []usize) std.builtin.StackTrace { @@ -210,8 +210,8 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ return captureItInner(buf); \\ ^ \\source.zig:7:22: [address] in threadMain - \\ dumpIt(&captureIt(stack_trace_buf)); - \\ ^ + \\ dumpIt(captureIt(stack_trace_buf)); + \\ ^ \\ , .expect_strip = diff --git a/test/standalone/coff_dwarf/main.zig b/test/standalone/coff_dwarf/main.zig index 6707bab4dc..e7590f3f07 100644 --- a/test/standalone/coff_dwarf/main.zig +++ b/test/standalone/coff_dwarf/main.zig @@ -11,10 +11,14 @@ pub fn main() void { var di: std.debug.SelfInfo = .init; defer di.deinit(gpa); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var add_addr: usize = undefined; _ = add(1, 2, &add_addr); - const symbol = di.getSymbol(gpa, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err}); + const symbol = di.getSymbol(gpa, io, add_addr) catch |err| fatal("failed to get symbol: {t}", .{err}); defer if (symbol.source_location) |sl| gpa.free(sl.file_name); if (symbol.name == null) fatal("failed to resolve symbol name", .{}); diff --git a/test/standalone/libfuzzer/main.zig b/test/standalone/libfuzzer/main.zig index b21e9be250..b275b6d593 100644 --- a/test/standalone/libfuzzer/main.zig +++ b/test/standalone/libfuzzer/main.zig @@ -15,6 +15,10 @@ pub fn main() !void { defer args.deinit(); _ = args.skip(); // executable name + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const cache_dir_path = args.next() orelse @panic("expected cache directory path argument"); var cache_dir = try std.fs.cwd().openDir(cache_dir_path, .{}); defer cache_dir.close(); @@ -30,7 +34,7 @@ pub fn main() !void { defer coverage_file.close(); var read_buf: [@sizeOf(abi.SeenPcsHeader)]u8 = undefined; - var r = coverage_file.reader(&read_buf); + var r = coverage_file.reader(io, &read_buf); const pcs_header = r.interface.takeStruct(abi.SeenPcsHeader, native_endian) catch return r.err.?; if (pcs_header.pcs_len == 0) diff --git a/tools/fetch_them_macos_headers.zig b/tools/fetch_them_macos_headers.zig index ca022e9b0c..d91235fee9 100644 --- a/tools/fetch_them_macos_headers.zig +++ b/tools/fetch_them_macos_headers.zig @@ -85,8 +85,12 @@ pub fn main() anyerror!void { } else try argv.append(arg); } + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const sysroot_path = sysroot orelse blk: { - const target = try std.zig.system.resolveTargetQuery(.{}); + const target = try std.zig.system.resolveTargetQuery(io, .{}); break :blk std.zig.system.darwin.getSdk(allocator, &target) orelse fatal("no SDK found; you can provide one explicitly with '--sysroot' flag", .{}); }; diff --git a/tools/gen_macos_headers_c.zig b/tools/gen_macos_headers_c.zig index f95023adb7..a5d865bf03 100644 --- a/tools/gen_macos_headers_c.zig +++ b/tools/gen_macos_headers_c.zig @@ -33,7 +33,7 @@ pub fn main() anyerror!void { if (positionals.items.len != 1) fatal("expected one positional argument: [dir]", .{}); - var dir = try std.fs.cwd().openDir(positionals.items[0], .{ .no_follow = true }); + var dir = try std.fs.cwd().openDir(positionals.items[0], .{ .follow_symlinks = false }); defer dir.close(); var paths = std.array_list.Managed([]const u8).init(arena); try findHeaders(arena, dir, "", &paths); diff --git a/tools/generate_c_size_and_align_checks.zig b/tools/generate_c_size_and_align_checks.zig index 8c278407e4..3663756533 100644 --- a/tools/generate_c_size_and_align_checks.zig +++ b/tools/generate_c_size_and_align_checks.zig @@ -39,8 +39,12 @@ pub fn main() !void { std.process.exit(1); } + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const query = try std.Target.Query.parse(.{ .arch_os_abi = args[1] }); - const target = try std.zig.system.resolveTargetQuery(query); + const target = try std.zig.system.resolveTargetQuery(io, query); var buffer: [2000]u8 = undefined; var stdout_writer = std.fs.File.stdout().writerStreaming(&buffer); diff --git a/tools/migrate_langref.zig b/tools/migrate_langref.zig index d880db1c25..3544cee175 100644 --- a/tools/migrate_langref.zig +++ b/tools/migrate_langref.zig @@ -13,10 +13,16 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + const gpa = arena; + const args = try std.process.argsAlloc(arena); const input_file = args[1]; const output_file = args[2]; + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var in_file = try fs.cwd().openFile(input_file, .{ .mode = .read_only }); defer in_file.close(); @@ -28,7 +34,7 @@ pub fn main() !void { var out_dir = try fs.cwd().openDir(fs.path.dirname(output_file).?, .{}); defer out_dir.close(); - var in_file_reader = in_file.reader(&.{}); + var in_file_reader = in_file.reader(io, &.{}); const input_file_bytes = try in_file_reader.interface.allocRemaining(arena, .unlimited); var tokenizer = Tokenizer.init(input_file, input_file_bytes); From 4ed74a9f8ab97f3358fc881d0e2f6359e22186d6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 04:30:36 -0700 Subject: [PATCH 162/244] std.Io.Threaded: implement netConnectIp for Windows --- lib/std/Io/Threaded.zig | 196 +++++++++++++++++++++++----------------- lib/std/posix.zig | 79 +--------------- 2 files changed, 116 insertions(+), 159 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 4b942e2379..ed7966d014 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -252,7 +252,7 @@ pub fn io(t: *Threaded) Io { else => netBindIpPosix, }, .netConnectIp = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => netConnectIpWindows, else => netConnectIpPosix, }, .netConnectUnix = netConnectUnix, @@ -2810,28 +2810,10 @@ fn netListenIpWindows( if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(&address); - const mode = posixSocketMode(options.mode); - const protocol = posixProtocol(options.protocol); - - const socket_handle = while (true) { - try t.checkCancel(); - const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; - const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags); - if (rc != ws2_32.INVALID_SOCKET) break rc; - switch (ws2_32.WSAGetLastError()) { - .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, - .NOTINITIALISED => { - try initializeWsa(t); - continue; - }, - .EAFNOSUPPORT => return error.AddressFamilyUnsupported, - .EMFILE => return error.ProcessFdQuotaExceeded, - .ENOBUFS => return error.SystemResources, - .EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, - else => |err| return windows.unexpectedWSAError(err), - } - }; + const socket_handle = try openSocketWsa(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); errdefer closeSocketWindows(socket_handle); if (options.reuse_address) @@ -2885,24 +2867,7 @@ fn netListenIpWindows( } } - while (true) { - try t.checkCancel(); - const rc = ws2_32.getsockname(socket_handle, &storage.any, &addr_len); - if (rc != ws2_32.SOCKET_ERROR) break; - switch (ws2_32.WSAGetLastError()) { - .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, - .NOTINITIALISED => { - try initializeWsa(t); - continue; - }, - .ENETDOWN => return error.NetworkDown, - .EFAULT => |err| return wsaErrorBug(err), - .ENOTSOCK => |err| return wsaErrorBug(err), - .EINVAL => |err| return wsaErrorBug(err), - else => |err| return windows.unexpectedWSAError(err), - } - } + try wsaGetSockName(t, socket_handle, &storage.any, &addr_len); return .{ .socket = .{ @@ -3076,6 +3041,27 @@ fn posixGetSockName(t: *Threaded, socket_fd: posix.fd_t, addr: *posix.sockaddr, } } +fn wsaGetSockName(t: *Threaded, handle: ws2_32.SOCKET, addr: *ws2_32.sockaddr, addr_len: *i32) !void { + while (true) { + try t.checkCancel(); + const rc = ws2_32.getsockname(handle, addr, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EFAULT => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + fn setSocketOption(t: *Threaded, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { const o: []const u8 = @ptrCast(&option); while (true) { @@ -3139,6 +3125,61 @@ fn netConnectIpPosix( } }; } +fn netConnectIpWindows( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.ConnectOptions, +) IpAddress.ConnectError!net.Stream { + if (!have_networking) return error.NetworkDown; + if (options.timeout != .none) @panic("TODO"); + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const family = posixAddressFamily(address); + const socket_handle = try openSocketWsa(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer closeSocketWindows(socket_handle); + + var storage: WsaAddress = undefined; + var addr_len = addressToWsa(address, &storage); + + while (true) { + const rc = ws2_32.connect(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ECONNREFUSED => return error.ConnectionRefused, + .ECONNRESET => return error.ConnectionResetByPeer, + .ETIMEDOUT => return error.Timeout, + .EHOSTUNREACH => return error.HostUnreachable, + .ENETUNREACH => return error.NetworkUnreachable, + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EISCONN => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EWOULDBLOCK => return error.WouldBlock, + .EACCES => return error.AccessDenied, + .ENOBUFS => return error.SystemResources, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + else => |err| return windows.unexpectedWSAError(err), + } + } + + try wsaGetSockName(t, socket_handle, &storage.any, &addr_len); + + return .{ .socket = .{ + .handle = socket_handle, + .address = addressFromWsa(&storage), + } }; +} + fn netConnectUnix( userdata: ?*anyopaque, address: *const net.UnixAddress, @@ -3184,28 +3225,12 @@ fn netBindIpWindows( if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); - const mode = posixSocketMode(options.mode); - const protocol = posixProtocol(options.protocol); - const socket_handle = while (true) { - try t.checkCancel(); - const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; - const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags); - if (rc != ws2_32.INVALID_SOCKET) break rc; - switch (ws2_32.WSAGetLastError()) { - .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, - .NOTINITIALISED => { - try initializeWsa(t); - continue; - }, - .EAFNOSUPPORT => return error.AddressFamilyUnsupported, - .EMFILE => return error.ProcessFdQuotaExceeded, - .ENOBUFS => return error.SystemResources, - .EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, - else => |err| return windows.unexpectedWSAError(err), - } - }; + const socket_handle = try openSocketWsa(t, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); errdefer closeSocketWindows(socket_handle); + var storage: WsaAddress = undefined; var addr_len = addressToWsa(address, &storage); @@ -3231,24 +3256,7 @@ fn netBindIpWindows( } } - while (true) { - try t.checkCancel(); - const rc = ws2_32.getsockname(socket_handle, &storage.any, &addr_len); - if (rc != ws2_32.SOCKET_ERROR) break; - switch (ws2_32.WSAGetLastError()) { - .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, - .NOTINITIALISED => { - try initializeWsa(t); - continue; - }, - .ENETDOWN => return error.NetworkDown, - .EFAULT => |err| return wsaErrorBug(err), - .ENOTSOCK => |err| return wsaErrorBug(err), - .EINVAL => |err| return wsaErrorBug(err), - else => |err| return windows.unexpectedWSAError(err), - } - } + try wsaGetSockName(t, socket_handle, &storage.any, &addr_len); return .{ .handle = socket_handle, @@ -3317,6 +3325,30 @@ fn openSocketPosix( return socket_fd; } +fn openSocketWsa(t: *Threaded, family: posix.sa_family_t, options: IpAddress.BindOptions) !ws2_32.SOCKET { + const mode = posixSocketMode(options.mode); + const protocol = posixProtocol(options.protocol); + const flags: u32 = ws2_32.WSA_FLAG_OVERLAPPED | ws2_32.WSA_FLAG_NO_HANDLE_INHERIT; + while (true) { + try t.checkCancel(); + const rc = ws2_32.WSASocketW(family, @bitCast(mode), @bitCast(protocol), null, 0, flags); + if (rc != ws2_32.INVALID_SOCKET) return rc; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .EMFILE => return error.ProcessFdQuotaExceeded, + .ENOBUFS => return error.SystemResources, + .EPROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + else => |err| return windows.unexpectedWSAError(err), + } + } +} + fn netAcceptPosix(userdata: ?*anyopaque, listen_fd: net.Socket.Handle) net.Server.AcceptError!net.Stream { if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -3376,11 +3408,11 @@ fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net while (true) { try t.checkCancel(); const rc = ws2_32.accept(listen_handle, &storage.any, &addr_len); - if (rc != windows.ws2_32.INVALID_SOCKET) return .{ .socket = .{ + if (rc != ws2_32.INVALID_SOCKET) return .{ .socket = .{ .handle = rc, .address = addressFromWsa(&storage), } }; - switch (windows.ws2_32.WSAGetLastError()) { + switch (ws2_32.WSAGetLastError()) { .EINTR => continue, .ECANCELLED, .E_CANCELLED => return error.Canceled, .NOTINITIALISED => { diff --git a/lib/std/posix.zig b/lib/std/posix.zig index dd0ad16695..bb556be133 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3757,86 +3757,11 @@ pub fn getpeername(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock } } -pub const ConnectError = error{ - /// For UNIX domain sockets, which are identified by pathname: Write permission is denied on the socket - /// file, or search permission is denied for one of the directories in the path prefix. - /// or - /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled or - /// the connection request failed because of a local firewall rule. - AccessDenied, +pub const ConnectError = std.Io.net.IpAddress.ConnectError || std.Io.net.UnixAddress.ConnectError; - /// See AccessDenied - PermissionDenied, - - /// Local address is already in use. - AddressInUse, - - /// (Internet domain sockets) The socket referred to by sockfd had not previously been bound to an - /// address and, upon attempting to bind it to an ephemeral port, it was determined that all port numbers - /// in the ephemeral port range are currently in use. See the discussion of - /// /proc/sys/net/ipv4/ip_local_port_range in ip(7). - AddressUnavailable, - - /// The passed address didn't have the correct address family in its sa_family field. - AddressFamilyUnsupported, - - /// Insufficient entries in the routing cache. - SystemResources, - - /// A connect() on a stream socket found no one listening on the remote address. - ConnectionRefused, - - /// Network is unreachable. - NetworkUnreachable, - - /// Timeout while attempting connection. The server may be too busy to accept new connections. Note - /// that for IP sockets the timeout may be very long when syncookies are enabled on the server. - Timeout, - - /// This error occurs when no global event loop is configured, - /// and connecting to the socket would block. - WouldBlock, - - /// The given path for the unix socket does not exist. - FileNotFound, - - /// Connection was reset by peer before connect could complete. - ConnectionResetByPeer, - - /// Socket is non-blocking and already has a pending connection in progress. - ConnectionPending, - - /// Socket was already connected - AlreadyConnected, -} || UnexpectedError; - -/// Initiate a connection on a socket. -/// If `sockfd` is opened in non blocking mode, the function will -/// return error.WouldBlock when EAGAIN or EINPROGRESS is received. pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) ConnectError!void { if (native_os == .windows) { - const rc = windows.ws2_32.connect(sock, sock_addr, @intCast(len)); - if (rc == 0) return; - switch (windows.ws2_32.WSAGetLastError()) { - .EADDRINUSE => return error.AddressInUse, - .EADDRNOTAVAIL => return error.AddressUnavailable, - .ECONNREFUSED => return error.ConnectionRefused, - .ECONNRESET => return error.ConnectionResetByPeer, - .ETIMEDOUT => return error.Timeout, - .EHOSTUNREACH, // TODO: should we return NetworkUnreachable in this case as well? - .ENETUNREACH, - => return error.NetworkUnreachable, - .EFAULT => unreachable, - .EINVAL => unreachable, - .EISCONN => return error.AlreadyConnected, - .ENOTSOCK => unreachable, - .EWOULDBLOCK => return error.WouldBlock, - .EACCES => unreachable, - .ENOBUFS => return error.SystemResources, - .EAFNOSUPPORT => return error.AddressFamilyUnsupported, - else => |err| return windows.unexpectedWSAError(err), - } - return; + @compileError("use std.Io instead"); } while (true) { From aadd8d4a3efd8912c47b8ea1bbd4c3a6649c3e5d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 04:40:44 -0700 Subject: [PATCH 163/244] std: back out the StackTrace byval changes Let's keep passing this thing by pointer --- lib/compiler/test_runner.zig | 4 ++-- lib/std/Build/Step.zig | 2 +- lib/std/Thread.zig | 4 ++-- lib/std/debug.zig | 10 +++++----- lib/std/start.zig | 2 +- test/stack_traces.zig | 20 ++++++++++---------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index fbf37f7ec9..0d6f451947 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -148,7 +148,7 @@ fn mainServer() !void { error.SkipZigTest => .skip, else => s: { if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace); } break :s .fail; }, @@ -269,7 +269,7 @@ fn mainTerminal() void { std.debug.print("FAIL ({t})\n", .{err}); } if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace); } test_node.end(); }, diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index d1985739bd..aa922ff37b 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -332,7 +332,7 @@ pub fn cast(step: *Step, comptime T: type) ?*T { pub fn dump(step: *Step, w: *Io.Writer, tty_config: Io.tty.Config) void { if (step.debug_stack_trace.instruction_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; - std.debug.writeStackTrace(step.debug_stack_trace, w, tty_config) catch {}; + std.debug.writeStackTrace(&step.debug_stack_trace, w, tty_config) catch {}; } else { const field = "debug_stack_frames_count"; comptime assert(@hasField(Build, field)); diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 891c4f220e..7e1ce45a46 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -577,7 +577,7 @@ fn callFn(comptime f: anytype, args: anytype) switch (Impl) { @call(.auto, f, args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace); } }; @@ -1010,7 +1010,7 @@ const WasiThreadImpl = struct { @call(.auto, f, w.args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace); } }; }, diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 3d88123c64..a92a4b360f 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -553,7 +553,7 @@ pub fn defaultPanic( if (@errorReturnTrace()) |t| if (t.index > 0) { stderr.writeAll("error return context:\n") catch break :trace; - writeStackTrace(t.*, stderr, tty_config) catch break :trace; + writeStackTrace(t, stderr, tty_config) catch break :trace; stderr.writeAll("\nstack trace:\n") catch break :trace; }; writeCurrentStackTrace(.{ @@ -765,12 +765,12 @@ pub const FormatStackTrace = struct { pub fn format(context: @This(), writer: *Io.Writer) Io.Writer.Error!void { try writer.writeAll("\n"); - try writeStackTrace(context.stack_trace, writer, context.tty_config); + try writeStackTrace(&context.stack_trace, writer, context.tty_config); } }; /// Write a previously captured stack trace to `writer`, annotated with source locations. -pub fn writeStackTrace(st: StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { +pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.Config) Writer.Error!void { if (!std.options.allow_stack_tracing) { tty_config.setColor(writer, .dim) catch {}; try writer.print("Cannot print stack trace: stack tracing is disabled\n", .{}); @@ -808,7 +808,7 @@ pub fn writeStackTrace(st: StackTrace, writer: *Writer, tty_config: tty.Config) } } /// A thin wrapper around `writeStackTrace` which writes to stderr and ignores write errors. -pub fn dumpStackTrace(st: StackTrace) void { +pub fn dumpStackTrace(st: *const StackTrace) void { const tty_config = tty.detectConfig(.stderr()); const stderr = lockStderrWriter(&.{}); defer unlockStderrWriter(); @@ -1686,7 +1686,7 @@ pub fn ConfigurableTrace(comptime size: usize, comptime stack_frame_count: usize .index = frames.len, .instruction_addresses = frames, }; - writeStackTrace(stack_trace, stderr, tty_config) catch return; + writeStackTrace(&stack_trace, stderr, tty_config) catch return; } if (t.index > end) { stderr.print("{d} more traces not shown; consider increasing trace size\n", .{ diff --git a/lib/std/start.zig b/lib/std/start.zig index 4063b2027b..09c1f3b5c4 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -708,7 +708,7 @@ pub inline fn callMain() u8 { switch (native_os) { .freestanding, .other => {}, else => if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); + std.debug.dumpStackTrace(trace); }, } return 1; diff --git a/test/stack_traces.zig b/test/stack_traces.zig index c8245f6820..d0f1acc08b 100644 --- a/test/stack_traces.zig +++ b/test/stack_traces.zig @@ -116,12 +116,12 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { .source = \\pub fn main() void { \\ var stack_trace_buf: [8]usize = undefined; - \\ dumpIt(captureIt(&stack_trace_buf)); + \\ dumpIt(&captureIt(&stack_trace_buf)); \\} \\fn captureIt(buf: []usize) std.builtin.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: std.builtin.StackTrace) void { + \\fn dumpIt(st: *const std.builtin.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} \\fn captureItInner(buf: []usize) std.builtin.StackTrace { @@ -140,8 +140,8 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ return captureItInner(buf); \\ ^ \\source.zig:3:22: [address] in main - \\ dumpIt(captureIt(&stack_trace_buf)); - \\ ^ + \\ dumpIt(&captureIt(&stack_trace_buf)); + \\ ^ \\ , .expect_strip = @@ -157,12 +157,12 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { .source = \\pub fn main() void { \\ var stack_trace_buf: [8]usize = undefined; - \\ dumpIt(captureIt(&stack_trace_buf)); + \\ dumpIt(&captureIt(&stack_trace_buf)); \\} \\fn captureIt(buf: []usize) std.builtin.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: std.builtin.StackTrace) void { + \\fn dumpIt(st: *const std.builtin.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} \\fn captureItInner(buf: []usize) std.builtin.StackTrace { @@ -186,12 +186,12 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ t.join(); \\} \\fn threadMain(stack_trace_buf: []usize) void { - \\ dumpIt(captureIt(stack_trace_buf)); + \\ dumpIt(&captureIt(stack_trace_buf)); \\} \\fn captureIt(buf: []usize) std.builtin.StackTrace { \\ return captureItInner(buf); \\} - \\fn dumpIt(st: std.builtin.StackTrace) void { + \\fn dumpIt(st: *const std.builtin.StackTrace) void { \\ std.debug.dumpStackTrace(st); \\} \\fn captureItInner(buf: []usize) std.builtin.StackTrace { @@ -210,8 +210,8 @@ pub fn addCases(cases: *@import("tests.zig").StackTracesContext) void { \\ return captureItInner(buf); \\ ^ \\source.zig:7:22: [address] in threadMain - \\ dumpIt(captureIt(stack_trace_buf)); - \\ ^ + \\ dumpIt(&captureIt(stack_trace_buf)); + \\ ^ \\ , .expect_strip = From d257b1337a1b0cbe5b2694518017216aa1d2ef1a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 07:10:33 -0700 Subject: [PATCH 164/244] std.Io.Threaded: fix compilation failures on Windows --- lib/std/Io/Threaded.zig | 299 +++++++++++++++----------- lib/std/Io/net/test.zig | 18 -- lib/std/crypto/Certificate/Bundle.zig | 6 +- lib/std/os/windows.zig | 7 + 4 files changed, 186 insertions(+), 144 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index ed7966d014..5aba86c2bb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -220,8 +220,14 @@ pub fn io(t: *Threaded) Io { .fileClose = fileClose, .fileWriteStreaming = fileWriteStreaming, .fileWritePositional = fileWritePositional, - .fileReadStreaming = fileReadStreaming, - .fileReadPositional = fileReadPositional, + .fileReadStreaming = switch (builtin.os.tag) { + .windows => fileReadStreamingWindows, + else => fileReadStreamingPosix, + }, + .fileReadPositional = switch (builtin.os.tag) { + .windows => fileReadPositionalWindows, + else => fileReadPositionalPosix, + }, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, .openSelfExe = openSelfExe, @@ -258,15 +264,21 @@ pub fn io(t: *Threaded) Io { .netConnectUnix = netConnectUnix, .netClose = netClose, .netRead = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => netReadWindows, else => netReadPosix, }, .netWrite = switch (builtin.os.tag) { - .windows => @panic("TODO"), + .windows => netWriteWindows, else => netWritePosix, }, - .netSend = netSend, - .netReceive = netReceive, + .netSend = switch (builtin.os.tag) { + .windows => netSendWindows, + else => netSendPosix, + }, + .netReceive = switch (builtin.os.tag) { + .windows => netReceiveWindows, + else => netReceivePosix, + }, .netInterfaceNameResolve = netInterfaceNameResolve, .netInterfaceName = netInterfaceName, .netLookup = netLookup, @@ -284,6 +296,10 @@ const have_futex = switch (builtin.cpu.arch) { .wasm32, .wasm64 => builtin.cpu.has(.wasm, .atomics), else => true, }; +const have_preadv = switch (native_os) { + .windows, .haiku, .serenity => false, // 💩💩💩 + else => true, +}; const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; @@ -1899,12 +1915,19 @@ fn dirOpenFileWindows( flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); - - const w = windows; - const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); + const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); + return dirOpenFileWindowsInner(t, dir, sub_path_w, flags); +} +fn dirOpenFileWindowsInner( + t: *Threaded, + dir: Io.Dir, + sub_path_w: [:0]const u16, + flags: Io.File.OpenFlags, +) Io.File.OpenError!Io.File { + try t.checkCancel(); + const w = windows; const handle = try w.OpenFile(sub_path_w, .{ .dir = dir.handle, .access_mask = w.SYNCHRONIZE | @@ -2247,47 +2270,9 @@ fn fileClose(userdata: ?*anyopaque, file: Io.File) void { posix.close(file.handle); } -fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { +fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - if (is_windows) { - const DWORD = windows.DWORD; - var index: usize = 0; - var truncate: usize = 0; - var total: usize = 0; - while (index < data.len) { - try t.checkCancel(); - { - const untruncated = data[index]; - data[index] = untruncated[truncate..]; - defer data[index] = untruncated; - const buffer = data[index..]; - const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); - var n: DWORD = undefined; - if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) == 0) { - switch (windows.GetLastError()) { - .IO_PENDING => |err| return windows.statusBug(err), - .OPERATION_ABORTED => continue, - .BROKEN_PIPE => return 0, - .HANDLE_EOF => return 0, - .NETNAME_DELETED => return error.ConnectionResetByPeer, - .LOCK_VIOLATION => return error.LockViolation, - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_HANDLE => return error.NotOpenForReading, - else => |err| return windows.unexpectedError(err), - } - } - total += n; - truncate += n; - } - while (index < data.len and truncate >= data[index].len) { - truncate -= data[index].len; - index += 1; - } - } - return total; - } - var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; for (data) |buf| { @@ -2348,70 +2333,37 @@ fn fileReadStreaming(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File } } -fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { +fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const DWORD = windows.DWORD; + var index: usize = 0; + while (data[index].len == 0) index += 1; + + const buffer = data[index]; + const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); + var n: DWORD = undefined; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) == 0) { + switch (windows.GetLastError()) { + .IO_PENDING => |err| return windows.errorBug(err), + .OPERATION_ABORTED => return error.Canceled, + .BROKEN_PIPE => return 0, + .HANDLE_EOF => return 0, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + .LOCK_VIOLATION => return error.LockViolation, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.NotOpenForReading, + else => |err| return windows.unexpectedError(err), + } + } + return n; +} + +fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - if (is_windows) { - const DWORD = windows.DWORD; - const OVERLAPPED = windows.OVERLAPPED; - var index: usize = 0; - var truncate: usize = 0; - var total: usize = 0; - while (true) { - try t.checkCancel(); - { - const untruncated = data[index]; - data[index] = untruncated[truncate..]; - defer data[index] = untruncated; - const buffer = data[index..]; - const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); - var n: DWORD = undefined; - var overlapped_data: OVERLAPPED = undefined; - const overlapped: ?*OVERLAPPED = if (offset) |off| blk: { - overlapped_data = .{ - .Internal = 0, - .InternalHigh = 0, - .DUMMYUNIONNAME = .{ - .DUMMYSTRUCTNAME = .{ - .Offset = @as(u32, @truncate(off)), - .OffsetHigh = @as(u32, @truncate(off >> 32)), - }, - }, - .hEvent = null, - }; - break :blk &overlapped_data; - } else null; - if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, overlapped) == 0) { - switch (windows.GetLastError()) { - .IO_PENDING => |err| return windows.statusBug(err), - .OPERATION_ABORTED => continue, - .BROKEN_PIPE => return 0, - .HANDLE_EOF => return 0, - .NETNAME_DELETED => return error.ConnectionResetByPeer, - .LOCK_VIOLATION => return error.LockViolation, - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_HANDLE => return error.NotOpenForReading, - else => |err| return windows.unexpectedError(err), - } - } - total += n; - truncate += n; - } - while (index < data.len and truncate >= data[index].len) { - truncate -= data[index].len; - index += 1; - } - } - return total; - } - - const have_pread_but_not_preadv = switch (native_os) { - .windows, .haiku, .serenity => true, - else => false, - }; - if (have_pread_but_not_preadv) { - @compileError("TODO"); - } + if (!have_preadv) @compileError("TODO"); var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; var i: usize = 0; @@ -2480,6 +2432,48 @@ fn fileReadPositional(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset } } +fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + + const DWORD = windows.DWORD; + const OVERLAPPED = windows.OVERLAPPED; + + var index: usize = 0; + while (data[index].len == 0) index += 1; + + const buffer = data[index]; + const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); + var n: DWORD = undefined; + var overlapped: OVERLAPPED = .{ + .Internal = 0, + .InternalHigh = 0, + .DUMMYUNIONNAME = .{ + .DUMMYSTRUCTNAME = .{ + .Offset = @as(u32, @truncate(offset)), + .OffsetHigh = @as(u32, @truncate(offset >> 32)), + }, + }, + .hEvent = null, + }; + + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, &overlapped) == 0) { + switch (windows.GetLastError()) { + .IO_PENDING => |err| return windows.errorBug(err), + .OPERATION_ABORTED => return error.Canceled, + .BROKEN_PIPE => return 0, + .HANDLE_EOF => return 0, + .NETNAME_DELETED => return error.ConnectionResetByPeer, + .LOCK_VIOLATION => return error.LockViolation, + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_HANDLE => return error.NotOpenForReading, + else => |err| return windows.unexpectedError(err), + } + } + + return n; +} + fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -2563,11 +2557,9 @@ fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelf // the file, we can let the openFileW call follow the symlink for us. const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - const prefixed_path_w_array = try windows.wToPrefixedFileW(null, image_path_name); - const prefixed_path_w = prefixed_path_w_array.span(); const cwd_handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle; - return dirOpenFileWindows(t, .{ .handle = cwd_handle }, prefixed_path_w, flags); + return dirOpenFileWindowsInner(t, .{ .handle = cwd_handle }, image_path_name, flags); } @panic("TODO"); } @@ -3493,7 +3485,16 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. } } -fn netSend( +fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = handle; + _ = data; + @panic("TODO"); +} + +fn netSendPosix( userdata: ?*anyopaque, handle: net.Socket.Handle, messages: []net.OutgoingMessage, @@ -3504,9 +3505,9 @@ fn netSend( const posix_flags: u32 = @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) | - @as(u32, if (flags.dont_route) posix.MSG.DONTROUTE else 0) | - @as(u32, if (flags.eor) posix.MSG.EOR else 0) | - @as(u32, if (flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (@hasDecl(posix.MSG, "DONTROUTE") and flags.dont_route) posix.MSG.DONTROUTE else 0) | + @as(u32, if (@hasDecl(posix.MSG, "EOR") and flags.eor) posix.MSG.EOR else 0) | + @as(u32, if (@hasDecl(posix.MSG, "OOB") and flags.oob) posix.MSG.OOB else 0) | @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) | posix.MSG.NOSIGNAL; @@ -3522,6 +3523,21 @@ fn netSend( return .{ null, i }; } +fn netSendWindows( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = handle; + _ = messages; + _ = flags; + @panic("TODO"); +} + fn netSendOne( t: *Threaded, handle: net.Socket.Handle, @@ -3676,7 +3692,7 @@ fn netSendMany( } } -fn netReceive( +fn netReceivePosix( userdata: ?*anyopaque, handle: net.Socket.Handle, message_buffer: []net.IncomingMessage, @@ -3805,6 +3821,25 @@ fn netReceive( } } +fn netReceiveWindows( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + if (!have_networking) return .{ error.NetworkDown, 0 }; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = handle; + _ = message_buffer; + _ = data_buffer; + _ = flags; + _ = timeout; + @panic("TODO"); +} + fn netWritePosix( userdata: ?*anyopaque, fd: net.Socket.Handle, @@ -3887,6 +3922,22 @@ fn netWritePosix( } } +fn netWriteWindows( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + header: []const u8, + data: []const []const u8, + splat: usize, +) net.Stream.Writer.Error!usize { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = handle; + _ = header; + _ = data; + _ = splat; + @panic("TODO"); +} + fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { // OS checks ptr addr before length so zero length vectors must be omitted. if (bytes.len == 0) return; @@ -3899,7 +3950,7 @@ fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; switch (native_os) { - .windows => closeSocketWindows(handle) catch recoverableOsBugDetected(), + .windows => closeSocketWindows(handle), else => posix.close(handle), } } @@ -4559,7 +4610,7 @@ fn lookupDns( message_i += 1; } } - _ = netSend(t, socket.handle, message_buffer[0..message_i], .{}); + _ = netSendPosix(t, socket.handle, message_buffer[0..message_i], .{}); } const timeout: Io.Timeout = .{ .deadline = .{ @@ -4607,7 +4658,7 @@ fn lookupDns( .data_ptr = query.ptr, .data_len = query.len, }; - _ = netSend(t, socket.handle, (&retry_message)[0..1], .{}); + _ = netSendPosix(t, socket.handle, (&retry_message)[0..1], .{}); continue; }, else => continue, @@ -5157,9 +5208,9 @@ fn closeSocketWindows(s: ws2_32.SOCKET) void { if (builtin.mode == .Debug) switch (rc) { 0 => {}, ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { - else => unreachable, + else => recoverableOsBugDetected(), }, - else => unreachable, + else => recoverableOsBugDetected(), }; } diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index feef19e0e4..65f857ea46 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -186,15 +186,6 @@ test "listen on a port, send bytes, receive bytes" { const io = testing.io; - if (builtin.os.tag == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (builtin.os.tag == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - // Try only the IPv4 variant as some CI builders have no IPv6 localhost // configured. const localhost: net.IpAddress = .{ .ip4 = .loopback(0) }; @@ -282,15 +273,6 @@ test "listen on a unix socket, send bytes, receive bytes" { const io = testing.io; - if (builtin.os.tag == .windows) { - _ = try std.os.windows.WSAStartup(2, 2); - } - defer { - if (builtin.os.tag == .windows) { - std.os.windows.WSACleanup() catch unreachable; - } - } - const socket_path = try generateFileName("socket.unix"); defer testing.allocator.free(socket_path); diff --git a/lib/std/crypto/Certificate/Bundle.zig b/lib/std/crypto/Certificate/Bundle.zig index e2090e01ac..cc52ce71d3 100644 --- a/lib/std/crypto/Certificate/Bundle.zig +++ b/lib/std/crypto/Certificate/Bundle.zig @@ -144,10 +144,12 @@ fn rescanWithPath(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp, cert_f const RescanWindowsError = Allocator.Error || ParseCertError || std.posix.UnexpectedError || error{FileNotFound}; -fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void { +fn rescanWindows(cb: *Bundle, gpa: Allocator, io: Io, now: Io.Timestamp) RescanWindowsError!void { cb.bytes.clearRetainingCapacity(); cb.map.clearRetainingCapacity(); + _ = io; + const w = std.os.windows; const GetLastError = w.GetLastError; const root = [4:0]u16{ 'R', 'O', 'O', 'T' }; @@ -157,7 +159,7 @@ fn rescanWindows(cb: *Bundle, gpa: Allocator) RescanWindowsError!void { }; defer _ = w.crypt32.CertCloseStore(store, 0); - const now_sec = std.time.timestamp(); + const now_sec = now.toSeconds(); var ctx = w.crypt32.CertEnumCertificatesInStore(store, null); while (ctx) |context| : (ctx = w.crypt32.CertEnumCertificatesInStore(store, ctx)) { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index ad02698354..26e1b7cfaa 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -2735,6 +2735,13 @@ pub fn statusBug(status: NTSTATUS) UnexpectedError { } } +pub fn errorBug(err: Win32Error) UnexpectedError { + switch (builtin.mode) { + .Debug => std.debug.panic("programmer bug caused syscall status: {t}", .{err}), + else => return error.Unexpected, + } +} + pub const Win32Error = @import("windows/win32error.zig").Win32Error; pub const NTSTATUS = @import("windows/ntstatus.zig").NTSTATUS; pub const LANG = @import("windows/lang.zig"); From 0107e584ef4c8071b9ab71f20aae83a5cfb91921 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 07:24:47 -0700 Subject: [PATCH 165/244] std.Io.Threaded: implement Windows futex functions --- lib/std/Io/Threaded.zig | 66 ++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 5aba86c2bb..29934320d3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4880,10 +4880,7 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca .FAULT => unreachable, // ptr was invalid else => unreachable, }; - return; - } - - if (native_os.isDarwin()) { + } else if (native_os.isDarwin()) { const c = std.c; const flags: c.UL = .{ .op = .COMPARE_AND_WAIT, @@ -4907,10 +4904,7 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca .TIMEDOUT => unreachable, else => unreachable, }; - return; - } - - if (builtin.cpu.arch.isWasm()) { + } else if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); try t.checkCancel(); const timeout: i64 = -1; @@ -4933,10 +4927,16 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca 2 => assert(!is_debug), // timeout else => assert(!is_debug), } - return; + } else if (is_windows) { + try t.checkCancel(); + switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { + .SUCCESS => {}, + .CANCELLED => return error.Canceled, + else => recoverableOsBugDetected(), + } + } else { + @compileError("TODO"); } - - @compileError("TODO"); } pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) void { @@ -4954,10 +4954,7 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi .FAULT => unreachable, // ptr was invalid else => unreachable, }; - return; - } - - if (native_os.isDarwin()) { + } else if (native_os.isDarwin()) { const c = std.c; const flags: c.UL = .{ .op = .COMPARE_AND_WAIT, @@ -4980,10 +4977,7 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi .TIMEDOUT => unreachable, else => unreachable, }; - return; - } - - if (builtin.cpu.arch.isWasm()) { + } else if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); const timeout: i64 = -1; const signed_expect: i32 = @bitCast(expect); @@ -5005,10 +4999,14 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi 2 => assert(!is_debug), // timeout else => assert(!is_debug), } - return; + } else if (is_windows) { + switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { + .SUCCESS, .CANCELLED => {}, + else => recoverableOsBugDetected(), + } + } else { + @compileError("TODO"); } - - @compileError("TODO"); } pub fn futexWaitDurationUncancelable(ptr: *const std.atomic.Value(u32), expect: u32, timeout: Io.Duration) void { @@ -5028,9 +5026,9 @@ pub fn futexWaitDurationUncancelable(ptr: *const std.atomic.Value(u32), expect: else => unreachable, }; return; + } else { + @compileError("TODO"); } - - @compileError("TODO"); } pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { @@ -5049,10 +5047,7 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { .FAULT => {}, // pointer became invalid while doing the wake else => unreachable, }; - return; - } - - if (native_os.isDarwin()) { + } else if (native_os.isDarwin()) { const c = std.c; const flags: c.UL = .{ .op = .COMPARE_AND_WAIT, @@ -5071,9 +5066,7 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { else => assert(!is_debug), } } - } - - if (builtin.cpu.arch.isWasm()) { + } else if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); assert(max_waiters != 0); const woken_count = asm volatile ( @@ -5086,10 +5079,15 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { [waiters] "r" (max_waiters), ); _ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled - return; + } else if (is_windows) { + assert(max_waiters != 0); + switch (max_waiters) { + 1 => windows.ntdll.RtlWakeAddressSingle(ptr), + else => windows.ntdll.RtlWakeAddressAll(ptr), + } + } else { + @compileError("TODO"); } - - @compileError("TODO"); } /// A thread-safe logical boolean value which can be `set` and `unset`. From dab8dd5e0306af4ef9b2388cfa8012ea596920ae Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 10:26:41 -0700 Subject: [PATCH 166/244] std.os.windows.ws2_32: remove 'A' variants --- lib/std/os/windows/ws2_32.zig | 124 +++------------------------------- 1 file changed, 11 insertions(+), 113 deletions(-) diff --git a/lib/std/os/windows/ws2_32.zig b/lib/std/os/windows/ws2_32.zig index 64f5f57d45..b40f84af76 100644 --- a/lib/std/os/windows/ws2_32.zig +++ b/lib/std/os/windows/ws2_32.zig @@ -1080,31 +1080,18 @@ pub const WSANETWORKEVENTS = extern struct { iErrorCode: [10]i32, }; -pub const addrinfo = addrinfoa; - -pub const addrinfoa = extern struct { +pub const ADDRINFOEXW = extern struct { flags: AI, family: i32, socktype: i32, protocol: i32, addrlen: usize, - canonname: ?[*:0]u8, - addr: ?*sockaddr, - next: ?*addrinfo, -}; - -pub const addrinfoexA = extern struct { - flags: AI, - family: i32, - socktype: i32, - protocol: i32, - addrlen: usize, - canonname: [*:0]u8, + canonname: [*:0]u16, addr: *sockaddr, blob: *anyopaque, bloblen: usize, provider: *GUID, - next: *addrinfoexA, + next: *ADDRINFOEXW, }; pub const sockaddr = extern struct { @@ -1851,18 +1838,6 @@ pub extern "ws2_32" fn WSAConnectByNameW( Reserved: *OVERLAPPED, ) callconv(.winapi) BOOL; -pub extern "ws2_32" fn WSAConnectByNameA( - s: SOCKET, - nodename: [*:0]const u8, - servicename: [*:0]const u8, - LocalAddressLength: ?*u32, - LocalAddress: ?*sockaddr, - RemoteAddressLength: ?*u32, - RemoteAddress: ?*sockaddr, - timeout: ?*const timeval, - Reserved: *OVERLAPPED, -) callconv(.winapi) BOOL; - pub extern "ws2_32" fn WSAConnectByList( s: SOCKET, SocketAddress: *SOCKET_ADDRESS_LIST, @@ -1876,12 +1851,6 @@ pub extern "ws2_32" fn WSAConnectByList( pub extern "ws2_32" fn WSACreateEvent() callconv(.winapi) HANDLE; -pub extern "ws2_32" fn WSADuplicateSocketA( - s: SOCKET, - dwProcessId: u32, - lpProtocolInfo: *WSAPROTOCOL_INFOA, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSADuplicateSocketW( s: SOCKET, dwProcessId: u32, @@ -1894,12 +1863,6 @@ pub extern "ws2_32" fn WSAEnumNetworkEvents( lpNetworkEvents: *WSANETWORKEVENTS, ) callconv(.winapi) i32; -pub extern "ws2_32" fn WSAEnumProtocolsA( - lpiProtocols: ?*i32, - lpProtocolBuffer: ?*WSAPROTOCOL_INFOA, - lpdwBufferLength: *u32, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSAEnumProtocolsW( lpiProtocols: ?*i32, lpProtocolBuffer: ?*WSAPROTOCOL_INFOW, @@ -2042,15 +2005,6 @@ pub extern "ws2_32" fn WSASetEvent( hEvent: HANDLE, ) callconv(.winapi) BOOL; -pub extern "ws2_32" fn WSASocketA( - af: i32, - @"type": i32, - protocol: i32, - lpProtocolInfo: ?*WSAPROTOCOL_INFOA, - g: u32, - dwFlags: u32, -) callconv(.winapi) SOCKET; - pub extern "ws2_32" fn WSASocketW( af: i32, @"type": i32, @@ -2068,14 +2022,6 @@ pub extern "ws2_32" fn WSAWaitForMultipleEvents( fAlertable: BOOL, ) callconv(.winapi) u32; -pub extern "ws2_32" fn WSAAddressToStringA( - lpsaAddress: *sockaddr, - dwAddressLength: u32, - lpProtocolInfo: ?*WSAPROTOCOL_INFOA, - lpszAddressString: [*]u8, - lpdwAddressStringLength: *u32, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSAAddressToStringW( lpsaAddress: *sockaddr, dwAddressLength: u32, @@ -2084,14 +2030,6 @@ pub extern "ws2_32" fn WSAAddressToStringW( lpdwAddressStringLength: *u32, ) callconv(.winapi) i32; -pub extern "ws2_32" fn WSAStringToAddressA( - AddressString: [*:0]const u8, - AddressFamily: i32, - lpProtocolInfo: ?*WSAPROTOCOL_INFOA, - lpAddress: *sockaddr, - lpAddressLength: *i32, -) callconv(.winapi) i32; - pub extern "ws2_32" fn WSAStringToAddressW( AddressString: [*:0]const u16, AddressFamily: i32, @@ -2156,30 +2094,12 @@ pub extern "ws2_32" fn WSAProviderCompleteAsyncCall( iRetCode: i32, ) callconv(.winapi) i32; -pub extern "mswsock" fn EnumProtocolsA( - lpiProtocols: ?*i32, - lpProtocolBuffer: *anyopaque, - lpdwBufferLength: *u32, -) callconv(.winapi) i32; - pub extern "mswsock" fn EnumProtocolsW( lpiProtocols: ?*i32, lpProtocolBuffer: *anyopaque, lpdwBufferLength: *u32, ) callconv(.winapi) i32; -pub extern "mswsock" fn GetAddressByNameA( - dwNameSpace: u32, - lpServiceType: *GUID, - lpServiceName: ?[*:0]u8, - lpiProtocols: ?*i32, - dwResolution: u32, - lpServiceAsyncInfo: ?*SERVICE_ASYNC_INFO, - lpCsaddrBuffer: *anyopaque, - lpAliasBuffer: ?[*:0]const u8, - lpdwAliasBufferLength: *u32, -) callconv(.winapi) i32; - pub extern "mswsock" fn GetAddressByNameW( dwNameSpace: u32, lpServiceType: *GUID, @@ -2193,42 +2113,24 @@ pub extern "mswsock" fn GetAddressByNameW( lpdwAliasBufferLength: *u32, ) callconv(.winapi) i32; -pub extern "mswsock" fn GetTypeByNameA( - lpServiceName: [*:0]u8, - lpServiceType: *GUID, -) callconv(.winapi) i32; - pub extern "mswsock" fn GetTypeByNameW( lpServiceName: [*:0]u16, lpServiceType: *GUID, ) callconv(.winapi) i32; -pub extern "mswsock" fn GetNameByTypeA( - lpServiceType: *GUID, - lpServiceName: [*:0]u8, - dwNameLength: u32, -) callconv(.winapi) i32; - pub extern "mswsock" fn GetNameByTypeW( lpServiceType: *GUID, lpServiceName: [*:0]u16, dwNameLength: u32, ) callconv(.winapi) i32; -pub extern "ws2_32" fn getaddrinfo( - pNodeName: ?[*:0]const u8, - pServiceName: ?[*:0]const u8, - pHints: ?*const addrinfoa, - ppResult: *?*addrinfoa, -) callconv(.winapi) i32; - -pub extern "ws2_32" fn GetAddrInfoExA( - pName: ?[*:0]const u8, - pServiceName: ?[*:0]const u8, - dwNameSapce: u32, +pub extern "ws2_32" fn GetAddrInfoExW( + pName: ?[*:0]const u16, + pServiceName: ?[*:0]const u16, + dwNameSpace: DWORD, lpNspId: ?*GUID, - hints: ?*const addrinfoexA, - ppResult: **addrinfoexA, + hints: ?*const ADDRINFOEXW, + ppResult: **ADDRINFOEXW, timeout: ?*timeval, lpOverlapped: ?*OVERLAPPED, lpCompletionRoutine: ?LPLOOKUPSERVICE_COMPLETION_ROUTINE, @@ -2242,12 +2144,8 @@ pub extern "ws2_32" fn GetAddrInfoExOverlappedResult( lpOverlapped: *OVERLAPPED, ) callconv(.winapi) i32; -pub extern "ws2_32" fn freeaddrinfo( - pAddrInfo: ?*addrinfoa, -) callconv(.winapi) void; - -pub extern "ws2_32" fn FreeAddrInfoEx( - pAddrInfoEx: ?*addrinfoexA, +pub extern "ws2_32" fn FreeAddrInfoExW( + pAddrInfoEx: ?*ADDRINFOEXW, ) callconv(.winapi) void; pub extern "ws2_32" fn getnameinfo( From ab003cd0545e54e66410b8e67136d4ef079b3198 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 10:30:06 -0700 Subject: [PATCH 167/244] std.Io.Threaded: implement netLookup for Windows --- lib/std/Io/Threaded.zig | 77 ++++++++++++++++++++++++++++++++++- lib/std/os/windows/ws2_32.zig | 62 +++++++++++++++------------- 2 files changed, 108 insertions(+), 31 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 29934320d3..fbbd9a985c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4058,8 +4058,81 @@ fn netLookupFallible( assert(name.len <= HostName.max_len); if (is_windows) { - // TODO use GetAddrInfoExW / GetAddrInfoExCancel - @compileError("TODO"); + var name_buffer: [HostName.max_len + 1]u16 = undefined; + const name_len = std.unicode.wtf8ToWtf16Le(&name_buffer, host_name.bytes) catch + unreachable; // HostName is prevalidated. + name_buffer[name_len] = 0; + const name_w = name_buffer[0..name_len :0]; + + var port_buffer: [8]u8 = undefined; + var port_buffer_wide: [8]u16 = undefined; + const port = std.fmt.bufPrint(&port_buffer, "{d}", .{options.port}) catch + unreachable; // `port_buffer` is big enough for decimal u16. + for (port, port_buffer[0..port.len]) |byte, *wide| wide.* = byte; + port_buffer_wide[port.len] = 0; + const port_w = port_buffer_wide[0..port.len :0]; + + const hints: ws2_32.ADDRINFOEXW = .{ + .flags = .{ .NUMERICSERV = true }, + .family = posix.AF.UNSPEC, + .socktype = posix.SOCK.STREAM, + .protocol = posix.IPPROTO.TCP, + .canonname = null, + .addr = null, + .addrlen = 0, + .blob = null, + .bloblen = 0, + .provider = null, + .next = null, + }; + var res: *ws2_32.ADDRINFOEXW = undefined; + const timeout: ?*ws2_32.timeval = null; + while (true) { + try t.checkCancel(); // TODO make requestCancel call GetAddrInfoExCancel + // TODO make this append to the queue eagerly rather than blocking until + // the whole thing finishes + const rc: ws2_32.WinsockError = @enumFromInt(ws2_32.GetAddrInfoExW(name_w, port_w, .DNS, null, &hints, &res, timeout, null, null)); + switch (rc) { + @as(ws2_32.WinsockError, @enumFromInt(0)) => break, + .EINTR => continue, + .ECANCELLED, .E_CANCELLED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .TRY_AGAIN => return error.NameServerFailure, + .EINVAL => |err| return wsaErrorBug(err), + .NO_RECOVERY => return error.NameServerFailure, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + .NOT_ENOUGH_MEMORY => return error.SystemResources, + .HOST_NOT_FOUND => return error.UnknownHostName, + .TYPE_NOT_FOUND => return error.ProtocolUnsupportedByAddressFamily, + .ESOCKTNOSUPPORT => return error.ProtocolUnsupportedBySystem, + else => |err| return windows.unexpectedWSAError(err), + } + } + defer ws2_32.FreeAddrInfoExW(res); + + var it: ?*ws2_32.ADDRINFOEXW = res; + var canon_name: ?[*:0]const u16 = null; + while (it) |info| : (it = info.next) { + const addr = info.addr orelse continue; + const storage: WsaAddress = .{ .any = addr.* }; + try resolved.putOne(t_io, .{ .address = addressFromWsa(&storage) }); + + if (info.canonname) |n| { + if (canon_name == null) { + canon_name = n; + } + } + } + if (canon_name) |n| { + const len = std.unicode.wtf16LeToWtf8(options.canonical_name_buffer, std.mem.sliceTo(n, 0)); + try resolved.putOne(t_io, .{ .canonical_name = .{ + .bytes = options.canonical_name_buffer[0..len], + } }); + } + return; } // On Linux, glibc provides getaddrinfo_a which is capable of supporting our semantics. diff --git a/lib/std/os/windows/ws2_32.zig b/lib/std/os/windows/ws2_32.zig index b40f84af76..c10759b153 100644 --- a/lib/std/os/windows/ws2_32.zig +++ b/lib/std/os/windows/ws2_32.zig @@ -702,28 +702,32 @@ pub const FIONBIO = -2147195266; pub const ADDRINFOEX_VERSION_2 = 2; pub const ADDRINFOEX_VERSION_3 = 3; pub const ADDRINFOEX_VERSION_4 = 4; -pub const NS_ALL = 0; -pub const NS_SAP = 1; -pub const NS_NDS = 2; -pub const NS_PEER_BROWSE = 3; -pub const NS_SLP = 5; -pub const NS_DHCP = 6; -pub const NS_TCPIP_LOCAL = 10; -pub const NS_TCPIP_HOSTS = 11; -pub const NS_DNS = 12; -pub const NS_NETBT = 13; -pub const NS_WINS = 14; -pub const NS_NLA = 15; -pub const NS_NBP = 20; -pub const NS_MS = 30; -pub const NS_STDA = 31; -pub const NS_NTDS = 32; -pub const NS_EMAIL = 37; -pub const NS_X500 = 40; -pub const NS_NIS = 41; -pub const NS_NISPLUS = 42; -pub const NS_WRQ = 50; -pub const NS_NETDES = 60; + +pub const NS = enum(u32) { + ALL = 0, + SAP = 1, + NDS = 2, + PEER_BROWSE = 3, + SLP = 5, + DHCP = 6, + TCPIP_LOCAL = 10, + TCPIP_HOSTS = 11, + DNS = 12, + NETBT = 13, + WINS = 14, + NLA = 15, + NBP = 20, + MS = 30, + STDA = 31, + NTDS = 32, + EMAIL = 37, + X500 = 40, + NIS = 41, + NISPLUS = 42, + WRQ = 50, + NETDES = 60, +}; + pub const NI_NOFQDN = 1; pub const NI_NUMERICHOST = 2; pub const NI_NAMEREQD = 4; @@ -1086,12 +1090,12 @@ pub const ADDRINFOEXW = extern struct { socktype: i32, protocol: i32, addrlen: usize, - canonname: [*:0]u16, - addr: *sockaddr, - blob: *anyopaque, + canonname: ?[*:0]u16, + addr: ?*sockaddr, + blob: ?*anyopaque, bloblen: usize, - provider: *GUID, - next: *ADDRINFOEXW, + provider: ?*GUID, + next: ?*ADDRINFOEXW, }; pub const sockaddr = extern struct { @@ -2101,7 +2105,7 @@ pub extern "mswsock" fn EnumProtocolsW( ) callconv(.winapi) i32; pub extern "mswsock" fn GetAddressByNameW( - dwNameSpace: u32, + dwNameSpace: NS, lpServiceType: *GUID, lpServiceName: ?[*:0]u16, lpiProtocols: ?*i32, @@ -2127,7 +2131,7 @@ pub extern "mswsock" fn GetNameByTypeW( pub extern "ws2_32" fn GetAddrInfoExW( pName: ?[*:0]const u16, pServiceName: ?[*:0]const u16, - dwNameSpace: DWORD, + dwNameSpace: NS, lpNspId: ?*GUID, hints: ?*const ADDRINFOEXW, ppResult: **ADDRINFOEXW, From 00a3123fbe79d9bb74b2c877130579299d2ba5ba Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 13:55:26 -0700 Subject: [PATCH 168/244] std.process.Child: update for std.Io changes --- lib/std/Io/Threaded.zig | 19 +++++++++---------- lib/std/posix.zig | 3 +-- lib/std/process/Child.zig | 16 ++++++++++++---- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index fbbd9a985c..a567878b82 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -211,7 +211,6 @@ pub fn io(t: *Threaded) Io { else => dirOpenFilePosix, }, .dirOpenDir = switch (builtin.os.tag) { - .windows => dirOpenDirWindows, .wasi => dirOpenDirWasi, .haiku => dirOpenDirHaiku, else => dirOpenDirPosix, @@ -2035,6 +2034,11 @@ fn dirOpenDirPosix( ) Io.Dir.OpenError!Io.Dir { const t: *Threaded = @ptrCast(@alignCast(userdata)); + if (is_windows) { + const sub_path_w = try windows.sliceToPrefixedFileW(dir.handle, sub_path); + return dirOpenDirWindows(t, dir, sub_path_w.span(), options); + } + var path_buffer: [posix.PATH_MAX]u8 = undefined; const sub_path_posix = try pathToPosix(sub_path, &path_buffer); @@ -2123,19 +2127,13 @@ fn dirOpenDirHaiku( } } -fn dirOpenDirWindows( - userdata: ?*anyopaque, +pub fn dirOpenDirWindows( + t: *Io.Threaded, dir: Io.Dir, - sub_path: []const u8, + sub_path_w: [:0]const u16, options: Io.Dir.OpenOptions, ) Io.Dir.OpenError!Io.Dir { - const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); - const w = windows; - const sub_path_w_array = try w.sliceToPrefixedFileW(dir.handle, sub_path); - const sub_path_w = sub_path_w_array.span(); - // TODO remove some of these flags if options.access_sub_paths is false const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE; @@ -2158,6 +2156,7 @@ fn dirOpenDirWindows( const open_reparse_point: w.DWORD = if (!options.follow_symlinks) w.FILE_OPEN_REPARSE_POINT else 0x0; var io_status_block: w.IO_STATUS_BLOCK = undefined; var result: Io.Dir = .{ .handle = undefined }; + try t.checkCancel(); const rc = w.ntdll.NtCreateFile( &result.handle, access_mask, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index bb556be133..f708dfeaba 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -3769,7 +3769,6 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .SUCCESS => return, .ACCES => return error.AccessDenied, .PERM => return error.PermissionDenied, - .ADDRINUSE => return error.AddressInUse, .ADDRNOTAVAIL => return error.AddressUnavailable, .AFNOSUPPORT => return error.AddressFamilyUnsupported, .AGAIN, .INPROGRESS => return error.WouldBlock, @@ -3779,7 +3778,7 @@ pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) Conne .CONNRESET => return error.ConnectionResetByPeer, .FAULT => unreachable, // The socket structure address is outside the user's address space. .INTR => continue, - .ISCONN => return error.AlreadyConnected, // The socket is already connected. + .ISCONN => @panic("AlreadyConnected"), // The socket is already connected. .HOSTUNREACH => return error.NetworkUnreachable, .NETUNREACH => return error.NetworkUnreachable, .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index 7ca919ca46..e49c5c4532 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -1084,16 +1084,24 @@ fn windowsCreateProcessPathExt( // or a version with a supported PATHEXT appended. We then try calling CreateProcessW // with the found versions in the appropriate order. + // In the future, child process execution needs to move to Io implementation. + // Under those conditions, here we will have access to lower level directory + // opening function knowing which implementation we are in. Here, we imitate + // that scenario. + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.io(); + var dir = dir: { // needs to be null-terminated try dir_buf.append(allocator, 0); defer dir_buf.shrinkRetainingCapacity(dir_path_len); const dir_path_z = dir_buf.items[0 .. dir_buf.items.len - 1 :0]; const prefixed_path = try windows.wToPrefixedFileW(null, dir_path_z); - break :dir fs.cwd().openDirW(prefixed_path.span().ptr, .{ .iterate = true }) catch - return error.FileNotFound; + break :dir threaded.dirOpenDirWindows(.cwd(), prefixed_path.span(), .{ + .iterate = true, + }) catch return error.FileNotFound; }; - defer dir.close(); + defer dir.close(io); // Add wildcard and null-terminator try app_buf.append(allocator, '*'); @@ -1127,7 +1135,7 @@ fn windowsCreateProcessPathExt( .Buffer = @constCast(app_name_wildcard.ptr), }; const rc = windows.ntdll.NtQueryDirectoryFile( - dir.fd, + dir.handle, null, null, null, From 2f260256903a5130e879ab7dc020fa160befc4d9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 21 Oct 2025 14:12:28 -0700 Subject: [PATCH 169/244] std: fix build failure on wasm32-freestanding --- lib/std/fs/File.zig | 6 +----- lib/std/posix.zig | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index ba0f29fd87..be54041485 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -32,11 +32,7 @@ pub const Kind = Io.File.Kind; /// the `touch` command, which would correspond to `0o644`. However, POSIX /// libc implementations use `0o666` inside `fopen` and then rely on the /// process-scoped "umask" setting to adjust this number for file creation. -pub const default_mode = switch (builtin.os.tag) { - .windows => 0, - .wasi => 0, - else => 0o666, -}; +pub const default_mode: Mode = if (Mode == u0) 0 else 0o666; /// Deprecated in favor of `Io.File.OpenError`. pub const OpenError = Io.File.OpenError || error{WouldBlock}; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index f708dfeaba..b240a6583d 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -52,8 +52,9 @@ else switch (native_os) { pub const fd_t = void; pub const uid_t = void; pub const gid_t = void; - pub const mode_t = void; + pub const mode_t = u0; pub const ino_t = void; + pub const IFNAMESIZE = {}; }, }; From a3ddca36571993af4a575296292e17471890fc87 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 04:20:52 -0700 Subject: [PATCH 170/244] std.Io.Threaded: delete Windows implementation of if_nametoindex Microsoft documentation says "The if_nametoindex function is implemented for portability of applications with Unix environments, but the ConvertInterface functions are preferred." This was also the only dependency on iphlpapi. --- lib/std/Io/Threaded.zig | 4 +--- lib/std/os/windows/ws2_32.zig | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a567878b82..59af60032a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4000,9 +4000,7 @@ fn netInterfaceNameResolve( if (native_os == .windows) { try t.checkCancel(); - const index = ws2_32.if_nametoindex(&name.bytes); - if (index == 0) return error.InterfaceNotFound; - return .{ .index = index }; + @panic("TODO"); } if (builtin.link_libc) { diff --git a/lib/std/os/windows/ws2_32.zig b/lib/std/os/windows/ws2_32.zig index c10759b153..5d5e29c00e 100644 --- a/lib/std/os/windows/ws2_32.zig +++ b/lib/std/os/windows/ws2_32.zig @@ -2161,7 +2161,3 @@ pub extern "ws2_32" fn getnameinfo( ServiceBufferName: u32, Flags: i32, ) callconv(.winapi) i32; - -pub extern "iphlpapi" fn if_nametoindex( - InterfaceName: [*:0]const u8, -) callconv(.winapi) u32; From 032152409be26f12031c6c16ac9de5e29cd08b8f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 04:43:46 -0700 Subject: [PATCH 171/244] std.Io.Threaded: fix signature of dirMakeOpenPathWasi --- lib/std/Io/Threaded.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 59af60032a..bce028c13f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1159,7 +1159,12 @@ fn dirMakeOpenPathWindows( } } -fn dirMakeOpenPathWasi(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeOpenPathError!Io.Dir { +fn dirMakeOpenPathWasi( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + mode: Io.Dir.OpenOptions, +) Io.Dir.MakeOpenPathError!Io.Dir { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; _ = dir; From 873bcb5aa66fd4fdd747b90ba0ed529117478368 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 05:01:37 -0700 Subject: [PATCH 172/244] fix some std.Io compilation failures --- lib/compiler/objcopy.zig | 7 +++++-- test/standalone/child_process/main.zig | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/compiler/objcopy.zig b/lib/compiler/objcopy.zig index 5908f8b73d..3e8c4a508a 100644 --- a/lib/compiler/objcopy.zig +++ b/lib/compiler/objcopy.zig @@ -29,7 +29,6 @@ pub fn main() !void { } fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { - _ = gpa; var i: usize = 0; var opt_out_fmt: ?std.Target.ObjectFormat = null; var opt_input: ?[]const u8 = null; @@ -148,12 +147,16 @@ fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void const input = opt_input orelse fatal("expected input parameter", .{}); const output = opt_output orelse fatal("expected output parameter", .{}); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + const input_file = fs.cwd().openFile(input, .{}) catch |err| fatal("failed to open {s}: {t}", .{ input, err }); defer input_file.close(); const stat = input_file.stat() catch |err| fatal("failed to stat {s}: {t}", .{ input, err }); - var in: File.Reader = .initSize(input_file, &input_buffer, stat.size); + var in: File.Reader = .initSize(input_file, io, &input_buffer, stat.size); const elf_hdr = std.elf.Header.read(&in.interface) catch |err| switch (err) { error.ReadFailed => fatal("unable to read {s}: {t}", .{ input, in.err.? }), diff --git a/test/standalone/child_process/main.zig b/test/standalone/child_process/main.zig index 4be5e1fec3..5970cdd952 100644 --- a/test/standalone/child_process/main.zig +++ b/test/standalone/child_process/main.zig @@ -20,6 +20,10 @@ pub fn main() !void { }; defer if (needs_free) gpa.free(child_path); + var threaded: std.Io.Threaded = .init(gpa); + defer threaded.deinit(); + const io = threaded.io(); + var child = std.process.Child.init(&.{ child_path, "hello arg" }, gpa); child.stdin_behavior = .Pipe; child.stdout_behavior = .Pipe; @@ -32,7 +36,7 @@ pub fn main() !void { const hello_stdout = "hello from stdout"; var buf: [hello_stdout.len]u8 = undefined; - var stdout_reader = child.stdout.?.readerStreaming(&.{}); + var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); const n = try stdout_reader.interface.readSliceShort(&buf); if (!std.mem.eql(u8, buf[0..n], hello_stdout)) { testError("child stdout: '{s}'; want '{s}'", .{ buf[0..n], hello_stdout }); From 59ffa607a46135d0d02af72a40914e438e56d586 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 06:05:55 -0700 Subject: [PATCH 173/244] std.Io.Threaded: fix sending invalid pointer OS wants valid control pointer even when len is zero --- lib/std/Io/Threaded.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index bce028c13f..4a216c5601 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -3555,7 +3555,8 @@ fn netSendOne( .namelen = addressToPosix(message.address, &addr), .iov = (&iovec)[0..1], .iovlen = 1, - .control = @constCast(message.control.ptr), + // OS returns EINVAL if this pointer is invalid even if controllen is zero. + .control = if (message.control.len == 0) null else @constCast(message.control.ptr), .controllen = @intCast(message.control.len), .flags = 0, }; From a5c309a692f49d205b1e6260ac9c55c9815636eb Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 06:06:49 -0700 Subject: [PATCH 174/244] std.Io.net.Socket.send: fix compilation errors --- lib/std/Io/net.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index f262a0d6b5..fc7482f7cb 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -1075,7 +1075,8 @@ pub const Socket = struct { /// Transfers `data` to `dest`, connectionless, in one packet. pub fn send(s: *const Socket, io: Io, dest: *const IpAddress, data: []const u8) SendError!void { var message: OutgoingMessage = .{ .address = dest, .data_ptr = data.ptr, .data_len = data.len }; - try io.vtable.netSend(io.userdata, s.handle, &message, .{}); + const err, const n = io.vtable.netSend(io.userdata, s.handle, (&message)[0..1], .{}); + if (n != 1) return err.?; if (message.data_len != data.len) return error.MessageOversize; } From d6b0686b055070f390bc3465dbcf678bb590ecae Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 10:52:25 -0700 Subject: [PATCH 175/244] std.Io: add Kqueue implementation --- lib/std/Io.zig | 1 + lib/std/Io/IoUring.zig | 7 +- lib/std/Io/Kqueue.zig | 535 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 540 insertions(+), 3 deletions(-) create mode 100644 lib/std/Io/Kqueue.zig diff --git a/lib/std/Io.zig b/lib/std/Io.zig index a3cb2cf29f..d89f2d23af 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -559,6 +559,7 @@ const Io = @This(); pub const Evented = switch (builtin.os.tag) { .linux => @import("Io/IoUring.zig"), + .dragonfly, .freebsd, .netbsd, .openbsd, .macos, .ios, .tvos, .visionos, .watchos => @import("Io/Kqueue.zig"), else => void, }; pub const Threaded = @import("Io/Threaded.zig"); diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig index c6dca1597d..43ce8e1c0d 100644 --- a/lib/std/Io/IoUring.zig +++ b/lib/std/Io/IoUring.zig @@ -1,9 +1,10 @@ -const std = @import("../std.zig"); +const EventLoop = @This(); const builtin = @import("builtin"); + +const std = @import("../std.zig"); +const Io = std.Io; 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; diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig new file mode 100644 index 0000000000..edfa76cd5e --- /dev/null +++ b/lib/std/Io/Kqueue.zig @@ -0,0 +1,535 @@ +const Kqueue = @This(); +const builtin = @import("builtin"); + +const std = @import("../std.zig"); +const Io = std.Io; +const Dir = std.Io.Dir; +const File = std.Io.File; +const net = std.Io.net; +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; + +/// Must be a thread-safe allocator. +gpa: Allocator, + +pub fn init(gpa: Allocator) Kqueue { + return .{ + .gpa = gpa, + }; +} + +pub fn deinit(k: *Kqueue) void { + k.* = undefined; +} + +pub fn io(k: *Kqueue) Io { + return .{ + .userdata = k, + .vtable = &.{ + .async = async, + .concurrent = concurrent, + .await = await, + .cancel = cancel, + .cancelRequested = cancelRequested, + .select = select, + + .groupAsync = groupAsync, + .groupWait = groupWait, + .groupWaitUncancelable = groupWaitUncancelable, + .groupCancel = groupCancel, + + .mutexLock = mutexLock, + .mutexLockUncancelable = mutexLockUncancelable, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWaitUncancelable = conditionWaitUncancelable, + .conditionWake = conditionWake, + + .dirMake = dirMake, + .dirMakePath = dirMakePath, + .dirMakeOpenPath = dirMakeOpenPath, + .dirStat = dirStat, + .dirStatPath = dirStatPath, + + .fileStat = fileStat, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirOpenFile = dirOpenFile, + .dirOpenDir = dirOpenDir, + .dirClose = dirClose, + .fileClose = fileClose, + .fileWriteStreaming = fileWriteStreaming, + .fileWritePositional = fileWritePositional, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, + .openSelfExe = openSelfExe, + + .now = now, + .sleep = sleep, + + .netListenIp = netListenIp, + .netListenUnix = netListenUnix, + .netAccept = netAccept, + .netBindIp = netBindIp, + .netConnectIp = netConnectIp, + .netConnectUnix = netConnectUnix, + .netClose = netClose, + .netRead = netRead, + .netWrite = netWrite, + .netSend = netSend, + .netReceive = netReceive, + .netInterfaceNameResolve = netInterfaceNameResolve, + .netInterfaceName = netInterfaceName, + .netLookup = netLookup, + }, + }; +} + +fn async( + userdata: ?*anyopaque, + result: []u8, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) ?*Io.AnyFuture { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = result; + _ = result_alignment; + _ = context; + _ = context_alignment; + _ = start; + @panic("TODO"); +} + +fn concurrent( + userdata: ?*anyopaque, + result_len: usize, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, +) error{OutOfMemory}!*Io.AnyFuture { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = result_len; + _ = result_alignment; + _ = context; + _ = context_alignment; + _ = start; + @panic("TODO"); +} + +fn await( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = any_future; + _ = result; + _ = result_alignment; + @panic("TODO"); +} + +fn cancel( + userdata: ?*anyopaque, + any_future: *Io.AnyFuture, + result: []u8, + result_alignment: std.mem.Alignment, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = any_future; + _ = result; + _ = result_alignment; + @panic("TODO"); +} + +fn cancelRequested(userdata: ?*anyopaque) bool { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + @panic("TODO"); +} + +fn groupAsync( + userdata: ?*anyopaque, + group: *Io.Group, + context: []const u8, + context_alignment: std.mem.Alignment, + start: *const fn (*Io.Group, context: *const anyopaque) void, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = context; + _ = context_alignment; + _ = start; + @panic("TODO"); +} + +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) Io.Cancelable!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = token; + @panic("TODO"); +} + +fn groupWaitUncancelable(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = token; + @panic("TODO"); +} + +fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = group; + _ = token; + @panic("TODO"); +} + +fn select(userdata: ?*anyopaque, futures: []const *Io.AnyFuture) Io.Cancelable!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = futures; + @panic("TODO"); +} + +fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = prev_state; + _ = mutex; + @panic("TODO"); +} +fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = prev_state; + _ = mutex; + @panic("TODO"); +} +fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = prev_state; + _ = mutex; + @panic("TODO"); +} + +fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = cond; + _ = mutex; + @panic("TODO"); +} +fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = cond; + _ = mutex; + @panic("TODO"); +} +fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = cond; + _ = wake; + @panic("TODO"); +} + +fn dirMake(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} +fn dirMakePath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = mode; + @panic("TODO"); +} +fn dirMakeOpenPath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.OpenOptions) Dir.MakeOpenPathError!Dir { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirStat(userdata: ?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + @panic("TODO"); +} +fn dirStatPath(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.StatPathOptions) Dir.StatPathError!File.Stat { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirAccess(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.AccessOptions) Dir.AccessError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirCreateFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = flags; + @panic("TODO"); +} +fn dirOpenFile(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = flags; + @panic("TODO"); +} +fn dirOpenDir(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, options: Dir.OpenOptions) Dir.OpenError!Dir { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + _ = sub_path; + _ = options; + @panic("TODO"); +} +fn dirClose(userdata: ?*anyopaque, dir: Dir) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dir; + @panic("TODO"); +} +fn fileStat(userdata: ?*anyopaque, file: File) File.StatError!File.Stat { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + @panic("TODO"); +} +fn fileClose(userdata: ?*anyopaque, file: File) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + @panic("TODO"); +} +fn fileWriteStreaming(userdata: ?*anyopaque, file: File, buffer: [][]const u8) File.WriteStreamingError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = buffer; + @panic("TODO"); +} +fn fileWritePositional(userdata: ?*anyopaque, file: File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = buffer; + _ = offset; + @panic("TODO"); +} +fn fileReadStreaming(userdata: ?*anyopaque, file: File, data: [][]u8) File.ReadStreamingError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = data; + @panic("TODO"); +} +fn fileReadPositional(userdata: ?*anyopaque, file: File, data: [][]u8, offset: u64) File.ReadPositionalError!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = data; + _ = offset; + @panic("TODO"); +} +fn fileSeekBy(userdata: ?*anyopaque, file: File, relative_offset: i64) File.SeekError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = relative_offset; + @panic("TODO"); +} +fn fileSeekTo(userdata: ?*anyopaque, file: File, absolute_offset: u64) File.SeekError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + _ = absolute_offset; + @panic("TODO"); +} +fn openSelfExe(userdata: ?*anyopaque, file: File.OpenFlags) File.OpenSelfExeError!File { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = file; + @panic("TODO"); +} + +fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = clock; + @panic("TODO"); +} +fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = timeout; + @panic("TODO"); +} + +fn netListenIp( + userdata: ?*anyopaque, + address: net.IpAddress, + options: net.IpAddress.ListenOptions, +) net.IpAddress.ListenError!net.Server { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = address; + _ = options; + @panic("TODO"); +} +fn netAccept(userdata: ?*anyopaque, server: net.Socket.Handle) net.Server.AcceptError!net.Stream { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = server; + @panic("TODO"); +} +fn netBindIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = address; + _ = options; + @panic("TODO"); +} +fn netConnectIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = address; + _ = options; + @panic("TODO"); +} +fn netListenUnix( + userdata: ?*anyopaque, + unix_address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = unix_address; + _ = options; + @panic("TODO"); +} +fn netConnectUnix( + userdata: ?*anyopaque, + unix_address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = unix_address; + @panic("TODO"); +} +fn netSend( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + outgoing_messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = handle; + _ = outgoing_messages; + _ = flags; + @panic("TODO"); +} +fn netReceive( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = handle; + _ = message_buffer; + _ = data_buffer; + _ = flags; + _ = timeout; + @panic("TODO"); +} +fn netRead(userdata: ?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = src; + _ = data; + @panic("TODO"); +} +fn netWrite(userdata: ?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = dest; + _ = header; + _ = data; + _ = splat; + @panic("TODO"); +} +fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = handle; + @panic("TODO"); +} +fn netInterfaceNameResolve( + userdata: ?*anyopaque, + name: *const net.Interface.Name, +) net.Interface.Name.ResolveError!net.Interface { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = name; + @panic("TODO"); +} +fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = interface; + @panic("TODO"); +} +fn netLookup( + userdata: ?*anyopaque, + host_name: net.HostName, + result: *Io.Queue(net.HostName.LookupResult), + options: net.HostName.LookupOptions, +) void { + const k: *Kqueue = @ptrCast(@alignCast(userdata)); + _ = k; + _ = host_name; + _ = result; + _ = options; + @panic("TODO"); +} From df84dc18bc2fa18770591be4d1e0cabc8d4b28c2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 13:21:13 -0700 Subject: [PATCH 176/244] add bind --- lib/std/Io/Kqueue.zig | 201 +++++++++++++++++++++++++++++++++++++--- lib/std/Io/Threaded.zig | 16 ++-- 2 files changed, 194 insertions(+), 23 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index edfa76cd5e..eef959155a 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -9,6 +9,9 @@ const net = std.Io.net; const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; +const posix = std.posix; +const IpAddress = std.Io.net.IpAddress; +const errnoBug = std.Io.Threaded.errnoBug; /// Must be a thread-safe allocator. gpa: Allocator, @@ -97,14 +100,10 @@ fn async( context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) ?*Io.AnyFuture { - const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = result; - _ = result_alignment; - _ = context; - _ = context_alignment; - _ = start; - @panic("TODO"); + return concurrent(userdata, result.len, result_alignment, context, context_alignment, start) catch { + start(context.ptr, result.ptr); + return null; + }; } fn concurrent( @@ -156,7 +155,7 @@ fn cancel( fn cancelRequested(userdata: ?*anyopaque) bool { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; - @panic("TODO"); + return false; // TODO } fn groupAsync( @@ -419,12 +418,23 @@ fn netAccept(userdata: ?*anyopaque, server: net.Socket.Handle) net.Server.Accept _ = server; @panic("TODO"); } -fn netBindIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.BindOptions) net.IpAddress.BindError!net.Socket { +fn netBindIp( + userdata: ?*anyopaque, + address: *const net.IpAddress, + options: net.IpAddress.BindOptions, +) net.IpAddress.BindError!net.Socket { const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = address; - _ = options; - @panic("TODO"); + const family = Io.Threaded.posixAddressFamily(address); + const socket_fd = try openSocketPosix(k, family, options); + errdefer posix.close(socket_fd); + var storage: Io.Threaded.PosixAddress = undefined; + var addr_len = Io.Threaded.addressToPosix(address, &storage); + try posixBind(k, socket_fd, &storage.any, addr_len); + try posixGetSockName(k, socket_fd, &storage.any, &addr_len); + return .{ + .handle = socket_fd, + .address = Io.Threaded.addressFromPosix(&storage), + }; } fn netConnectIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream { const k: *Kqueue = @ptrCast(@alignCast(userdata)); @@ -453,6 +463,7 @@ fn netConnectUnix( _ = unix_address; @panic("TODO"); } + fn netSend( userdata: ?*anyopaque, handle: net.Socket.Handle, @@ -460,12 +471,22 @@ fn netSend( flags: net.SendFlags, ) struct { ?net.Socket.SendError, usize } { const k: *Kqueue = @ptrCast(@alignCast(userdata)); + + const posix_flags: u32 = + @as(u32, if (@hasDecl(posix.MSG, "CONFIRM") and flags.confirm) posix.MSG.CONFIRM else 0) | + @as(u32, if (@hasDecl(posix.MSG, "DONTROUTE") and flags.dont_route) posix.MSG.DONTROUTE else 0) | + @as(u32, if (@hasDecl(posix.MSG, "EOR") and flags.eor) posix.MSG.EOR else 0) | + @as(u32, if (@hasDecl(posix.MSG, "OOB") and flags.oob) posix.MSG.OOB else 0) | + @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) | + posix.MSG.NOSIGNAL; + _ = k; + _ = posix_flags; _ = handle; _ = outgoing_messages; - _ = flags; @panic("TODO"); } + fn netReceive( userdata: ?*anyopaque, handle: net.Socket.Handle, @@ -533,3 +554,153 @@ fn netLookup( _ = options; @panic("TODO"); } + +fn openSocketPosix( + k: *Kqueue, + family: posix.sa_family_t, + options: IpAddress.BindOptions, +) error{ + AddressFamilyUnsupported, + ProtocolUnsupportedBySystem, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + SystemResources, + ProtocolUnsupportedByAddressFamily, + SocketModeUnsupported, + OptionUnsupported, + Unexpected, + Canceled, +}!posix.socket_t { + const mode = Io.Threaded.posixSocketMode(options.mode); + const protocol = Io.Threaded.posixProtocol(options.protocol); + const socket_fd = while (true) { + try k.checkCancel(); + const flags: u32 = mode | if (Io.Threaded.socket_flags_unsupported) 0 else posix.SOCK.CLOEXEC; + const socket_rc = posix.system.socket(family, flags, protocol); + switch (posix.errno(socket_rc)) { + .SUCCESS => { + const fd: posix.fd_t = @intCast(socket_rc); + errdefer posix.close(fd); + if (Io.Threaded.socket_flags_unsupported) { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC)))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + } + + var fl_flags: usize = while (true) { + try k.checkCancel(); + const rc = posix.system.fcntl(fd, posix.F.GETFL, @as(usize, 0)); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + else => |err| return posix.unexpectedErrno(err), + } + } + } + break fd; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .INVAL => return error.ProtocolUnsupportedBySystem, + .MFILE => return error.ProcessFdQuotaExceeded, + .NFILE => return error.SystemFdQuotaExceeded, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .PROTONOSUPPORT => return error.ProtocolUnsupportedByAddressFamily, + .PROTOTYPE => return error.SocketModeUnsupported, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(socket_fd); + + if (options.ip6_only) { + if (posix.IPV6 == void) return error.OptionUnsupported; + try setSocketOption(k, socket_fd, posix.IPPROTO.IPV6, posix.IPV6.V6ONLY, 0); + } + + return socket_fd; +} + +fn posixBind( + k: *Kqueue, + socket_fd: posix.socket_t, + addr: *const posix.sockaddr, + addr_len: posix.socklen_t, +) !void { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.bind(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ADDRINUSE => return error.AddressInUse, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // invalid `sockfd` + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ADDRNOTAVAIL => return error.AddressUnavailable, + .FAULT => |err| return errnoBug(err), // invalid `addr` pointer + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn posixGetSockName(k: *Kqueue, socket_fd: posix.fd_t, addr: *posix.sockaddr, addr_len: *posix.socklen_t) !void { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.getsockname(socket_fd, addr, addr_len))) { + .SUCCESS => break, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOTSOCK => |err| return errnoBug(err), // always a race condition + .NOBUFS => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn setSocketOption(k: *Kqueue, fd: posix.fd_t, level: i32, opt_name: u32, option: u32) !void { + const o: []const u8 = @ptrCast(&option); + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.setsockopt(fd, level, opt_name, o.ptr, @intCast(o.len)))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOTSOCK => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn checkCancel(k: *Kqueue) error{Canceled}!void { + if (cancelRequested(k)) return error.Canceled; +} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 4a216c5601..c1365e0f44 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -285,7 +285,7 @@ pub fn io(t: *Threaded) Io { }; } -const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 +pub const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 const have_accept4 = !socket_flags_unsupported; const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); const have_networking = builtin.os.tag != .wasi; @@ -4283,7 +4283,7 @@ fn netLookupFallible( return error.OptionUnsupported; } -const PosixAddress = extern union { +pub const PosixAddress = extern union { any: posix.sockaddr, in: posix.sockaddr.in, in6: posix.sockaddr.in6, @@ -4301,14 +4301,14 @@ const WsaAddress = extern union { un: ws2_32.sockaddr.un, }; -fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t { +pub fn posixAddressFamily(a: *const IpAddress) posix.sa_family_t { return switch (a.*) { .ip4 => posix.AF.INET, .ip6 => posix.AF.INET6, }; } -fn addressFromPosix(posix_address: *const PosixAddress) IpAddress { +pub fn addressFromPosix(posix_address: *const PosixAddress) IpAddress { return switch (posix_address.any.family) { posix.AF.INET => .{ .ip4 = address4FromPosix(&posix_address.in) }, posix.AF.INET6 => .{ .ip6 = address6FromPosix(&posix_address.in6) }, @@ -4324,7 +4324,7 @@ fn addressFromWsa(wsa_address: *const WsaAddress) IpAddress { }; } -fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t { +pub fn addressToPosix(a: *const IpAddress, storage: *PosixAddress) posix.socklen_t { return switch (a.*) { .ip4 => |ip4| { storage.in = address4ToPosix(ip4); @@ -4405,7 +4405,7 @@ fn address6ToPosix(a: *const net.Ip6Address) posix.sockaddr.in6 { }; } -fn errnoBug(err: posix.E) Io.UnexpectedError { +pub fn errnoBug(err: posix.E) Io.UnexpectedError { switch (builtin.mode) { .Debug => std.debug.panic("programmer bug caused syscall error: {t}", .{err}), else => return error.Unexpected, @@ -4419,7 +4419,7 @@ fn wsaErrorBug(err: ws2_32.WinsockError) Io.UnexpectedError { } } -fn posixSocketMode(mode: net.Socket.Mode) u32 { +pub fn posixSocketMode(mode: net.Socket.Mode) u32 { return switch (mode) { .stream => posix.SOCK.STREAM, .dgram => posix.SOCK.DGRAM, @@ -4429,7 +4429,7 @@ fn posixSocketMode(mode: net.Socket.Mode) u32 { }; } -fn posixProtocol(protocol: ?net.Protocol) u32 { +pub fn posixProtocol(protocol: ?net.Protocol) u32 { return @intFromEnum(protocol orelse return 0); } From 41070932f8a3a1dead7fb424f427b04824c19c72 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 22 Oct 2025 13:21:59 -0700 Subject: [PATCH 177/244] revert adding asyncDetached instead we will have Io.Group --- lib/std/Io/IoUring.zig | 114 ++++------------------------------------- 1 file changed, 10 insertions(+), 104 deletions(-) diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig index 43ce8e1c0d..6d31f337c5 100644 --- a/lib/std/Io/IoUring.zig +++ b/lib/std/Io/IoUring.zig @@ -10,12 +10,9 @@ const IoUring = std.os.linux.IoUring; /// Must be a thread-safe allocator. gpa: Allocator, +mutex: std.Thread.Mutex, main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)), threads: Thread.List, -detached: struct { - mutex: std.Io.Mutex, - list: std.DoublyLinkedList, -}, /// Empirically saw >128KB being used by the self-hosted backend to panic. const idle_stack_size = 256 * 1024; @@ -142,7 +139,6 @@ pub fn io(el: *EventLoop) Io { .async = async, .concurrent = concurrent, .await = await, - .asyncDetached = asyncDetached, .select = select, .cancel = cancel, .cancelRequested = cancelRequested, @@ -172,16 +168,13 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { errdefer gpa.free(allocated_slice); el.* = .{ .gpa = gpa, + .mutex = .{}, .main_fiber_buffer = undefined, .threads = .{ .allocated = @ptrCast(allocated_slice[0..threads_size]), .reserved = 1, .active = 1, }, - .detached = .{ - .mutex = .init, - .list = .{}, - }, }; const main_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); main_fiber.* = .{ @@ -223,22 +216,6 @@ pub fn init(el: *EventLoop, gpa: Allocator) !void { } pub fn deinit(el: *EventLoop) void { - while (true) cancel(el, detached_future: { - el.detached.mutex.lock(el.io()) catch |err| switch (err) { - error.Canceled => unreachable, // main fiber cannot be canceled - }; - defer el.detached.mutex.unlock(el.io()); - const detached: *DetachedClosure = @fieldParentPtr( - "detached_queue_node", - el.detached.list.pop() orelse break, - ); - // notify the detached fiber that it is no longer allowed to recycle itself - detached.detached_queue_node = .{ - .prev = &detached.detached_queue_node, - .next = &detached.detached_queue_node, - }; - break :detached_future @ptrCast(detached.fiber); - }, &.{}, .@"1"); const active_threads = @atomicLoad(u32, &el.threads.active, .acquire); for (el.threads.allocated[0..active_threads]) |*thread| { const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); @@ -492,7 +469,7 @@ const SwitchMessage = struct { const PendingTask = union(enum) { nothing, reschedule, - recycle, + recycle: *Fiber, register_awaiter: *?*Fiber, register_select: []const *Io.AnyFuture, mutex_lock: struct { @@ -516,10 +493,8 @@ const SwitchMessage = struct { assert(prev_fiber.queue_next == null); el.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); }, - .recycle => { - const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); - assert(prev_fiber.queue_next == null); - el.recycle(prev_fiber); + .recycle => |fiber| { + el.recycle(fiber); }, .register_awaiter => |awaiter| { const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); @@ -829,12 +804,9 @@ fn fiberEntry() callconv(.naked) void { switch (builtin.cpu.arch) { .x86_64 => asm volatile ( \\ leaq 8(%%rsp), %%rdi - \\ jmpq *(%%rsp) - ), - .aarch64 => asm volatile ( - \\ mov x0, sp - \\ ldr x2, [sp, #-8] - \\ br x2 + \\ jmp %[AsyncClosure_call:P] + : + : [AsyncClosure_call] "X" (&AsyncClosure.call), ), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), } @@ -905,18 +877,16 @@ fn concurrent( std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = .fromFiber(fiber); - const stack_end: [*]align(16) usize = @ptrCast(@alignCast(closure)); - (stack_end - 1)[0..1].* = .{@intFromPtr(&AsyncClosure.call)}; fiber.* = .{ .required_align = {}, .context = switch (builtin.cpu.arch) { .x86_64 => .{ - .rsp = @intFromPtr(stack_end - 1), + .rsp = @intFromPtr(closure) - @sizeOf(usize), .rbp = 0, .rip = @intFromPtr(&fiberEntry), }, .aarch64 => .{ - .sp = @intFromPtr(stack_end), + .sp = @intFromPtr(closure) - @sizeOf(usize) - 1, .fp = 0, .pc = @intFromPtr(&fiberEntry), }, @@ -968,70 +938,6 @@ const DetachedClosure = struct { } }; -fn asyncDetached( - userdata: ?*anyopaque, - context: []const u8, - context_alignment: std.mem.Alignment, - start: *const fn (context: *const anyopaque) void, -) void { - assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO - assert(context.len <= Fiber.max_context_size); // TODO - - const event_loop: *EventLoop = @ptrCast(@alignCast(userdata)); - const fiber = Fiber.allocate(event_loop) catch { - start(context.ptr); - return; - }; - std.log.debug("allocated {*}", .{fiber}); - - const current_thread: *Thread = .current(); - const closure: *DetachedClosure = @ptrFromInt(Fiber.max_context_align.max(.of(DetachedClosure)).backward( - @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, - ) - @sizeOf(DetachedClosure)); - const stack_end: [*]align(16) usize = @ptrCast(@alignCast(closure)); - (stack_end - 1)[0..1].* = .{@intFromPtr(&DetachedClosure.call)}; - fiber.* = .{ - .required_align = {}, - .context = switch (builtin.cpu.arch) { - .x86_64 => .{ - .rsp = @intFromPtr(stack_end - 1), - .rbp = 0, - .rip = @intFromPtr(&fiberEntry), - }, - .aarch64 => .{ - .sp = @intFromPtr(stack_end), - .fp = 0, - .pc = @intFromPtr(&fiberEntry), - }, - else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), - }, - .awaiter = null, - .queue_next = null, - .cancel_thread = null, - .awaiting_completions = .initEmpty(), - }; - closure.* = .{ - .event_loop = event_loop, - .fiber = fiber, - .start = start, - .detached_queue_node = .{}, - }; - { - event_loop.detached.mutex.lock(event_loop.io()) catch |err| switch (err) { - error.Canceled => { - event_loop.recycle(fiber); - start(context.ptr); - return; - }, - }; - defer event_loop.detached.mutex.unlock(event_loop.io()); - event_loop.detached.list.append(&closure.detached_queue_node); - } - @memcpy(closure.contextPointer(), context); - - event_loop.schedule(current_thread, .{ .head = fiber, .tail = fiber }); -} - fn await( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, From dd945bf1f8963452f5acf448dd26c73d2d7b29f6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 03:21:32 -0700 Subject: [PATCH 178/244] one kqueue per thread --- lib/std/Io/IoUring.zig | 34 +- lib/std/Io/Kqueue.zig | 827 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 817 insertions(+), 44 deletions(-) diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig index 6d31f337c5..9ec1dafb31 100644 --- a/lib/std/Io/IoUring.zig +++ b/lib/std/Io/IoUring.zig @@ -67,8 +67,8 @@ const Fiber = struct { const min_stack_size = 4 * 1024 * 1024; const max_context_align: Alignment = .@"16"; const max_context_size = max_context_align.forward(1024); - const max_closure_size: usize = @max(@sizeOf(AsyncClosure), @sizeOf(DetachedClosure)); - const max_closure_align: Alignment = .max(.of(AsyncClosure), .of(DetachedClosure)); + const max_closure_size: usize = @sizeOf(AsyncClosure); + const max_closure_align: Alignment = .of(AsyncClosure); const allocation_size = std.mem.alignForward( usize, max_closure_align.max(max_context_align).forward( @@ -886,7 +886,7 @@ fn concurrent( .rip = @intFromPtr(&fiberEntry), }, .aarch64 => .{ - .sp = @intFromPtr(closure) - @sizeOf(usize) - 1, + .sp = @intFromPtr(closure), .fp = 0, .pc = @intFromPtr(&fiberEntry), }, @@ -910,34 +910,6 @@ fn concurrent( return @ptrCast(fiber); } -const DetachedClosure = struct { - event_loop: *EventLoop, - fiber: *Fiber, - start: *const fn (context: *const anyopaque) void, - detached_queue_node: std.DoublyLinkedList.Node, - - fn contextPointer(closure: *DetachedClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { - return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(DetachedClosure)); - } - - fn call(closure: *DetachedClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(DetachedClosure))) noreturn { - message.handle(closure.event_loop); - std.log.debug("{*} performing async detached", .{closure.fiber}); - closure.start(closure.contextPointer()); - const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); - closure.event_loop.yield(awaiter, pending_task: { - closure.event_loop.detached.mutex.lock(closure.event_loop.io()) catch |err| switch (err) { - error.Canceled => break :pending_task .nothing, - }; - defer closure.event_loop.detached.mutex.unlock(closure.event_loop.io()); - if (closure.detached_queue_node.next == &closure.detached_queue_node) break :pending_task .nothing; - closure.event_loop.detached.list.remove(&closure.detached_queue_node); - break :pending_task .recycle; - }); - unreachable; // switched to dead fiber - } -}; - fn await( userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index eef959155a..45274aba93 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -9,23 +9,795 @@ const net = std.Io.net; const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Alignment = std.mem.Alignment; -const posix = std.posix; const IpAddress = std.Io.net.IpAddress; const errnoBug = std.Io.Threaded.errnoBug; +const posix = std.posix; /// Must be a thread-safe allocator. gpa: Allocator, +mutex: std.Thread.Mutex, +main_fiber_buffer: [@sizeOf(Fiber) + Fiber.max_result_size]u8 align(@alignOf(Fiber)), +threads: Thread.List, -pub fn init(gpa: Allocator) Kqueue { - return .{ - .gpa = gpa, +/// Empirically saw >128KB being used by the self-hosted backend to panic. +const idle_stack_size = 256 * 1024; + +const max_idle_search = 4; +const max_steal_ready_search = 4; + +const changes_buffer_len = 64; + +const Thread = struct { + thread: std.Thread, + idle_context: Context, + current_context: *Context, + ready_queue: ?*Fiber, + kq_fd: posix.fd_t, + idle_search_index: u32, + steal_ready_search_index: u32, + + const canceling: ?*Thread = @ptrFromInt(@alignOf(Thread)); + + threadlocal var self: *Thread = undefined; + + fn current() *Thread { + return self; + } + + fn currentFiber(thread: *Thread) *Fiber { + return @fieldParentPtr("context", thread.current_context); + } + + const List = struct { + allocated: []Thread, + reserved: u32, + active: u32, }; +}; + +const Fiber = struct { + required_align: void align(4), + context: Context, + awaiter: ?*Fiber, + queue_next: ?*Fiber, + cancel_thread: ?*Thread, + awaiting_completions: std.StaticBitSet(3), + + const finished: ?*Fiber = @ptrFromInt(@alignOf(Thread)); + + 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 max_closure_size: usize = @sizeOf(AsyncClosure); + const max_closure_align: Alignment = .of(AsyncClosure); + const allocation_size = std.mem.alignForward( + usize, + max_closure_align.max(max_context_align).forward( + max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size, + ) + max_closure_size + max_context_size, + std.heap.page_size_max, + ); + + fn allocate(k: *Kqueue) error{OutOfMemory}!*Fiber { + return @ptrCast(try k.gpa.alignedAlloc(u8, .of(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, comptime Result: type) *Result { + return @ptrCast(@alignCast(f.resultBytes(.of(Result)))); + } + + fn resultBytes(f: *Fiber, alignment: Alignment) [*]u8 { + return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); + } + + fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{Canceled}!void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + null, + thread, + .acq_rel, + .acquire, + )) |cancel_thread| { + assert(cancel_thread == Thread.canceling); + return error.Canceled; + } + } + + fn exitCancelRegion(fiber: *Fiber, thread: *Thread) void { + if (@cmpxchgStrong( + ?*Thread, + &fiber.cancel_thread, + thread, + null, + .acq_rel, + .acquire, + )) |cancel_thread| assert(cancel_thread == Thread.canceling); + } + + const Queue = struct { head: *Fiber, tail: *Fiber }; +}; + +fn recycle(k: *Kqueue, fiber: *Fiber) void { + std.log.debug("recyling {*}", .{fiber}); + assert(fiber.queue_next == null); + k.gpa.free(fiber.allocatedSlice()); +} + +pub fn init(k: *Kqueue, 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, .of(Thread), idle_stack_end_offset); + errdefer gpa.free(allocated_slice); + k.* = .{ + .gpa = gpa, + .mutex = .{}, + .main_fiber_buffer = undefined, + .threads = .{ + .allocated = @ptrCast(allocated_slice[0..threads_size]), + .reserved = 1, + .active = 1, + }, + }; + const main_fiber: *Fiber = @ptrCast(&k.main_fiber_buffer); + main_fiber.* = .{ + .required_align = {}, + .context = undefined, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + const main_thread = &k.threads.allocated[0]; + Thread.self = main_thread; + const idle_stack_end: [*]align(16) usize = @ptrCast(@alignCast(allocated_slice[idle_stack_end_offset..].ptr)); + (idle_stack_end - 1)[0..1].* = .{@intFromPtr(k)}; + main_thread.* = .{ + .thread = undefined, + .idle_context = switch (builtin.cpu.arch) { + .aarch64 => .{ + .sp = @intFromPtr(idle_stack_end), + .fp = 0, + .pc = @intFromPtr(&mainIdleEntry), + .x18 = asm ("" + : [x18] "={x18}" (-> u64), + ), + }, + .x86_64 => .{ + .rsp = @intFromPtr(idle_stack_end - 1), + .rbp = 0, + .rip = @intFromPtr(&mainIdleEntry), + }, + else => @compileError("unimplemented architecture"), + }, + .current_context = &main_fiber.context, + .ready_queue = null, + .kq_fd = try posix.kqueue(), + .idle_search_index = 1, + .steal_ready_search_index = 1, + }; + errdefer std.posix.close(main_thread.kq_fd); + std.log.debug("created main idle {*}", .{&main_thread.idle_context}); + std.log.debug("created main {*}", .{main_fiber}); } pub fn deinit(k: *Kqueue) void { + const active_threads = @atomicLoad(u32, &k.threads.active, .acquire); + for (k.threads.allocated[0..active_threads]) |*thread| { + const ready_fiber = @atomicLoad(?*Fiber, &thread.ready_queue, .monotonic); + assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async + } + k.yield(null, .exit); + const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @ptrCast(@alignCast(k.threads.allocated.ptr)); + const idle_stack_end_offset = std.mem.alignForward(usize, k.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); + for (k.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); + k.gpa.free(allocated_ptr[0..idle_stack_end_offset]); k.* = undefined; } +fn findReadyFiber(k: *Kqueue, thread: *Thread) ?*Fiber { + if (@atomicRmw(?*Fiber, &thread.ready_queue, .Xchg, Fiber.finished, .acquire)) |ready_fiber| { + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + const active_threads = @atomicLoad(u32, &k.threads.active, .acquire); + for (0..@min(max_steal_ready_search, active_threads)) |_| { + defer thread.steal_ready_search_index += 1; + if (thread.steal_ready_search_index == active_threads) thread.steal_ready_search_index = 0; + const steal_ready_search_thread = &k.threads.allocated[0..active_threads][thread.steal_ready_search_index]; + if (steal_ready_search_thread == thread) continue; + const ready_fiber = @atomicLoad(?*Fiber, &steal_ready_search_thread.ready_queue, .acquire) orelse continue; + if (ready_fiber == Fiber.finished) continue; + if (@cmpxchgWeak( + ?*Fiber, + &steal_ready_search_thread.ready_queue, + ready_fiber, + null, + .acquire, + .monotonic, + )) |_| continue; + @atomicStore(?*Fiber, &thread.ready_queue, ready_fiber.queue_next, .release); + ready_fiber.queue_next = null; + return ready_fiber; + } + // couldn't find anything to do, so we are now open for business + @atomicStore(?*Fiber, &thread.ready_queue, null, .monotonic); + return null; +} + +fn yield(k: *Kqueue, maybe_ready_fiber: ?*Fiber, pending_task: SwitchMessage.PendingTask) void { + const thread: *Thread = .current(); + const ready_context = if (maybe_ready_fiber orelse k.findReadyFiber(thread)) |ready_fiber| + &ready_fiber.context + else + &thread.idle_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(k); +} + +fn schedule(k: *Kqueue, thread: *Thread, ready_queue: Fiber.Queue) void { + { + var fiber = ready_queue.head; + while (true) { + std.log.debug("scheduling {*}", .{fiber}); + fiber = fiber.queue_next orelse break; + } + assert(fiber == ready_queue.tail); + } + // shared fields of previous `Thread` must be initialized before later ones are marked as active + const new_thread_index = @atomicLoad(u32, &k.threads.active, .acquire); + for (0..@min(max_idle_search, new_thread_index)) |_| { + defer thread.idle_search_index += 1; + if (thread.idle_search_index == new_thread_index) thread.idle_search_index = 0; + const idle_search_thread = &k.threads.allocated[0..new_thread_index][thread.idle_search_index]; + if (idle_search_thread == thread) continue; + if (@cmpxchgWeak( + ?*Fiber, + &idle_search_thread.ready_queue, + null, + ready_queue.head, + .release, + .monotonic, + )) |_| continue; + const changes = [_]posix.Kevent{ + .{ + .ident = 0, + .filter = std.c.EVFILT.USER, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = std.c.NOTE.TRIGGER, + .data = 0, + .udata = @intFromEnum(Completion.UserData.wakeup), + }, + }; + // If an error occurs it only pessimises scheduling. + _ = posix.kevent(idle_search_thread.kq_fd, &changes, &.{}, null) catch {}; + return; + } + spawn_thread: { + // previous failed reservations must have completed before retrying + if (new_thread_index == k.threads.allocated.len or @cmpxchgWeak( + u32, + &k.threads.reserved, + new_thread_index, + new_thread_index + 1, + .acquire, + .monotonic, + ) != null) break :spawn_thread; + const new_thread = &k.threads.allocated[new_thread_index]; + const next_thread_index = new_thread_index + 1; + new_thread.* = .{ + .thread = undefined, + .idle_context = undefined, + .current_context = &new_thread.idle_context, + .ready_queue = ready_queue.head, + .kq_fd = posix.kqueue() catch |err| { + @atomicStore(u32, &k.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due to kqueue init failure: {t}", .{err}); + break :spawn_thread; + }, + .idle_search_index = 0, + .steal_ready_search_index = 0, + }; + new_thread.thread = std.Thread.spawn(.{ + .stack_size = idle_stack_size, + .allocator = k.gpa, + }, threadEntry, .{ k, new_thread_index }) catch |err| { + posix.close(new_thread.kq_fd); + @atomicStore(u32, &k.threads.reserved, new_thread_index, .release); + // no more access to `thread` after giving up reservation + std.log.warn("unable to create worker thread due spawn failure: {s}", .{@errorName(err)}); + break :spawn_thread; + }; + // shared fields of `Thread` must be initialized before being marked active + @atomicStore(u32, &k.threads.active, next_thread_index, .release); + return; + } + // nobody wanted it, so just queue it on ourselves + while (@cmpxchgWeak( + ?*Fiber, + &thread.ready_queue, + ready_queue.tail.queue_next, + ready_queue.head, + .acq_rel, + .acquire, + )) |old_head| ready_queue.tail.queue_next = old_head; +} + +fn mainIdle(k: *Kqueue, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { + message.handle(k); + k.idle(&k.threads.allocated[0]); + k.yield(@ptrCast(&k.main_fiber_buffer), .nothing); + unreachable; // switched to dead fiber +} + +fn threadEntry(k: *Kqueue, index: u32) void { + const thread: *Thread = &k.threads.allocated[index]; + Thread.self = thread; + std.log.debug("created thread idle {*}", .{&thread.idle_context}); + k.idle(thread); +} + +const Completion = struct { + const UserData = enum(usize) { + unused, + wakeup, + cleanup, + exit, + /// *Fiber + _, + }; + /// Corresponds to Kevent field. + flags: u16, + /// Corresponds to Kevent field. + fflags: u32, + /// Corresponds to Kevent field. + data: isize, +}; + +fn idle(k: *Kqueue, thread: *Thread) void { + var events_buffer: [changes_buffer_len]posix.Kevent = undefined; + var maybe_ready_fiber: ?*Fiber = null; + while (true) { + while (maybe_ready_fiber orelse k.findReadyFiber(thread)) |ready_fiber| { + k.yield(ready_fiber, .nothing); + maybe_ready_fiber = null; + } + const n = posix.kevent(thread.kq_fd, &.{}, &events_buffer, null) catch |err| { + // TODO handle EINTR for cancellation purposes + @panic(@errorName(err)); + }; + var maybe_ready_queue: ?Fiber.Queue = null; + for (events_buffer[0..n]) |event| switch (@as(Completion.UserData, @enumFromInt(event.udata))) { + .unused => unreachable, // bad submission queued? + .wakeup => {}, + .cleanup => @panic("failed to notify other threads that we are exiting"), + .exit => { + assert(maybe_ready_fiber == null and maybe_ready_queue == null); // pending async + return; + }, + _ => { + const fiber: *Fiber = @ptrFromInt(event.udata); + assert(fiber.queue_next == null); + fiber.resultPointer(Completion).* = .{ + .flags = event.flags, + .fflags = event.fflags, + .data = event.data, + }; + if (maybe_ready_fiber == null) maybe_ready_fiber = fiber else if (maybe_ready_queue) |*ready_queue| { + ready_queue.tail.queue_next = fiber; + ready_queue.tail = fiber; + } else maybe_ready_queue = .{ .head = fiber, .tail = fiber }; + }, + }; + if (maybe_ready_queue) |ready_queue| k.schedule(thread, ready_queue); + } +} + +const SwitchMessage = struct { + contexts: extern struct { + prev: *Context, + ready: *Context, + }, + pending_task: PendingTask, + + const PendingTask = union(enum) { + nothing, + reschedule, + recycle: *Fiber, + register_awaiter: *?*Fiber, + register_select: []const *Io.AnyFuture, + mutex_lock: struct { + prev_state: Io.Mutex.State, + mutex: *Io.Mutex, + }, + condition_wait: struct { + cond: *Io.Condition, + mutex: *Io.Mutex, + }, + exit, + }; + + fn handle(message: *const SwitchMessage, k: *Kqueue) void { + const thread: *Thread = .current(); + thread.current_context = message.contexts.ready; + switch (message.pending_task) { + .nothing => {}, + .reschedule => if (message.contexts.prev != &thread.idle_context) { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .recycle => |fiber| { + k.recycle(fiber); + }, + .register_awaiter => |awaiter| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + }, + .register_select => |futures| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + for (futures) |any_future| { + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicRmw(?*Fiber, &future_fiber.awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) { + const closure: *AsyncClosure = .fromFiber(future_fiber); + if (!@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .seq_cst)) { + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + } + } + } + }, + .mutex_lock => |mutex_lock| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + var prev_state = mutex_lock.prev_state; + while (switch (prev_state) { + else => next_state: { + prev_fiber.queue_next = @ptrFromInt(@intFromEnum(prev_state)); + break :next_state @cmpxchgWeak( + Io.Mutex.State, + &mutex_lock.mutex.state, + prev_state, + @enumFromInt(@intFromPtr(prev_fiber)), + .release, + .acquire, + ); + }, + .unlocked => @cmpxchgWeak( + Io.Mutex.State, + &mutex_lock.mutex.state, + .unlocked, + .locked_once, + .acquire, + .acquire, + ) orelse { + prev_fiber.queue_next = null; + k.schedule(thread, .{ .head = prev_fiber, .tail = prev_fiber }); + return; + }, + }) |next_state| prev_state = next_state; + }, + .condition_wait => |condition_wait| { + const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.contexts.prev)); + assert(prev_fiber.queue_next == null); + const cond_impl = prev_fiber.resultPointer(Condition); + cond_impl.* = .{ + .tail = prev_fiber, + .event = .queued, + }; + if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&condition_wait.cond.state)), + null, + prev_fiber, + .release, + .acquire, + )) |waiting_fiber| { + const waiting_cond_impl = waiting_fiber.?.resultPointer(Condition); + assert(waiting_cond_impl.tail.queue_next == null); + waiting_cond_impl.tail.queue_next = prev_fiber; + waiting_cond_impl.tail = prev_fiber; + } + condition_wait.mutex.unlock(k.io()); + }, + .exit => for (k.threads.allocated[0..@atomicLoad(u32, &k.threads.active, .acquire)]) |*each_thread| { + const changes = [_]posix.Kevent{ + .{ + .ident = 0, + .filter = std.c.EVFILT.USER, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = std.c.NOTE.TRIGGER, + .data = 0, + .udata = @intFromEnum(Completion.UserData.exit), + }, + }; + _ = posix.kevent(each_thread.kq_fd, &changes, &.{}, null) catch |err| { + @panic(@errorName(err)); + }; + }, + } + } +}; + +const Context = switch (builtin.cpu.arch) { + .aarch64 => extern struct { + sp: u64, + fp: u64, + pc: u64, + x18: u64, + }, + .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) { + .aarch64 => asm volatile ( + \\ ldp x0, x2, [x1] + \\ ldp x3, x18, [x2, #16] + \\ mov x4, sp + \\ stp x4, fp, [x0] + \\ adr x5, 0f + \\ ldp x4, fp, [x2] + \\ stp x5, x18, [x0, #16] + \\ mov sp, x4 + \\ br x3 + \\0: + : [received_message] "={x1}" (-> *const @FieldType(SwitchMessage, "contexts")), + : [message_to_send] "{x1}" (&message.contexts), + : .{ + .x0 = true, + .x1 = true, + .x2 = true, + .x3 = true, + .x4 = true, + .x5 = true, + .x6 = true, + .x7 = true, + .x8 = true, + .x9 = true, + .x10 = true, + .x11 = true, + .x12 = true, + .x13 = true, + .x14 = true, + .x15 = true, + .x16 = true, + .x17 = true, + .x19 = true, + .x20 = true, + .x21 = true, + .x22 = true, + .x23 = true, + .x24 = true, + .x25 = true, + .x26 = true, + .x27 = true, + .x28 = true, + .x30 = true, + .z0 = true, + .z1 = true, + .z2 = true, + .z3 = true, + .z4 = true, + .z5 = true, + .z6 = true, + .z7 = true, + .z8 = true, + .z9 = true, + .z10 = true, + .z11 = true, + .z12 = true, + .z13 = true, + .z14 = true, + .z15 = true, + .z16 = true, + .z17 = true, + .z18 = true, + .z19 = true, + .z20 = true, + .z21 = true, + .z22 = true, + .z23 = true, + .z24 = true, + .z25 = true, + .z26 = true, + .z27 = true, + .z28 = true, + .z29 = true, + .z30 = true, + .z31 = true, + .p0 = true, + .p1 = true, + .p2 = true, + .p3 = true, + .p4 = true, + .p5 = true, + .p6 = true, + .p7 = true, + .p8 = true, + .p9 = true, + .p10 = true, + .p11 = true, + .p12 = true, + .p13 = true, + .p14 = true, + .p15 = true, + .fpcr = true, + .fpsr = true, + .ffr = true, + .memory = true, + }), + .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 = true, + .rcx = true, + .rdx = true, + .rbx = true, + .rsi = true, + .rdi = true, + .r8 = true, + .r9 = true, + .r10 = true, + .r11 = true, + .r12 = true, + .r13 = true, + .r14 = true, + .r15 = true, + .mm0 = true, + .mm1 = true, + .mm2 = true, + .mm3 = true, + .mm4 = true, + .mm5 = true, + .mm6 = true, + .mm7 = true, + .zmm0 = true, + .zmm1 = true, + .zmm2 = true, + .zmm3 = true, + .zmm4 = true, + .zmm5 = true, + .zmm6 = true, + .zmm7 = true, + .zmm8 = true, + .zmm9 = true, + .zmm10 = true, + .zmm11 = true, + .zmm12 = true, + .zmm13 = true, + .zmm14 = true, + .zmm15 = true, + .zmm16 = true, + .zmm17 = true, + .zmm18 = true, + .zmm19 = true, + .zmm20 = true, + .zmm21 = true, + .zmm22 = true, + .zmm23 = true, + .zmm24 = true, + .zmm25 = true, + .zmm26 = true, + .zmm27 = true, + .zmm28 = true, + .zmm29 = true, + .zmm30 = true, + .zmm31 = true, + .fpsr = true, + .fpcr = true, + .mxcsr = true, + .rflags = true, + .dirflag = true, + .memory = true, + }), + 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), + ), + .aarch64 => asm volatile ( + \\ ldr x0, [sp, #-8] + \\ b %[mainIdle] + : + : [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)), + } +} + +const AsyncClosure = struct { + event_loop: *Kqueue, + fiber: *Fiber, + start: *const fn (context: *const anyopaque, result: *anyopaque) void, + result_align: Alignment, + already_awaited: bool, + + 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); + const fiber = closure.fiber; + std.log.debug("{*} performing async", .{fiber}); + closure.start(closure.contextPointer(), fiber.resultBytes(closure.result_align)); + const awaiter = @atomicRmw(?*Fiber, &fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); + const ready_awaiter = r: { + const a = awaiter orelse break :r null; + if (@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .acq_rel)) break :r null; + break :r a; + }; + closure.event_loop.yield(ready_awaiter, .nothing); + unreachable; // switched to dead fiber + } + + fn fromFiber(fiber: *Fiber) *AsyncClosure { + return @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( + @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, + ) - @sizeOf(AsyncClosure)); + } +}; + pub fn io(k: *Kqueue) Io { return .{ .userdata = k, @@ -229,11 +1001,33 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = cond; - _ = mutex; - @panic("TODO"); + k.yield(null, .{ .condition_wait = .{ .cond = cond, .mutex = mutex } }); + const thread = Thread.current(); + const fiber = thread.currentFiber(); + const cond_impl = fiber.resultPointer(Condition); + try mutex.lock(k.io()); + switch (cond_impl.event) { + .queued => {}, + .wake => |wake| if (fiber.queue_next) |next_fiber| switch (wake) { + .one => if (@cmpxchgStrong( + ?*Fiber, + @as(*?*Fiber, @ptrCast(&cond.state)), + null, + next_fiber, + .release, + .acquire, + )) |old_fiber| { + const old_cond_impl = old_fiber.?.resultPointer(Condition); + assert(old_cond_impl.tail.queue_next == null); + old_cond_impl.tail.queue_next = next_fiber; + old_cond_impl.tail = cond_impl.tail; + }, + .all => k.schedule(thread, .{ .head = next_fiber, .tail = cond_impl.tail }), + }, + } + fiber.queue_next = null; } + fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; @@ -243,10 +1037,9 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: } fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition.Wake) void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = cond; - _ = wake; - @panic("TODO"); + const waiting_fiber = @atomicRmw(?*Fiber, @as(*?*Fiber, @ptrCast(&cond.state)), .Xchg, null, .acquire) orelse return; + waiting_fiber.resultPointer(Condition).event = .{ .wake = wake }; + k.yield(waiting_fiber, .reschedule); } fn dirMake(userdata: ?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void { @@ -426,7 +1219,7 @@ fn netBindIp( const k: *Kqueue = @ptrCast(@alignCast(userdata)); const family = Io.Threaded.posixAddressFamily(address); const socket_fd = try openSocketPosix(k, family, options); - errdefer posix.close(socket_fd); + errdefer std.posix.close(socket_fd); var storage: Io.Threaded.PosixAddress = undefined; var addr_len = Io.Threaded.addressToPosix(address, &storage); try posixBind(k, socket_fd, &storage.any, addr_len); @@ -704,3 +1497,11 @@ fn setSocketOption(k: *Kqueue, fd: posix.fd_t, level: i32, opt_name: u32, option fn checkCancel(k: *Kqueue) error{Canceled}!void { if (cancelRequested(k)) return error.Canceled; } + +const Condition = struct { + tail: *Fiber, + event: union(enum) { + queued, + wake: Io.Condition.Wake, + }, +}; From 92b8378814697880ac3b5942abb47db4e5eeb958 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 03:38:34 -0700 Subject: [PATCH 179/244] concurrent and await --- lib/std/Io/Kqueue.zig | 81 +++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 45274aba93..181ecd4ebf 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -172,9 +172,6 @@ pub fn init(k: *Kqueue, gpa: Allocator) !void { .sp = @intFromPtr(idle_stack_end), .fp = 0, .pc = @intFromPtr(&mainIdleEntry), - .x18 = asm ("" - : [x18] "={x18}" (-> u64), - ), }, .x86_64 => .{ .rsp = @intFromPtr(idle_stack_end - 1), @@ -548,7 +545,6 @@ const Context = switch (builtin.cpu.arch) { sp: u64, fp: u64, pc: u64, - x18: u64, }, .x86_64 => extern struct { rsp: u64, @@ -562,12 +558,12 @@ inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { return @fieldParentPtr("contexts", switch (builtin.cpu.arch) { .aarch64 => asm volatile ( \\ ldp x0, x2, [x1] - \\ ldp x3, x18, [x2, #16] + \\ ldr x3, [x2, #16] \\ mov x4, sp \\ stp x4, fp, [x0] \\ adr x5, 0f \\ ldp x4, fp, [x2] - \\ stp x5, x18, [x0, #16] + \\ str x5, [x0, #16] \\ mov sp, x4 \\ br x3 \\0: @@ -761,12 +757,18 @@ fn fiberEntry() callconv(.naked) void { : : [AsyncClosure_call] "X" (&AsyncClosure.call), ), + .aarch64 => asm volatile ( + \\ mov x0, sp + \\ b %[AsyncClosure_call] + : + : [AsyncClosure_call] "X" (&AsyncClosure.call), + ), else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), } } const AsyncClosure = struct { - event_loop: *Kqueue, + kqueue: *Kqueue, fiber: *Fiber, start: *const fn (context: *const anyopaque, result: *anyopaque) void, result_align: Alignment, @@ -777,7 +779,7 @@ const AsyncClosure = struct { } fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(AsyncClosure))) noreturn { - message.handle(closure.event_loop); + message.handle(closure.kqueue); const fiber = closure.fiber; std.log.debug("{*} performing async", .{fiber}); closure.start(closure.contextPointer(), fiber.resultBytes(closure.result_align)); @@ -787,7 +789,7 @@ const AsyncClosure = struct { if (@atomicRmw(bool, &closure.already_awaited, .Xchg, true, .acq_rel)) break :r null; break :r a; }; - closure.event_loop.yield(ready_awaiter, .nothing); + closure.kqueue.yield(ready_awaiter, .nothing); unreachable; // switched to dead fiber } @@ -881,19 +883,52 @@ fn async( fn concurrent( userdata: ?*anyopaque, result_len: usize, - result_alignment: std.mem.Alignment, + result_alignment: Alignment, context: []const u8, - context_alignment: std.mem.Alignment, + context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, ) error{OutOfMemory}!*Io.AnyFuture { const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = result_len; - _ = result_alignment; - _ = context; - _ = context_alignment; - _ = start; - @panic("TODO"); + 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 fiber = try Fiber.allocate(k); + std.log.debug("allocated {*}", .{fiber}); + + const closure: *AsyncClosure = .fromFiber(fiber); + fiber.* = .{ + .required_align = {}, + .context = switch (builtin.cpu.arch) { + .x86_64 => .{ + .rsp = @intFromPtr(closure) - @sizeOf(usize), + .rbp = 0, + .rip = @intFromPtr(&fiberEntry), + }, + .aarch64 => .{ + .sp = @intFromPtr(closure), + .fp = 0, + .pc = @intFromPtr(&fiberEntry), + }, + else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), + }, + .awaiter = null, + .queue_next = null, + .cancel_thread = null, + .awaiting_completions = .initEmpty(), + }; + closure.* = .{ + .kqueue = k, + .fiber = fiber, + .start = start, + .result_align = result_alignment, + .already_awaited = false, + }; + @memcpy(closure.contextPointer(), context); + + k.schedule(.current(), .{ .head = fiber, .tail = fiber }); + return @ptrCast(fiber); } fn await( @@ -903,11 +938,11 @@ fn await( result_alignment: std.mem.Alignment, ) void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = any_future; - _ = result; - _ = result_alignment; - @panic("TODO"); + const future_fiber: *Fiber = @ptrCast(@alignCast(any_future)); + if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) + k.yield(null, .{ .register_awaiter = &future_fiber.awaiter }); + @memcpy(result, future_fiber.resultBytes(result_alignment)); + k.recycle(future_fiber); } fn cancel( From 9d6750f01ccef0d4dfb6fc69a2011d83a18fd325 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 03:44:45 -0700 Subject: [PATCH 180/244] std.Io.Kqueue: implement netSend --- lib/std/Io/Kqueue.zig | 63 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 181ecd4ebf..468c3b1347 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -1308,11 +1308,64 @@ fn netSend( @as(u32, if (@hasDecl(posix.MSG, "FASTOPEN") and flags.fastopen) posix.MSG.FASTOPEN else 0) | posix.MSG.NOSIGNAL; - _ = k; - _ = posix_flags; - _ = handle; - _ = outgoing_messages; - @panic("TODO"); + for (outgoing_messages, 0..) |*msg, i| { + netSendOne(k, handle, msg, posix_flags) catch |err| return .{ err, i }; + } + + return .{ null, outgoing_messages.len }; +} + +fn netSendOne( + k: *Kqueue, + handle: net.Socket.Handle, + message: *net.OutgoingMessage, + flags: u32, +) net.Socket.SendError!void { + var addr: Io.Threaded.PosixAddress = undefined; + var iovec: posix.iovec_const = .{ .base = @constCast(message.data_ptr), .len = message.data_len }; + const msg: posix.msghdr_const = .{ + .name = &addr.any, + .namelen = Io.Threaded.addressToPosix(message.address, &addr), + .iov = (&iovec)[0..1], + .iovlen = 1, + // OS returns EINVAL if this pointer is invalid even if controllen is zero. + .control = if (message.control.len == 0) null else @constCast(message.control.ptr), + .controllen = @intCast(message.control.len), + .flags = 0, + }; + while (true) { + try k.checkCancel(); + const rc = posix.system.sendmsg(handle, &msg, flags); + switch (posix.errno(rc)) { + .SUCCESS => { + message.data_len = @intCast(rc); + return; + }, + .INTR => continue, + .CANCELED => return error.Canceled, + + .ACCES => return error.AccessDenied, + .ALREADY => return error.FastOpenAlreadyInProgress, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNRESET => return error.ConnectionResetByPeer, + .DESTADDRREQ => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .MSGSIZE => return error.MessageOversize, + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTSOCK => |err| return errnoBug(err), + .OPNOTSUPP => |err| return errnoBug(err), + .PIPE => return error.SocketUnconnected, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTCONN => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } } fn netReceive( From f17c6bba579e2c8929bf37a5980b37ebe9ccc517 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 04:14:43 -0700 Subject: [PATCH 181/244] std.Io.Kqueue: implement netConnect --- lib/std/Io/Kqueue.zig | 54 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 468c3b1347..f8fb9f5edd 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -1265,12 +1265,57 @@ fn netBindIp( }; } fn netConnectIp(userdata: ?*anyopaque, address: *const net.IpAddress, options: net.IpAddress.ConnectOptions) net.IpAddress.ConnectError!net.Stream { + if (options.timeout != .none) @panic("TODO"); const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = address; - _ = options; - @panic("TODO"); + const family = Io.Threaded.posixAddressFamily(address); + const socket_fd = try openSocketPosix(k, family, .{ + .mode = options.mode, + .protocol = options.protocol, + }); + errdefer posix.close(socket_fd); + var storage: Io.Threaded.PosixAddress = undefined; + var addr_len = Io.Threaded.addressToPosix(address, &storage); + try posixConnect(k, socket_fd, &storage.any, addr_len); + try posixGetSockName(k, socket_fd, &storage.any, &addr_len); + return .{ .socket = .{ + .handle = socket_fd, + .address = Io.Threaded.addressFromPosix(&storage), + } }; } + +fn posixConnect(k: *Kqueue, socket_fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { + while (true) { + try k.checkCancel(); + switch (posix.errno(posix.system.connect(socket_fd, addr, addr_len))) { + .SUCCESS => return, + .INTR => continue, + .CANCELED => return error.Canceled, + .AGAIN => @panic("TODO"), + .INPROGRESS => return, // Due to TCP fast open, we find out possible error later. + + .ADDRNOTAVAIL => return error.AddressUnavailable, + .AFNOSUPPORT => return error.AddressFamilyUnsupported, + .ALREADY => return error.ConnectionPending, + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .CONNREFUSED => return error.ConnectionRefused, + .CONNRESET => return error.ConnectionResetByPeer, + .FAULT => |err| return errnoBug(err), + .ISCONN => |err| return errnoBug(err), + .HOSTUNREACH => return error.HostUnreachable, + .NETUNREACH => return error.NetworkUnreachable, + .NOTSOCK => |err| return errnoBug(err), + .PROTOTYPE => |err| return errnoBug(err), + .TIMEDOUT => return error.Timeout, + .CONNABORTED => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, + .PERM => |err| return errnoBug(err), + .NOENT => |err| return errnoBug(err), + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } +} + fn netListenUnix( userdata: ?*anyopaque, unix_address: *const net.UnixAddress, @@ -1343,6 +1388,7 @@ fn netSendOne( }, .INTR => continue, .CANCELED => return error.Canceled, + .AGAIN => @panic("TODO register kevent"), .ACCES => return error.AccessDenied, .ALREADY => return error.FastOpenAlreadyInProgress, From 1b0dcd400717b3ced323a66c525371ed1b928cbf Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 04:27:10 -0700 Subject: [PATCH 182/244] std.Io.Threaded: fix setting of O_NONBLOCK flag --- lib/std/Io/Threaded.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c1365e0f44..8b880a176f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1651,7 +1651,7 @@ fn dirCreateFilePosix( else => |err| return posix.unexpectedErrno(err), } }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { try t.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { @@ -1897,7 +1897,7 @@ fn dirOpenFilePosix( else => |err| return posix.unexpectedErrno(err), } }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { try t.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { From 0497f88d397276413edce8371b89825215c802b0 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 04:27:20 -0700 Subject: [PATCH 183/244] std.Io.Kqueue: implement netRead --- lib/std/Io/Kqueue.zig | 47 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index f8fb9f5edd..fa5f6033ef 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -24,6 +24,7 @@ const idle_stack_size = 256 * 1024; const max_idle_search = 4; const max_steal_ready_search = 4; +const max_iovecs_len = 8; const changes_buffer_len = 64; @@ -1431,13 +1432,47 @@ fn netReceive( _ = timeout; @panic("TODO"); } -fn netRead(userdata: ?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + +fn netRead(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = src; - _ = data; - @panic("TODO"); + + var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; + var i: usize = 0; + for (data) |buf| { + if (iovecs_buffer.len - i == 0) break; + if (buf.len != 0) { + iovecs_buffer[i] = .{ .base = buf.ptr, .len = buf.len }; + i += 1; + } + } + const dest = iovecs_buffer[0..i]; + assert(dest[0].len > 0); + + while (true) { + try k.checkCancel(); + std.debug.print("calling readv\n", .{}); + const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CANCELED => return error.Canceled, + .AGAIN => @panic("TODO"), + + .INVAL => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .NOBUFS => return error.SystemResources, + .NOMEM => return error.SystemResources, + .NOTCONN => return error.SocketUnconnected, + .CONNRESET => return error.ConnectionResetByPeer, + .TIMEDOUT => return error.Timeout, + .PIPE => return error.SocketUnconnected, + .NETDOWN => return error.NetworkDown, + else => |err| return posix.unexpectedErrno(err), + } + } } + fn netWrite(userdata: ?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; @@ -1529,7 +1564,7 @@ fn openSocketPosix( else => |err| return posix.unexpectedErrno(err), } }; - fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + fl_flags |= @as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); while (true) { try k.checkCancel(); switch (posix.errno(posix.system.fcntl(fd, posix.F.SETFL, fl_flags))) { From cc11dd1f87d0e16e9af4d80b28dbe2f0d1a7e3b2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 04:46:00 -0700 Subject: [PATCH 184/244] std.Io.Kqueue: implement EAGAIN logic for netRead --- lib/std/Io/Kqueue.zig | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index fa5f6033ef..3507c29462 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -1456,7 +1456,25 @@ fn netRead(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Strea .SUCCESS => return @intCast(rc), .INTR => continue, .CANCELED => return error.Canceled, - .AGAIN => @panic("TODO"), + .AGAIN => { + const thread: *Thread = .current(); + const fiber = thread.currentFiber(); + const changes = [_]posix.Kevent{ + .{ + .ident = @as(u32, @bitCast(fd)), + .filter = std.c.EVFILT.READ, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = 0, + .data = 0, + .udata = @intFromPtr(fiber), + }, + }; + assert(0 == (posix.kevent(thread.kq_fd, &changes, &.{}, null) catch |err| { + @panic(@errorName(err)); // TODO + })); + yield(k, null, .nothing); + continue; + }, .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), From 6a64c9b7c8971486a818d8cb2ae44bb4dab4497f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 05:24:41 -0700 Subject: [PATCH 185/244] std.Io.Kqueue: add missing Thread deinit logic --- lib/std/Io/Kqueue.zig | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 3507c29462..be0723448b 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -36,6 +36,14 @@ const Thread = struct { kq_fd: posix.fd_t, idle_search_index: u32, steal_ready_search_index: u32, + /// For ensuring multiple fibers waiting on the same file descriptor and + /// filter use the same kevent. + wait_queues: std.AutoArrayHashMapUnmanaged(WaitQueueKey, *Fiber), + + const WaitQueueKey = struct { + ident: usize, + filter: i32, + }; const canceling: ?*Thread = @ptrFromInt(@alignOf(Thread)); @@ -54,6 +62,13 @@ const Thread = struct { reserved: u32, active: u32, }; + + fn deinit(thread: *Thread, gpa: Allocator) void { + posix.close(thread.kq_fd); + assert(thread.wait_queues.count() == 0); + thread.wait_queues.deinit(gpa); + thread.* = undefined; + } }; const Fiber = struct { @@ -138,8 +153,14 @@ fn recycle(k: *Kqueue, fiber: *Fiber) void { k.gpa.free(fiber.allocatedSlice()); } -pub fn init(k: *Kqueue, gpa: Allocator) !void { - const threads_size = @max(std.Thread.getCpuCount() catch 1, 1) * @sizeOf(Thread); +pub const InitOptions = struct { + n_threads: ?usize = null, +}; + +pub fn init(k: *Kqueue, gpa: Allocator, options: InitOptions) !void { + assert(options.n_threads != 0); + const n_threads = @max(1, options.n_threads orelse std.Thread.getCpuCount() catch 1); + const threads_size = n_threads * @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, .of(Thread), idle_stack_end_offset); errdefer gpa.free(allocated_slice); @@ -186,6 +207,7 @@ pub fn init(k: *Kqueue, gpa: Allocator) !void { .kq_fd = try posix.kqueue(), .idle_search_index = 1, .steal_ready_search_index = 1, + .wait_queues = .empty, }; errdefer std.posix.close(main_thread.kq_fd); std.log.debug("created main idle {*}", .{&main_thread.idle_context}); @@ -199,10 +221,13 @@ pub fn deinit(k: *Kqueue) void { assert(ready_fiber == null or ready_fiber == Fiber.finished); // pending async } k.yield(null, .exit); + const main_thread = &k.threads.allocated[0]; + const gpa = k.gpa; + main_thread.deinit(gpa); const allocated_ptr: [*]align(@alignOf(Thread)) u8 = @ptrCast(@alignCast(k.threads.allocated.ptr)); const idle_stack_end_offset = std.mem.alignForward(usize, k.threads.allocated.len * @sizeOf(Thread) + idle_stack_size, std.heap.page_size_max); for (k.threads.allocated[1..active_threads]) |*thread| thread.thread.join(); - k.gpa.free(allocated_ptr[0..idle_stack_end_offset]); + gpa.free(allocated_ptr[0..idle_stack_end_offset]); k.* = undefined; } @@ -317,6 +342,7 @@ fn schedule(k: *Kqueue, thread: *Thread, ready_queue: Fiber.Queue) void { }, .idle_search_index = 0, .steal_ready_search_index = 0, + .wait_queues = .empty, }; new_thread.thread = std.Thread.spawn(.{ .stack_size = idle_stack_size, @@ -355,6 +381,7 @@ fn threadEntry(k: *Kqueue, index: u32) void { Thread.self = thread; std.log.debug("created thread idle {*}", .{&thread.idle_context}); k.idle(thread); + thread.deinit(k.gpa); } const Completion = struct { From 5578c760a77bd43ce13c9352f68f7e44c5440c8f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 06:02:46 -0700 Subject: [PATCH 186/244] std.Io.Kqueue: implement wait queue per fd Solves the issue when one kevent() call would clobber another if they used the same file descriptor as an identifier. --- lib/std/Io/Kqueue.zig | 73 ++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index be0723448b..b41a0260e0 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -423,17 +423,35 @@ fn idle(k: *Kqueue, thread: *Thread) void { return; }, _ => { - const fiber: *Fiber = @ptrFromInt(event.udata); - assert(fiber.queue_next == null); - fiber.resultPointer(Completion).* = .{ + const event_head_fiber: *Fiber = @ptrFromInt(event.udata); + const event_tail_fiber = thread.wait_queues.fetchSwapRemove(.{ + .ident = event.ident, + .filter = event.filter, + }).?.value; + assert(event_tail_fiber.queue_next == null); + + // TODO reevaluate this logic + event_head_fiber.resultPointer(Completion).* = .{ .flags = event.flags, .fflags = event.fflags, .data = event.data, }; - if (maybe_ready_fiber == null) maybe_ready_fiber = fiber else if (maybe_ready_queue) |*ready_queue| { - ready_queue.tail.queue_next = fiber; - ready_queue.tail = fiber; - } else maybe_ready_queue = .{ .head = fiber, .tail = fiber }; + + queue_ready: { + const head: *Fiber = if (maybe_ready_fiber == null) f: { + maybe_ready_fiber = event_head_fiber; + const next = event_head_fiber.queue_next orelse break :queue_ready; + event_head_fiber.queue_next = null; + break :f next; + } else event_head_fiber; + + if (maybe_ready_queue) |*ready_queue| { + ready_queue.tail.queue_next = head; + ready_queue.tail = event_tail_fiber; + } else { + maybe_ready_queue = .{ .head = head, .tail = event_tail_fiber }; + } + } }, }; if (maybe_ready_queue) |ready_queue| k.schedule(thread, ready_queue); @@ -1477,7 +1495,6 @@ fn netRead(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Strea while (true) { try k.checkCancel(); - std.debug.print("calling readv\n", .{}); const rc = posix.system.readv(fd, dest.ptr, @intCast(dest.len)); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), @@ -1486,19 +1503,33 @@ fn netRead(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Strea .AGAIN => { const thread: *Thread = .current(); const fiber = thread.currentFiber(); - const changes = [_]posix.Kevent{ - .{ - .ident = @as(u32, @bitCast(fd)), - .filter = std.c.EVFILT.READ, - .flags = std.c.EV.ADD | std.c.EV.ONESHOT, - .fflags = 0, - .data = 0, - .udata = @intFromPtr(fiber), - }, - }; - assert(0 == (posix.kevent(thread.kq_fd, &changes, &.{}, null) catch |err| { - @panic(@errorName(err)); // TODO - })); + const ident: u32 = @bitCast(fd); + const filter = std.c.EVFILT.READ; + const gop = thread.wait_queues.getOrPut(k.gpa, .{ + .ident = ident, + .filter = filter, + }) catch return error.SystemResources; + if (gop.found_existing) { + const tail_fiber = gop.value_ptr.*; + assert(tail_fiber.queue_next == null); + tail_fiber.queue_next = fiber; + gop.value_ptr.* = fiber; + } else { + gop.value_ptr.* = fiber; + const changes = [_]posix.Kevent{ + .{ + .ident = ident, + .filter = filter, + .flags = std.c.EV.ADD | std.c.EV.ONESHOT, + .fflags = 0, + .data = 0, + .udata = @intFromPtr(fiber), + }, + }; + assert(0 == (posix.kevent(thread.kq_fd, &changes, &.{}, null) catch |err| { + @panic(@errorName(err)); // TODO + })); + } yield(k, null, .nothing); continue; }, From a1f177d6370f074b3aa74bcd733d4edbeb10c213 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 07:13:50 -0700 Subject: [PATCH 187/244] std.Io.Threaded: stub netListenUnix for Windows --- lib/std/Io/Threaded.zig | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 8b880a176f..180b491e2f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -247,7 +247,10 @@ pub fn io(t: *Threaded) Io { .windows => netListenIpWindows, else => netListenIpPosix, }, - .netListenUnix = netListenUnix, + .netListenUnix = switch (builtin.os.tag) { + .windows => netListenUnixWindows, + else => netListenUnixPosix, + }, .netAccept = switch (builtin.os.tag) { .windows => netAcceptWindows, else => netAcceptPosix, @@ -2873,7 +2876,7 @@ fn netListenIpWindows( }; } -fn netListenUnix( +fn netListenUnixPosix( userdata: ?*anyopaque, address: *const net.UnixAddress, options: net.UnixAddress.ListenOptions, @@ -2906,6 +2909,19 @@ fn netListenUnix( return socket_fd; } +fn netListenUnixWindows( + userdata: ?*anyopaque, + address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + _ = t; + _ = address; + _ = options; + @panic("TODO"); +} + fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { try t.checkCancel(); From 89bb58e5a3c663b139e55e943ce32331baa24c61 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 07:42:24 -0700 Subject: [PATCH 188/244] incr-check: windows source files depend on ws2_32 --- tools/incr-check.zig | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/incr-check.zig b/tools/incr-check.zig index 4c5a2d9978..22031e147a 100644 --- a/tools/incr-check.zig +++ b/tools/incr-check.zig @@ -100,15 +100,14 @@ pub fn main() !void { for (case.targets) |target| { const target_prog_node = node: { var name_buf: [std.Progress.Node.max_name_len]u8 = undefined; - const name = std.fmt.bufPrint(&name_buf, "{s}-{s}", .{ target.query, @tagName(target.backend) }) catch &name_buf; + const name = std.fmt.bufPrint(&name_buf, "{s}-{t}", .{ target.query, target.backend }) catch &name_buf; break :node prog_node.start(name, case.updates.len); }; defer target_prog_node.end(); if (debug_log_verbose) { - std.log.scoped(.status).info("target: '{s}-{s}'", .{ target.query, @tagName(target.backend) }); + std.log.scoped(.status).info("target: '{s}-{t}'", .{ target.query, target.backend }); } - var child_args: std.ArrayListUnmanaged([]const u8) = .empty; try child_args.appendSlice(arena, &.{ resolved_zig_exe, @@ -121,8 +120,10 @@ pub fn main() !void { ".local-cache", "--global-cache-dir", ".global-cache", - "--listen=-", }); + if (target.resolved.os.tag == .windows) try child_args.append(arena, "-lws2_32"); + try child_args.append(arena, "--listen=-"); + if (opt_resolved_lib_dir) |resolved_lib_dir| { try child_args.appendSlice(arena, &.{ "--zig-lib-dir", resolved_lib_dir }); } @@ -174,8 +175,12 @@ pub fn main() !void { target.query, "-I", opt_resolved_lib_dir.?, // verified earlier - "-o", }); + + if (target.resolved.os.tag == .windows) + try cc_child_args.append(arena, "-lws2_32"); + + try cc_child_args.append(arena, "-o"); } var eval: Eval = .{ From 5d7672f2adc8f1058aa1f49f80055b8816a06371 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 08:02:03 -0700 Subject: [PATCH 189/244] std.Io.Threaded: stub netConnectUnix for Windows --- lib/std/Io/Threaded.zig | 78 ++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 180b491e2f..3deeb917ae 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -57,7 +57,7 @@ const Closure = struct { fn requestCancel(closure: *Closure) void { switch (@atomicRmw(std.Thread.Id, &closure.cancel_tid, .Xchg, canceling_tid, .acq_rel)) { 0, canceling_tid => {}, - else => |tid| switch (builtin.os.tag) { + else => |tid| switch (native_os) { .linux => _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid), posix.SIG.IO), else => {}, }, @@ -168,49 +168,49 @@ pub fn io(t: *Threaded) Io { .conditionWaitUncancelable = conditionWaitUncancelable, .conditionWake = conditionWake, - .dirMake = switch (builtin.os.tag) { + .dirMake = switch (native_os) { .windows => dirMakeWindows, .wasi => dirMakeWasi, else => dirMakePosix, }, - .dirMakePath = switch (builtin.os.tag) { + .dirMakePath = switch (native_os) { .windows => dirMakePathWindows, else => dirMakePathPosix, }, - .dirMakeOpenPath = switch (builtin.os.tag) { + .dirMakeOpenPath = switch (native_os) { .windows => dirMakeOpenPathWindows, .wasi => dirMakeOpenPathWasi, else => dirMakeOpenPathPosix, }, .dirStat = dirStat, - .dirStatPath = switch (builtin.os.tag) { + .dirStatPath = switch (native_os) { .linux => dirStatPathLinux, .windows => dirStatPathWindows, .wasi => dirStatPathWasi, else => dirStatPathPosix, }, - .fileStat = switch (builtin.os.tag) { + .fileStat = switch (native_os) { .linux => fileStatLinux, .windows => fileStatWindows, .wasi => fileStatWasi, else => fileStatPosix, }, - .dirAccess = switch (builtin.os.tag) { + .dirAccess = switch (native_os) { .windows => dirAccessWindows, .wasi => dirAccessWasi, else => dirAccessPosix, }, - .dirCreateFile = switch (builtin.os.tag) { + .dirCreateFile = switch (native_os) { .windows => dirCreateFileWindows, .wasi => dirCreateFileWasi, else => dirCreateFilePosix, }, - .dirOpenFile = switch (builtin.os.tag) { + .dirOpenFile = switch (native_os) { .windows => dirOpenFileWindows, .wasi => dirOpenFileWasi, else => dirOpenFilePosix, }, - .dirOpenDir = switch (builtin.os.tag) { + .dirOpenDir = switch (native_os) { .wasi => dirOpenDirWasi, .haiku => dirOpenDirHaiku, else => dirOpenDirPosix, @@ -219,11 +219,11 @@ pub fn io(t: *Threaded) Io { .fileClose = fileClose, .fileWriteStreaming = fileWriteStreaming, .fileWritePositional = fileWritePositional, - .fileReadStreaming = switch (builtin.os.tag) { + .fileReadStreaming = switch (native_os) { .windows => fileReadStreamingWindows, else => fileReadStreamingPosix, }, - .fileReadPositional = switch (builtin.os.tag) { + .fileReadPositional = switch (native_os) { .windows => fileReadPositionalWindows, else => fileReadPositionalPosix, }, @@ -231,53 +231,56 @@ pub fn io(t: *Threaded) Io { .fileSeekTo = fileSeekTo, .openSelfExe = openSelfExe, - .now = switch (builtin.os.tag) { + .now = switch (native_os) { .windows => nowWindows, .wasi => nowWasi, else => nowPosix, }, - .sleep = switch (builtin.os.tag) { + .sleep = switch (native_os) { .windows => sleepWindows, .wasi => sleepWasi, .linux => sleepLinux, else => sleepPosix, }, - .netListenIp = switch (builtin.os.tag) { + .netListenIp = switch (native_os) { .windows => netListenIpWindows, else => netListenIpPosix, }, - .netListenUnix = switch (builtin.os.tag) { + .netListenUnix = switch (native_os) { .windows => netListenUnixWindows, else => netListenUnixPosix, }, - .netAccept = switch (builtin.os.tag) { + .netAccept = switch (native_os) { .windows => netAcceptWindows, else => netAcceptPosix, }, - .netBindIp = switch (builtin.os.tag) { + .netBindIp = switch (native_os) { .windows => netBindIpWindows, else => netBindIpPosix, }, - .netConnectIp = switch (builtin.os.tag) { + .netConnectIp = switch (native_os) { .windows => netConnectIpWindows, else => netConnectIpPosix, }, - .netConnectUnix = netConnectUnix, + .netConnectUnix = switch (native_os) { + .windows => netConnectUnixWindows, + else => netConnectUnixPosix, + }, .netClose = netClose, - .netRead = switch (builtin.os.tag) { + .netRead = switch (native_os) { .windows => netReadWindows, else => netReadPosix, }, - .netWrite = switch (builtin.os.tag) { + .netWrite = switch (native_os) { .windows => netWriteWindows, else => netWritePosix, }, - .netSend = switch (builtin.os.tag) { + .netSend = switch (native_os) { .windows => netSendWindows, else => netSendPosix, }, - .netReceive = switch (builtin.os.tag) { + .netReceive = switch (native_os) { .windows => netReceiveWindows, else => netReceivePosix, }, @@ -288,12 +291,12 @@ pub fn io(t: *Threaded) Io { }; } -pub const socket_flags_unsupported = builtin.os.tag.isDarwin() or native_os == .haiku; // 💩💩 +pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku; // 💩💩 const have_accept4 = !socket_flags_unsupported; const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); -const have_networking = builtin.os.tag != .wasi; +const have_networking = native_os != .wasi; const have_flock = @TypeOf(posix.system.flock) != void; -const have_sendmmsg = builtin.os.tag == .linux; +const have_sendmmsg = native_os == .linux; const have_futex = switch (builtin.cpu.arch) { .wasm32, .wasm64 => builtin.cpu.has(.wasm, .atomics), else => true, @@ -2916,7 +2919,7 @@ fn netListenUnixWindows( ) net.UnixAddress.ListenError!net.Socket.Handle { if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; + try t.checkCancel(); _ = address; _ = options; @panic("TODO"); @@ -3192,7 +3195,7 @@ fn netConnectIpWindows( } }; } -fn netConnectUnix( +fn netConnectUnixPosix( userdata: ?*anyopaque, address: *const net.UnixAddress, ) net.UnixAddress.ConnectError!net.Socket.Handle { @@ -3209,6 +3212,17 @@ fn netConnectUnix( return socket_fd; } +fn netConnectUnixWindows( + userdata: ?*anyopaque, + address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + try t.checkCancel(); + _ = address; + @panic("TODO"); +} + fn netBindIpPosix( userdata: ?*anyopaque, address: *const IpAddress, @@ -4456,11 +4470,11 @@ fn recoverableOsBugDetected() void { fn clockToPosix(clock: Io.Clock) posix.clockid_t { return switch (clock) { .real => posix.CLOCK.REALTIME, - .awake => switch (builtin.os.tag) { + .awake => switch (native_os) { .macos, .ios, .watchos, .tvos => posix.CLOCK.UPTIME_RAW, else => posix.CLOCK.MONOTONIC, }, - .boot => switch (builtin.os.tag) { + .boot => switch (native_os) { .macos, .ios, .watchos, .tvos => posix.CLOCK.MONOTONIC_RAW, else => posix.CLOCK.BOOTTIME, }, @@ -4523,7 +4537,7 @@ fn statFromPosix(st: *const std.posix.Stat) Io.File.Stat { std.posix.S.IFSOCK => break :k .unix_domain_socket, else => {}, } - if (builtin.os.tag == .illumos) switch (m) { + if (native_os == .illumos) switch (m) { std.posix.S.IFDOOR => break :k .door, std.posix.S.IFPORT => break :k .event_port, else => {}, From 5c527a18544a16c86c3036c143d44b7ed427d2ec Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 09:40:32 -0700 Subject: [PATCH 190/244] std.Io.Threaded: implement fileStat for Windows --- lib/std/Io/Threaded.zig | 84 ++++++++++++++++++++++++++++++----------- lib/std/fs/File.zig | 44 --------------------- 2 files changed, 62 insertions(+), 66 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 3deeb917ae..b2daa35bcb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1033,7 +1033,7 @@ fn dirMakePathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mo _ = dir; _ = sub_path; _ = mode; - @panic("TODO"); + @panic("TODO implement dirMakePathPosix"); } fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { @@ -1042,7 +1042,7 @@ fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, _ = dir; _ = sub_path; _ = mode; - @panic("TODO"); + @panic("TODO implement dirMakePathWindows"); } fn dirMakeOpenPathPosix( @@ -1176,7 +1176,7 @@ fn dirMakeOpenPathWasi( _ = dir; _ = sub_path; _ = mode; - @panic("TODO"); + @panic("TODO implement dirMakeOpenPathWindows"); } fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { @@ -1184,7 +1184,7 @@ fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { try t.checkCancel(); _ = dir; - @panic("TODO"); + @panic("TODO implement dirStat"); } fn dirStatPathLinux( @@ -1375,8 +1375,48 @@ fn fileStatLinux(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File fn fileStatWindows(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); - _ = file; - @panic("TODO"); + + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + var info: windows.FILE_ALL_INFORMATION = undefined; + const rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); + switch (rc) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + .INVALID_PARAMETER => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + return .{ + .inode = info.InternalInformation.IndexNumber, + .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), + .mode = 0, + .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: { + var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined; + const tag_rc = windows.ntdll.NtQueryInformationFile(file.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation); + switch (tag_rc) { + .SUCCESS => {}, + // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e + .INFO_LENGTH_MISMATCH => unreachable, + .ACCESS_DENIED => return error.AccessDenied, + else => return windows.unexpectedStatus(rc), + } + if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) { + break :reparse_point .sym_link; + } + // Unknown reparse point + break :reparse_point .unknown; + } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) + .directory + else + .file, + .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), + .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), + .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), + }; } fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { @@ -2490,7 +2530,7 @@ fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekErr _ = file; _ = offset; - @panic("TODO"); + @panic("TODO implement fileSeekBy"); } fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekError!void { @@ -2571,7 +2611,7 @@ fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelf return dirOpenFileWindowsInner(t, .{ .handle = cwd_handle }, image_path_name, flags); } - @panic("TODO"); + @panic("TODO implement openSelfExe"); } fn fileWritePositional( @@ -2586,7 +2626,7 @@ fn fileWritePositional( _ = file; _ = buffer; _ = offset; - @panic("TODO"); + @panic("TODO implement fileWritePositional"); } } @@ -2596,7 +2636,7 @@ fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, buffer: [][]const u8 try t.checkCancel(); _ = file; _ = buffer; - @panic("TODO"); + @panic("TODO implement fileWriteStreaming"); } } @@ -2922,7 +2962,7 @@ fn netListenUnixWindows( try t.checkCancel(); _ = address; _ = options; - @panic("TODO"); + @panic("TODO implement netListenUnixWindows"); } fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { @@ -3122,7 +3162,7 @@ fn netConnectIpPosix( options: IpAddress.ConnectOptions, ) IpAddress.ConnectError!net.Stream { if (!have_networking) return error.NetworkDown; - if (options.timeout != .none) @panic("TODO"); + if (options.timeout != .none) @panic("TODO implement netConnectIpPosix with timeout"); const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); const socket_fd = try openSocketPosix(t, family, .{ @@ -3146,7 +3186,7 @@ fn netConnectIpWindows( options: IpAddress.ConnectOptions, ) IpAddress.ConnectError!net.Stream { if (!have_networking) return error.NetworkDown; - if (options.timeout != .none) @panic("TODO"); + if (options.timeout != .none) @panic("TODO implement netConnectIpWindows with timeout"); const t: *Threaded = @ptrCast(@alignCast(userdata)); const family = posixAddressFamily(address); const socket_handle = try openSocketWsa(t, family, .{ @@ -3220,7 +3260,7 @@ fn netConnectUnixWindows( const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); _ = address; - @panic("TODO"); + @panic("TODO implement netConnectUnixWindows"); } fn netBindIpPosix( @@ -3525,7 +3565,7 @@ fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8 _ = t; _ = handle; _ = data; - @panic("TODO"); + @panic("TODO implement netReadWindows"); } fn netSendPosix( @@ -3569,7 +3609,7 @@ fn netSendWindows( _ = handle; _ = messages; _ = flags; - @panic("TODO"); + @panic("TODO netSendWindows"); } fn netSendOne( @@ -3872,7 +3912,7 @@ fn netReceiveWindows( _ = data_buffer; _ = flags; _ = timeout; - @panic("TODO"); + @panic("TODO implement netReceiveWindows"); } fn netWritePosix( @@ -3970,7 +4010,7 @@ fn netWriteWindows( _ = header; _ = data; _ = splat; - @panic("TODO"); + @panic("TODO implement netWriteWindows"); } fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { @@ -4036,7 +4076,7 @@ fn netInterfaceNameResolve( if (native_os == .windows) { try t.checkCancel(); - @panic("TODO"); + @panic("TODO implement netInterfaceNameResolve for Windows"); } if (builtin.link_libc) { @@ -4055,15 +4095,15 @@ fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interfa if (native_os == .linux) { _ = interface; - @panic("TODO"); + @panic("TODO implement netInterfaceName for linux"); } if (native_os == .windows) { - @panic("TODO"); + @panic("TODO implement netInterfaceName for windows"); } if (builtin.link_libc) { - @panic("TODO"); + @panic("TODO implement netInterfaceName for libc"); } @panic("unimplemented"); diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index be54041485..d6c5e9f969 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -312,50 +312,6 @@ pub const StatError = posix.FStatError; /// Returns `Stat` containing basic information about the `File`. pub fn stat(self: File) StatError!Stat { - if (builtin.os.tag == .windows) { - var io_status_block: windows.IO_STATUS_BLOCK = undefined; - var info: windows.FILE_ALL_INFORMATION = undefined; - const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); - switch (rc) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - .INVALID_PARAMETER => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - return .{ - .inode = info.InternalInformation.IndexNumber, - .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), - .mode = 0, - .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: { - var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined; - const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation); - switch (tag_rc) { - .SUCCESS => {}, - // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e - .INFO_LENGTH_MISMATCH => unreachable, - .ACCESS_DENIED => return error.AccessDenied, - else => return windows.unexpectedStatus(rc), - } - if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) { - break :reparse_point .sym_link; - } - // Unknown reparse point - break :reparse_point .unknown; - } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) - .directory - else - .file, - .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), - .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), - .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), - }; - } - var threaded: Io.Threaded = .init_single_threaded; const io = threaded.io(); return Io.File.stat(.{ .handle = self.handle }, io); From 701d4bc80b8cbb649364e8ecf86ed9cea8af739d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 09:48:16 -0700 Subject: [PATCH 191/244] objcopy: update for std.Io API --- lib/compiler/objcopy.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compiler/objcopy.zig b/lib/compiler/objcopy.zig index 3e8c4a508a..ee7456a87c 100644 --- a/lib/compiler/objcopy.zig +++ b/lib/compiler/objcopy.zig @@ -156,7 +156,7 @@ fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void const stat = input_file.stat() catch |err| fatal("failed to stat {s}: {t}", .{ input, err }); - var in: File.Reader = .initSize(input_file, io, &input_buffer, stat.size); + var in: File.Reader = .initSize(input_file.adaptToNewApi(), io, &input_buffer, stat.size); const elf_hdr = std.elf.Header.read(&in.interface) catch |err| switch (err) { error.ReadFailed => fatal("unable to read {s}: {t}", .{ input, in.err.? }), @@ -221,7 +221,7 @@ fn cmdObjCopy(gpa: Allocator, arena: Allocator, args: []const []const u8) !void try out.end(); if (listen) { - var stdin_reader = fs.File.stdin().reader(&stdin_buffer); + var stdin_reader = fs.File.stdin().reader(io, &stdin_buffer); var stdout_writer = fs.File.stdout().writer(&stdout_buffer); var server = try Server.init(.{ .in = &stdin_reader.interface, From 46f7e3ea9fff4b3f83cab58c20625d3961be6020 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 13:58:49 -0700 Subject: [PATCH 192/244] std.Io.Threaded: add ioBasic which disables networking --- lib/std/Io/Threaded.zig | 422 +++++++++++++++++++----- lib/std/Thread.zig | 2 +- lib/std/debug.zig | 6 +- lib/std/fs.zig | 2 +- lib/std/fs/Dir.zig | 22 +- lib/std/fs/File.zig | 2 +- lib/std/process/Child.zig | 2 +- test/standalone/child_process/child.zig | 13 +- test/standalone/simple/cat/main.zig | 8 +- tools/fetch_them_macos_headers.zig | 12 +- tools/gen_macos_headers_c.zig | 2 +- 11 files changed, 375 insertions(+), 118 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index b2daa35bcb..be9803adbf 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -168,80 +168,28 @@ pub fn io(t: *Threaded) Io { .conditionWaitUncancelable = conditionWaitUncancelable, .conditionWake = conditionWake, - .dirMake = switch (native_os) { - .windows => dirMakeWindows, - .wasi => dirMakeWasi, - else => dirMakePosix, - }, - .dirMakePath = switch (native_os) { - .windows => dirMakePathWindows, - else => dirMakePathPosix, - }, - .dirMakeOpenPath = switch (native_os) { - .windows => dirMakeOpenPathWindows, - .wasi => dirMakeOpenPathWasi, - else => dirMakeOpenPathPosix, - }, + .dirMake = dirMake, + .dirMakePath = dirMakePath, + .dirMakeOpenPath = dirMakeOpenPath, .dirStat = dirStat, - .dirStatPath = switch (native_os) { - .linux => dirStatPathLinux, - .windows => dirStatPathWindows, - .wasi => dirStatPathWasi, - else => dirStatPathPosix, - }, - .fileStat = switch (native_os) { - .linux => fileStatLinux, - .windows => fileStatWindows, - .wasi => fileStatWasi, - else => fileStatPosix, - }, - .dirAccess = switch (native_os) { - .windows => dirAccessWindows, - .wasi => dirAccessWasi, - else => dirAccessPosix, - }, - .dirCreateFile = switch (native_os) { - .windows => dirCreateFileWindows, - .wasi => dirCreateFileWasi, - else => dirCreateFilePosix, - }, - .dirOpenFile = switch (native_os) { - .windows => dirOpenFileWindows, - .wasi => dirOpenFileWasi, - else => dirOpenFilePosix, - }, - .dirOpenDir = switch (native_os) { - .wasi => dirOpenDirWasi, - .haiku => dirOpenDirHaiku, - else => dirOpenDirPosix, - }, + .dirStatPath = dirStatPath, + .fileStat = fileStat, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirOpenFile = dirOpenFile, + .dirOpenDir = dirOpenDir, .dirClose = dirClose, .fileClose = fileClose, .fileWriteStreaming = fileWriteStreaming, .fileWritePositional = fileWritePositional, - .fileReadStreaming = switch (native_os) { - .windows => fileReadStreamingWindows, - else => fileReadStreamingPosix, - }, - .fileReadPositional = switch (native_os) { - .windows => fileReadPositionalWindows, - else => fileReadPositionalPosix, - }, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, .fileSeekBy = fileSeekBy, .fileSeekTo = fileSeekTo, .openSelfExe = openSelfExe, - .now = switch (native_os) { - .windows => nowWindows, - .wasi => nowWasi, - else => nowPosix, - }, - .sleep = switch (native_os) { - .windows => sleepWindows, - .wasi => sleepWasi, - .linux => sleepLinux, - else => sleepPosix, - }, + .now = now, + .sleep = sleep, .netListenIp = switch (native_os) { .windows => netListenIpWindows, @@ -291,6 +239,73 @@ pub fn io(t: *Threaded) Io { }; } +/// Same as `io` but disables all networking functionality, which has +/// an additional dependency on Windows (ws2_32). +pub fn ioBasic(t: *Threaded) Io { + return .{ + .userdata = t, + .vtable = &.{ + .async = async, + .concurrent = concurrent, + .await = await, + .cancel = cancel, + .cancelRequested = cancelRequested, + .select = select, + + .groupAsync = groupAsync, + .groupWait = groupWait, + .groupWaitUncancelable = groupWaitUncancelable, + .groupCancel = groupCancel, + + .mutexLock = mutexLock, + .mutexLockUncancelable = mutexLockUncancelable, + .mutexUnlock = mutexUnlock, + + .conditionWait = conditionWait, + .conditionWaitUncancelable = conditionWaitUncancelable, + .conditionWake = conditionWake, + + .dirMake = dirMake, + .dirMakePath = dirMakePath, + .dirMakeOpenPath = dirMakeOpenPath, + .dirStat = dirStat, + .dirStatPath = dirStatPath, + .fileStat = fileStat, + .dirAccess = dirAccess, + .dirCreateFile = dirCreateFile, + .dirOpenFile = dirOpenFile, + .dirOpenDir = dirOpenDir, + .dirClose = dirClose, + .fileClose = fileClose, + .fileWriteStreaming = fileWriteStreaming, + .fileWritePositional = fileWritePositional, + .fileReadStreaming = fileReadStreaming, + .fileReadPositional = fileReadPositional, + .fileSeekBy = fileSeekBy, + .fileSeekTo = fileSeekTo, + .openSelfExe = openSelfExe, + + .now = now, + .sleep = sleep, + + .netListenIp = netListenIpUnavailable, + .netListenUnix = netListenUnixUnavailable, + .netAccept = netAcceptUnavailable, + .netBindIp = netBindIpUnavailable, + .netConnectIp = netConnectIpUnavailable, + .netConnectUnix = netConnectUnixUnavailable, + .netClose = netCloseUnavailable, + .netRead = netReadUnavailable, + .netWrite = netWriteUnavailable, + .netSend = netSendUnavailable, + .netReceive = netReceiveUnavailable, + .netInterfaceNameResolve = netInterfaceNameResolveUnavailable, + .netInterfaceName = netInterfaceNameUnavailable, + .netLookup = netLookupUnavailable, + }, + }; +} + pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku; // 💩💩 const have_accept4 = !socket_flags_unsupported; const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); @@ -804,7 +819,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { if (builtin.single_threaded) unreachable; // Deadlock. const t: *Threaded = @ptrCast(@alignCast(userdata)); - const t_io = t.io(); + const t_io = ioBasic(t); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); const cond_state = &ints[0]; @@ -835,6 +850,7 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { if (builtin.single_threaded) unreachable; // Deadlock. const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); comptime assert(@TypeOf(cond.state) == u64); const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); const cond_state = &ints[0]; @@ -858,8 +874,8 @@ fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) I assert(state & waiter_mask != waiter_mask); state += one_waiter; - mutex.unlock(t.io()); - defer mutex.lockUncancelable(t.io()); + mutex.unlock(t_io); + defer mutex.lockUncancelable(t_io); while (true) { try futexWait(t, cond_epoch, epoch); @@ -939,6 +955,12 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. } } +const dirMake = switch (native_os) { + .windows => dirMakeWindows, + .wasi => dirMakeWasi, + else => dirMakePosix, +}; + fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -1027,6 +1049,11 @@ fn dirMakeWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode windows.CloseHandle(sub_dir_handle); } +const dirMakePath = switch (native_os) { + .windows => dirMakePathWindows, + else => dirMakePathPosix, +}; + fn dirMakePathPosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -1045,6 +1072,12 @@ fn dirMakePathWindows(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, @panic("TODO implement dirMakePathWindows"); } +const dirMakeOpenPath = switch (native_os) { + .windows => dirMakeOpenPathWindows, + .wasi => dirMakeOpenPathWasi, + else => dirMakeOpenPathPosix, +}; + fn dirMakeOpenPathPosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -1052,11 +1085,11 @@ fn dirMakeOpenPathPosix( options: Io.Dir.OpenOptions, ) Io.Dir.MakeOpenPathError!Io.Dir { const t: *Threaded = @ptrCast(@alignCast(userdata)); - const t_io = t.io(); - return dir.openDir(t_io, sub_path, options) catch |err| switch (err) { + const t_io = ioBasic(t); + return dirOpenDirPosix(t, dir, sub_path, options) catch |err| switch (err) { error.FileNotFound => { try dir.makePath(t_io, sub_path); - return dir.openDir(t_io, sub_path, options); + return dirOpenDirPosix(t, dir, sub_path, options); }, else => |e| return e, }; @@ -1135,7 +1168,7 @@ fn dirMakeOpenPathWindows( // could cause an infinite loop check_dir: { // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = dir.statPath(t.io(), component.path, .{ + const fstat = dirStatPathWindows(t, dir, component.path, .{ .follow_symlinks = options.follow_symlinks, }) catch |stat_err| switch (stat_err) { error.IsDir => break :check_dir, @@ -1187,6 +1220,13 @@ fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { @panic("TODO implement dirStat"); } +const dirStatPath = switch (native_os) { + .linux => dirStatPathLinux, + .windows => dirStatPathWindows, + .wasi => dirStatPathWasi, + else => dirStatPathPosix, +}; + fn dirStatPathLinux( userdata: ?*anyopaque, dir: Io.Dir, @@ -1275,12 +1315,11 @@ fn dirStatPathWindows( options: Io.Dir.StatPathOptions, ) Io.Dir.StatPathError!Io.File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); - const t_io = t.io(); - var file = try dir.openFile(t_io, sub_path, .{ + const file = try dirOpenFileWindows(t, dir, sub_path, .{ .follow_symlinks = options.follow_symlinks, }); - defer file.close(t_io); - return file.stat(t_io); + defer windows.CloseHandle(file.handle); + return fileStatWindows(t, file); } fn dirStatPathWasi( @@ -1318,6 +1357,13 @@ fn dirStatPathWasi( } } +const fileStat = switch (native_os) { + .linux => fileStatLinux, + .windows => fileStatWindows, + .wasi => fileStatWasi, + else => fileStatPosix, +}; + fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -1440,6 +1486,12 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. } } +const dirAccess = switch (native_os) { + .windows => dirAccessWindows, + .wasi => dirAccessWasi, + else => dirAccessPosix, +}; + fn dirAccessPosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -1589,6 +1641,12 @@ fn dirAccessWindows( } } +const dirCreateFile = switch (native_os) { + .windows => dirCreateFileWindows, + .wasi => dirCreateFileWasi, + else => dirCreateFilePosix, +}; + fn dirCreateFilePosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -1827,6 +1885,12 @@ fn dirCreateFileWasi( } } +const dirOpenFile = switch (native_os) { + .windows => dirOpenFileWindows, + .wasi => dirOpenFileWasi, + else => dirOpenFilePosix, +}; + fn dirOpenFilePosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -2077,6 +2141,12 @@ fn dirOpenFileWasi( } } +const dirOpenDir = switch (native_os) { + .wasi => dirOpenDirWasi, + .haiku => dirOpenDirHaiku, + else => dirOpenDirPosix, +}; + fn dirOpenDirPosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -2320,6 +2390,11 @@ fn fileClose(userdata: ?*anyopaque, file: Io.File) void { posix.close(file.handle); } +const fileReadStreaming = switch (native_os) { + .windows => fileReadStreamingWindows, + else => fileReadStreamingPosix, +}; + fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); @@ -2482,6 +2557,11 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, o } } +const fileReadPositional = switch (native_os) { + .windows => fileReadPositionalWindows, + else => fileReadPositionalPosix, +}; + fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -2652,6 +2732,12 @@ fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp } } +const now = switch (native_os) { + .windows => nowWindows, + .wasi => nowWasi, + else => nowPosix, +}; + fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; @@ -2680,6 +2766,13 @@ fn nowWasi(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { return .fromNanoseconds(ns); } +const sleep = switch (native_os) { + .windows => sleepWindows, + .wasi => sleepWasi, + .linux => sleepLinux, + else => sleepPosix, +}; + fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const clock_id: posix.clockid_t = clockToPosix(switch (timeout) { @@ -2710,9 +2803,10 @@ fn sleepLinux(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); try t.checkCancel(); const ms = ms: { - const d = (try timeout.toDurationFromNow(t.io())) orelse + const d = (try timeout.toDurationFromNow(t_io)) orelse break :ms std.math.maxInt(windows.DWORD); break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds()); }; @@ -2721,11 +2815,12 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); try t.checkCancel(); const w = std.os.wasi; - const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t.io())) |d| .{ + const clock: w.subscription_clock_t = if (try timeout.toDurationFromNow(t_io)) |d| .{ .id = clockToWasi(d.clock), .timeout = std.math.lossyCast(u64, d.raw.nanoseconds), .precision = 0, @@ -2750,11 +2845,12 @@ fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { fn sleepPosix(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); const sec_type = @typeInfo(posix.timespec).@"struct".fields[0].type; const nsec_type = @typeInfo(posix.timespec).@"struct".fields[1].type; var timespec: posix.timespec = t: { - const d = (try timeout.toDurationFromNow(t.io())) orelse break :t .{ + const d = (try timeout.toDurationFromNow(t_io)) orelse break :t .{ .sec = std.math.maxInt(sec_type), .nsec = std.math.maxInt(nsec_type), }; @@ -2919,6 +3015,17 @@ fn netListenIpWindows( }; } +fn netListenIpUnavailable( + userdata: ?*anyopaque, + address: IpAddress, + options: IpAddress.ListenOptions, +) IpAddress.ListenError!net.Server { + _ = userdata; + _ = address; + _ = options; + return error.NetworkDown; +} + fn netListenUnixPosix( userdata: ?*anyopaque, address: *const net.UnixAddress, @@ -2965,6 +3072,17 @@ fn netListenUnixWindows( @panic("TODO implement netListenUnixWindows"); } +fn netListenUnixUnavailable( + userdata: ?*anyopaque, + address: *const net.UnixAddress, + options: net.UnixAddress.ListenOptions, +) net.UnixAddress.ListenError!net.Socket.Handle { + _ = userdata; + _ = address; + _ = options; + return error.AddressFamilyUnsupported; +} + fn posixBindUnix(t: *Threaded, fd: posix.socket_t, addr: *const posix.sockaddr, addr_len: posix.socklen_t) !void { while (true) { try t.checkCancel(); @@ -3235,6 +3353,17 @@ fn netConnectIpWindows( } }; } +fn netConnectIpUnavailable( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.ConnectOptions, +) IpAddress.ConnectError!net.Stream { + _ = userdata; + _ = address; + _ = options; + return error.NetworkDown; +} + fn netConnectUnixPosix( userdata: ?*anyopaque, address: *const net.UnixAddress, @@ -3263,6 +3392,15 @@ fn netConnectUnixWindows( @panic("TODO implement netConnectUnixWindows"); } +fn netConnectUnixUnavailable( + userdata: ?*anyopaque, + address: *const net.UnixAddress, +) net.UnixAddress.ConnectError!net.Socket.Handle { + _ = userdata; + _ = address; + return error.AddressFamilyUnsupported; +} + fn netBindIpPosix( userdata: ?*anyopaque, address: *const IpAddress, @@ -3330,6 +3468,17 @@ fn netBindIpWindows( }; } +fn netBindIpUnavailable( + userdata: ?*anyopaque, + address: *const IpAddress, + options: IpAddress.BindOptions, +) IpAddress.BindError!net.Socket { + _ = userdata; + _ = address; + _ = options; + return error.NetworkDown; +} + fn openSocketPosix( t: *Threaded, family: posix.sa_family_t, @@ -3498,7 +3647,14 @@ fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net } } +fn netAcceptUnavailable(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net.Server.AcceptError!net.Stream { + _ = userdata; + _ = listen_handle; + return error.NetworkDown; +} + fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; @@ -3560,7 +3716,7 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. } fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { - if (!have_networking) return .{ error.NetworkDown, 0 }; + if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; _ = handle; @@ -3568,6 +3724,13 @@ fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8 @panic("TODO implement netReadWindows"); } +fn netReadUnavailable(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { + _ = userdata; + _ = fd; + _ = data; + return error.NetworkDown; +} + fn netSendPosix( userdata: ?*anyopaque, handle: net.Socket.Handle, @@ -3612,6 +3775,19 @@ fn netSendWindows( @panic("TODO netSendWindows"); } +fn netSendUnavailable( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + messages: []net.OutgoingMessage, + flags: net.SendFlags, +) struct { ?net.Socket.SendError, usize } { + _ = userdata; + _ = handle; + _ = messages; + _ = flags; + return .{ error.NetworkDown, 0 }; +} + fn netSendOne( t: *Threaded, handle: net.Socket.Handle, @@ -3777,6 +3953,7 @@ fn netReceivePosix( ) struct { ?net.Socket.ReceiveTimeoutError, usize } { if (!have_networking) return .{ error.NetworkDown, 0 }; const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = io(t); // recvmmsg is useless, here's why: // * [timeout bug](https://bugzilla.kernel.org/show_bug.cgi?id=75371) @@ -3803,7 +3980,7 @@ fn netReceivePosix( var message_i: usize = 0; var data_i: usize = 0; - const deadline = timeout.toDeadline(t.io()) catch |err| return .{ err, message_i }; + const deadline = timeout.toDeadline(t_io) catch |err| return .{ err, message_i }; recv: while (true) { t.checkCancel() catch |err| return .{ err, message_i }; @@ -3849,7 +4026,7 @@ fn netReceivePosix( const max_poll_ms = std.math.maxInt(u31); const timeout_ms: u31 = if (deadline) |d| t: { - const duration = d.durationFromNow(t.io()) catch |err| return .{ err, message_i }; + const duration = d.durationFromNow(t_io) catch |err| return .{ err, message_i }; if (duration.raw.nanoseconds <= 0) return .{ error.Timeout, message_i }; break :t @intCast(@min(max_poll_ms, duration.raw.toMilliseconds())); } else max_poll_ms; @@ -3915,6 +4092,23 @@ fn netReceiveWindows( @panic("TODO implement netReceiveWindows"); } +fn netReceiveUnavailable( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + message_buffer: []net.IncomingMessage, + data_buffer: []u8, + flags: net.ReceiveFlags, + timeout: Io.Timeout, +) struct { ?net.Socket.ReceiveTimeoutError, usize } { + _ = userdata; + _ = handle; + _ = message_buffer; + _ = data_buffer; + _ = flags; + _ = timeout; + return .{ error.NetworkDown, 0 }; +} + fn netWritePosix( userdata: ?*anyopaque, fd: net.Socket.Handle, @@ -4013,6 +4207,21 @@ fn netWriteWindows( @panic("TODO implement netWriteWindows"); } +fn netWriteUnavailable( + userdata: ?*anyopaque, + handle: net.Socket.Handle, + header: []const u8, + data: []const []const u8, + splat: usize, +) net.Stream.Writer.Error!usize { + _ = userdata; + _ = handle; + _ = header; + _ = data; + _ = splat; + return error.NetworkDown; +} + fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { // OS checks ptr addr before length so zero length vectors must be omitted. if (bytes.len == 0) return; @@ -4030,6 +4239,12 @@ fn netClose(userdata: ?*anyopaque, handle: net.Socket.Handle) void { } } +fn netCloseUnavailable(userdata: ?*anyopaque, handle: net.Socket.Handle) void { + _ = userdata; + _ = handle; + unreachable; // How you gonna close something that was impossible to open? +} + fn netInterfaceNameResolve( userdata: ?*anyopaque, name: *const net.Interface.Name, @@ -4089,6 +4304,15 @@ fn netInterfaceNameResolve( @panic("unimplemented"); } +fn netInterfaceNameResolveUnavailable( + userdata: ?*anyopaque, + name: *const net.Interface.Name, +) net.Interface.Name.ResolveError!net.Interface { + _ = userdata; + _ = name; + return error.InterfaceNotFound; +} + fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { const t: *Threaded = @ptrCast(@alignCast(userdata)); try t.checkCancel(); @@ -4109,6 +4333,12 @@ fn netInterfaceName(userdata: ?*anyopaque, interface: net.Interface) net.Interfa @panic("unimplemented"); } +fn netInterfaceNameUnavailable(userdata: ?*anyopaque, interface: net.Interface) net.Interface.NameError!net.Interface.Name { + _ = userdata; + _ = interface; + return error.Unexpected; +} + fn netLookup( userdata: ?*anyopaque, host_name: HostName, @@ -4116,17 +4346,31 @@ fn netLookup( options: HostName.LookupOptions, ) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); - const t_io = t.io(); + const t_io = io(t); resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, host_name, resolved, options) }); } +fn netLookupUnavailable( + userdata: ?*anyopaque, + host_name: HostName, + resolved: *Io.Queue(HostName.LookupResult), + options: HostName.LookupOptions, +) void { + _ = host_name; + _ = options; + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + resolved.putOneUncancelable(t_io, .{ .end = error.NetworkDown }); +} + fn netLookupFallible( t: *Threaded, host_name: HostName, resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) !void { - const t_io = t.io(); + if (!have_networking) return error.NetworkDown; + const t_io = io(t); const name = host_name.bytes; assert(name.len <= HostName.max_len); @@ -4637,7 +4881,7 @@ fn lookupDnsSearch( resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) HostName.LookupError!void { - const t_io = t.io(); + const t_io = io(t); const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed; // Count dots, suppress search when >=ndots or name ends in @@ -4681,7 +4925,7 @@ fn lookupDns( resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) HostName.LookupError!void { - const t_io = t.io(); + const t_io = io(t); const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{ .{ .af = .ip6, .rr = .A }, .{ .af = .ip4, .rr = .AAAA }, @@ -4868,7 +5112,7 @@ fn lookupHosts( resolved: *Io.Queue(HostName.LookupResult), options: HostName.LookupOptions, ) !void { - const t_io = t.io(); + const t_io = io(t); const file = Io.File.openAbsolute(t_io, "/etc/hosts", .{}) catch |err| switch (err) { error.FileNotFound, error.NotDir, @@ -4906,7 +5150,7 @@ fn lookupHostsReader( options: HostName.LookupOptions, reader: *Io.Reader, ) error{ ReadFailed, Canceled, UnknownHostName }!void { - const t_io = t.io(); + const t_io = io(t); var addresses_len: usize = 0; var canonical_name: ?HostName = null; while (true) { @@ -5374,7 +5618,7 @@ const Wsa = struct { }; fn initializeWsa(t: *Threaded) error{NetworkDown}!void { - const t_io = t.io(); + const t_io = io(t); const wsa = &t.wsa; wsa.mutex.lockUncancelable(t_io); defer wsa.mutex.unlock(t_io); diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 7e1ce45a46..f3abe1e6cf 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -320,7 +320,7 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); var threaded: std.Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); diff --git a/lib/std/debug.zig b/lib/std/debug.zig index a92a4b360f..616a524011 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -649,7 +649,7 @@ pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: /// See `captureCurrentStackTrace` to capture the trace addresses into a buffer instead of printing. pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_config: tty.Config) Writer.Error!void { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); if (!std.options.allow_stack_tracing) { tty_config.setColor(writer, .dim) catch {}; @@ -780,7 +780,7 @@ pub fn writeStackTrace(st: *const StackTrace, writer: *Writer, tty_config: tty.C // We use an independent Io implementation here in case there was a problem // with the application's Io implementation itself. var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); // Fetch `st.index` straight away. Aside from avoiding redundant loads, this prevents issues if // `st` is `@errorReturnTrace()` and errors are encountered while writing the stack trace. @@ -1602,7 +1602,7 @@ test "manage resources correctly" { }; const gpa = std.testing.allocator; var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); var discarding: Io.Writer.Discarding = .init(&.{}); var di: SelfInfo = .init; defer di.deinit(gpa); diff --git a/lib/std/fs.zig b/lib/std/fs.zig index ea39ce8e33..6db63b6e2b 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -340,7 +340,7 @@ pub const OpenSelfExeError = Io.File.OpenSelfExeError; pub fn openSelfExe(flags: File.OpenFlags) OpenSelfExeError!File { if (native_os == .linux or native_os == .serenity or native_os == .windows) { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return .adaptFromNewApi(try Io.File.openSelfExe(io, flags)); } // Use of max_path_bytes here is valid as the resulting path is immediately diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index 15f3a6c45a..c90eeef508 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -849,14 +849,14 @@ pub fn close(self: *Dir) void { /// Deprecated in favor of `Io.Dir.openFile`. pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return .adaptFromNewApi(try Io.Dir.openFile(self.adaptToNewApi(), io, sub_path, flags)); } /// Deprecated in favor of `Io.Dir.createFile`. pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); const new_file = try Io.Dir.createFile(self.adaptToNewApi(), io, sub_path, flags); return .adaptFromNewApi(new_file); } @@ -867,7 +867,7 @@ pub const MakeError = Io.Dir.MakeError; /// Deprecated in favor of `Io.Dir.makeDir`. pub fn makeDir(self: Dir, sub_path: []const u8) MakeError!void { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return Io.Dir.makeDir(.{ .handle = self.fd }, io, sub_path); } @@ -894,14 +894,14 @@ pub const MakePathError = Io.Dir.MakePathError; /// Deprecated in favor of `Io.Dir.makePathStatus`. pub fn makePathStatus(self: Dir, sub_path: []const u8) MakePathError!MakePathStatus { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return Io.Dir.makePathStatus(.{ .handle = self.fd }, io, sub_path); } /// Deprecated in favor of `Io.Dir.makeOpenPath`. pub fn makeOpenPath(dir: Dir, sub_path: []const u8, options: OpenOptions) Io.Dir.MakeOpenPathError!Dir { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return .adaptFromNewApi(try Io.Dir.makeOpenPath(dir.adaptToNewApi(), io, sub_path, options)); } @@ -1070,7 +1070,7 @@ pub const OpenOptions = Io.Dir.OpenOptions; /// Deprecated in favor of `Io.Dir.openDir`. pub fn openDir(self: Dir, sub_path: []const u8, args: OpenOptions) OpenError!Dir { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return .adaptFromNewApi(try Io.Dir.openDir(.{ .handle = self.fd }, io, sub_path, args)); } @@ -1384,7 +1384,7 @@ pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { /// Deprecated in favor of `Io.Dir.readFile`. pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return Io.Dir.readFile(.{ .handle = self.fd }, io, file_path, buffer); } @@ -1437,7 +1437,7 @@ pub fn readFileAllocOptions( comptime sentinel: ?u8, ) ReadFileAllocError!(if (sentinel) |s| [:s]align(alignment.toByteUnits()) u8 else []align(alignment.toByteUnits()) u8) { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); var file = try dir.openFile(sub_path, .{}); defer file.close(); @@ -1892,7 +1892,7 @@ pub const AccessError = Io.Dir.AccessError; /// Deprecated in favor of `Io.Dir.access`. pub fn access(self: Dir, sub_path: []const u8, options: Io.Dir.AccessOptions) AccessError!void { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return Io.Dir.access(self.adaptToNewApi(), io, sub_path, options); } @@ -1928,7 +1928,7 @@ pub fn copyFile( options: CopyFileOptions, ) CopyFileError!void { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); const file = try source_dir.openFile(source_path, .{}); var file_reader: File.Reader = .init(.{ .handle = file.handle }, io, &.{}); @@ -1996,7 +1996,7 @@ pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError /// Deprecated in favor of `Io.Dir.statPath`. pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{}); } diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index d6c5e9f969..11d8e7471b 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -313,7 +313,7 @@ pub const StatError = posix.FStatError; /// Returns `Stat` containing basic information about the `File`. pub fn stat(self: File) StatError!Stat { var threaded: Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); return Io.File.stat(.{ .handle = self.handle }, io); } diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig index e49c5c4532..c84c878972 100644 --- a/lib/std/process/Child.zig +++ b/lib/std/process/Child.zig @@ -1089,7 +1089,7 @@ fn windowsCreateProcessPathExt( // opening function knowing which implementation we are in. Here, we imitate // that scenario. var threaded: std.Io.Threaded = .init_single_threaded; - const io = threaded.io(); + const io = threaded.ioBasic(); var dir = dir: { // needs to be null-terminated diff --git a/test/standalone/child_process/child.zig b/test/standalone/child_process/child.zig index b02bec3500..2e74f30882 100644 --- a/test/standalone/child_process/child.zig +++ b/test/standalone/child_process/child.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; // 42 is expected by parent; other values result in test failure var exit_code: u8 = 42; @@ -6,12 +7,17 @@ var exit_code: u8 = 42; pub fn main() !void { var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); const arena = arena_state.allocator(); - try run(arena); + + var threaded: std.Io.Threaded = .init(arena); + defer threaded.deinit(); + const io = threaded.io(); + + try run(arena, io); arena_state.deinit(); std.process.exit(exit_code); } -fn run(allocator: std.mem.Allocator) !void { +fn run(allocator: std.mem.Allocator, io: Io) !void { var args = try std.process.argsWithAllocator(allocator); defer args.deinit(); _ = args.next() orelse unreachable; // skip binary name @@ -33,7 +39,8 @@ fn run(allocator: std.mem.Allocator) !void { const hello_stdin = "hello from stdin"; var buf: [hello_stdin.len]u8 = undefined; const stdin: std.fs.File = .stdin(); - const n = try stdin.readAll(&buf); + var reader = stdin.reader(io, &.{}); + const n = try reader.interface.readSliceShort(&buf); if (!std.mem.eql(u8, buf[0..n], hello_stdin)) { testError("stdin: '{s}'; want '{s}'", .{ buf[0..n], hello_stdin }); } diff --git a/test/standalone/simple/cat/main.zig b/test/standalone/simple/cat/main.zig index 61b7fbe7ce..9ea980aecc 100644 --- a/test/standalone/simple/cat/main.zig +++ b/test/standalone/simple/cat/main.zig @@ -9,6 +9,10 @@ pub fn main() !void { defer arena_instance.deinit(); const arena = arena_instance.allocator(); + var threaded: std.Io.Threaded = .init(arena); + defer threaded.deinit(); + const io = threaded.io(); + const args = try std.process.argsAlloc(arena); const exe = args[0]; @@ -16,7 +20,7 @@ pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = fs.File.stdout().writerStreaming(&stdout_buffer); const stdout = &stdout_writer.interface; - var stdin_reader = fs.File.stdin().readerStreaming(&.{}); + var stdin_reader = fs.File.stdin().readerStreaming(io, &.{}); const cwd = fs.cwd(); @@ -32,7 +36,7 @@ pub fn main() !void { defer file.close(); catted_anything = true; - var file_reader = file.reader(&.{}); + var file_reader = file.reader(io, &.{}); _ = try stdout.sendFileAll(&file_reader, .unlimited); try stdout.flush(); } diff --git a/tools/fetch_them_macos_headers.zig b/tools/fetch_them_macos_headers.zig index d91235fee9..2a2a2452e7 100644 --- a/tools/fetch_them_macos_headers.zig +++ b/tools/fetch_them_macos_headers.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const fs = std.fs; const mem = std.mem; const process = std.process; @@ -85,7 +86,7 @@ pub fn main() anyerror!void { } else try argv.append(arg); } - var threaded: std.Io.Threaded = .init(gpa); + var threaded: Io.Threaded = .init(gpa); defer threaded.deinit(); const io = threaded.io(); @@ -118,12 +119,13 @@ pub fn main() anyerror!void { .arch = arch, .os_ver = os_ver, }; - try fetchTarget(allocator, argv.items, sysroot_path, target, version, tmp); + try fetchTarget(allocator, io, argv.items, sysroot_path, target, version, tmp); } } fn fetchTarget( arena: Allocator, + io: Io, args: []const []const u8, sysroot: []const u8, target: Target, @@ -194,7 +196,7 @@ fn fetchTarget( var dirs = std.StringHashMap(fs.Dir).init(arena); try dirs.putNoClobber(".", dest_dir); - var headers_list_file_reader = headers_list_file.reader(&.{}); + var headers_list_file_reader = headers_list_file.reader(io, &.{}); const headers_list_str = try headers_list_file_reader.interface.allocRemaining(arena, .unlimited); const prefix = "/usr/include"; @@ -267,8 +269,8 @@ const Version = struct { pub fn format( v: Version, - writer: *std.Io.Writer, - ) std.Io.Writer.Error!void { + writer: *Io.Writer, + ) Io.Writer.Error!void { try writer.print("{d}.{d}.{d}", .{ v.major, v.minor, v.patch }); } }; diff --git a/tools/gen_macos_headers_c.zig b/tools/gen_macos_headers_c.zig index a5d865bf03..fe036cf6b7 100644 --- a/tools/gen_macos_headers_c.zig +++ b/tools/gen_macos_headers_c.zig @@ -73,7 +73,7 @@ fn findHeaders( switch (entry.kind) { .directory => { const path = try std.fs.path.join(arena, &.{ prefix, entry.name }); - var subdir = try dir.openDir(entry.name, .{ .no_follow = true }); + var subdir = try dir.openDir(entry.name, .{ .follow_symlinks = false }); defer subdir.close(); try findHeaders(arena, subdir, path, paths); }, From c87fbd58787823da6db48e0bc488b5344fe52d43 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 14:05:39 -0700 Subject: [PATCH 193/244] std.os.linux.IoUring: use linux msghdr it disagrees with posix msghdr --- lib/std/os/linux/IoUring.zig | 132 +++++++++++++++++------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/lib/std/os/linux/IoUring.zig b/lib/std/os/linux/IoUring.zig index 5bcb4ec1a9..3b2f753a37 100644 --- a/lib/std/os/linux/IoUring.zig +++ b/lib/std/os/linux/IoUring.zig @@ -10,7 +10,7 @@ const testing = std.testing; const is_linux = builtin.os.tag == .linux; const page_size_min = std.heap.page_size_min; -fd: posix.fd_t = -1, +fd: linux.fd_t = -1, sq: SubmissionQueue, cq: CompletionQueue, flags: u32, @@ -62,7 +62,7 @@ pub fn init_params(entries: u16, p: *linux.io_uring_params) !IoUring { .NOSYS => return error.SystemOutdated, else => |errno| return posix.unexpectedErrno(errno), } - const fd = @as(posix.fd_t, @intCast(res)); + const fd = @as(linux.fd_t, @intCast(res)); assert(fd >= 0); errdefer posix.close(fd); @@ -341,7 +341,7 @@ pub fn cq_advance(self: *IoUring, count: u32) void { /// apply to the write, since the fsync may complete before the write is issued to the disk. /// You should preferably use `link_with_next_sqe()` on a write's SQE to link it with an fsync, /// or else insert a full write barrier using `drain_previous_sqes()` when queueing an fsync. -pub fn fsync(self: *IoUring, user_data: u64, fd: posix.fd_t, flags: u32) !*linux.io_uring_sqe { +pub fn fsync(self: *IoUring, user_data: u64, fd: linux.fd_t, flags: u32) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); sqe.prep_fsync(fd, flags); sqe.user_data = user_data; @@ -386,7 +386,7 @@ pub const ReadBuffer = union(enum) { pub fn read( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: ReadBuffer, offset: u64, ) !*linux.io_uring_sqe { @@ -409,7 +409,7 @@ pub fn read( pub fn write( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, offset: u64, ) !*linux.io_uring_sqe { @@ -433,7 +433,7 @@ pub fn write( /// See https://github.com/axboe/liburing/issues/291 /// /// Returns a pointer to the SQE so that you can further modify the SQE for advanced use cases. -pub fn splice(self: *IoUring, user_data: u64, fd_in: posix.fd_t, off_in: u64, fd_out: posix.fd_t, off_out: u64, len: usize) !*linux.io_uring_sqe { +pub fn splice(self: *IoUring, user_data: u64, fd_in: linux.fd_t, off_in: u64, fd_out: linux.fd_t, off_out: u64, len: usize) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); sqe.prep_splice(fd_in, off_in, fd_out, off_out, len); sqe.user_data = user_data; @@ -448,7 +448,7 @@ pub fn splice(self: *IoUring, user_data: u64, fd_in: posix.fd_t, off_in: u64, fd pub fn read_fixed( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: *posix.iovec, offset: u64, buffer_index: u16, @@ -466,7 +466,7 @@ pub fn read_fixed( pub fn writev( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, iovecs: []const posix.iovec_const, offset: u64, ) !*linux.io_uring_sqe { @@ -484,7 +484,7 @@ pub fn writev( pub fn write_fixed( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: *posix.iovec, offset: u64, buffer_index: u16, @@ -501,7 +501,7 @@ pub fn write_fixed( pub fn accept( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -523,7 +523,7 @@ pub fn accept( pub fn accept_multishot( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -548,7 +548,7 @@ pub fn accept_multishot( pub fn accept_direct( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -564,7 +564,7 @@ pub fn accept_direct( pub fn accept_multishot_direct( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: ?*posix.sockaddr, addrlen: ?*posix.socklen_t, flags: u32, @@ -580,7 +580,7 @@ pub fn accept_multishot_direct( pub fn connect( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: *const posix.sockaddr, addrlen: posix.socklen_t, ) !*linux.io_uring_sqe { @@ -595,8 +595,8 @@ pub fn connect( pub fn epoll_ctl( self: *IoUring, user_data: u64, - epfd: posix.fd_t, - fd: posix.fd_t, + epfd: linux.fd_t, + fd: linux.fd_t, op: u32, ev: ?*linux.epoll_event, ) !*linux.io_uring_sqe { @@ -626,7 +626,7 @@ pub const RecvBuffer = union(enum) { pub fn recv( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: RecvBuffer, flags: u32, ) !*linux.io_uring_sqe { @@ -650,7 +650,7 @@ pub fn recv( pub fn send( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -678,7 +678,7 @@ pub fn send( pub fn send_zc( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, send_flags: u32, zc_flags: u16, @@ -695,7 +695,7 @@ pub fn send_zc( pub fn send_zc_fixed( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, buffer: []const u8, send_flags: u32, zc_flags: u16, @@ -713,8 +713,8 @@ pub fn send_zc_fixed( pub fn recvmsg( self: *IoUring, user_data: u64, - fd: posix.fd_t, - msg: *posix.msghdr, + fd: linux.fd_t, + msg: *linux.msghdr, flags: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -729,8 +729,8 @@ pub fn recvmsg( pub fn sendmsg( self: *IoUring, user_data: u64, - fd: posix.fd_t, - msg: *const posix.msghdr_const, + fd: linux.fd_t, + msg: *const linux.msghdr_const, flags: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -745,8 +745,8 @@ pub fn sendmsg( pub fn sendmsg_zc( self: *IoUring, user_data: u64, - fd: posix.fd_t, - msg: *const posix.msghdr_const, + fd: linux.fd_t, + msg: *const linux.msghdr_const, flags: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -761,7 +761,7 @@ pub fn sendmsg_zc( pub fn openat( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, path: [*:0]const u8, flags: linux.O, mode: posix.mode_t, @@ -786,7 +786,7 @@ pub fn openat( pub fn openat_direct( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, path: [*:0]const u8, flags: linux.O, mode: posix.mode_t, @@ -801,7 +801,7 @@ pub fn openat_direct( /// Queues (but does not submit) an SQE to perform a `close(2)`. /// Returns a pointer to the SQE. /// Available since 5.6. -pub fn close(self: *IoUring, user_data: u64, fd: posix.fd_t) !*linux.io_uring_sqe { +pub fn close(self: *IoUring, user_data: u64, fd: linux.fd_t) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); sqe.prep_close(fd); sqe.user_data = user_data; @@ -896,7 +896,7 @@ pub fn link_timeout( pub fn poll_add( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, poll_mask: u32, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -939,7 +939,7 @@ pub fn poll_update( pub fn fallocate( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, mode: i32, offset: u64, len: u64, @@ -955,7 +955,7 @@ pub fn fallocate( pub fn statx( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, path: [:0]const u8, flags: u32, mask: u32, @@ -1008,9 +1008,9 @@ pub fn shutdown( pub fn renameat( self: *IoUring, user_data: u64, - old_dir_fd: posix.fd_t, + old_dir_fd: linux.fd_t, old_path: [*:0]const u8, - new_dir_fd: posix.fd_t, + new_dir_fd: linux.fd_t, new_path: [*:0]const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -1025,7 +1025,7 @@ pub fn renameat( pub fn unlinkat( self: *IoUring, user_data: u64, - dir_fd: posix.fd_t, + dir_fd: linux.fd_t, path: [*:0]const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -1040,7 +1040,7 @@ pub fn unlinkat( pub fn mkdirat( self: *IoUring, user_data: u64, - dir_fd: posix.fd_t, + dir_fd: linux.fd_t, path: [*:0]const u8, mode: posix.mode_t, ) !*linux.io_uring_sqe { @@ -1056,7 +1056,7 @@ pub fn symlinkat( self: *IoUring, user_data: u64, target: [*:0]const u8, - new_dir_fd: posix.fd_t, + new_dir_fd: linux.fd_t, link_path: [*:0]const u8, ) !*linux.io_uring_sqe { const sqe = try self.get_sqe(); @@ -1070,9 +1070,9 @@ pub fn symlinkat( pub fn linkat( self: *IoUring, user_data: u64, - old_dir_fd: posix.fd_t, + old_dir_fd: linux.fd_t, old_path: [*:0]const u8, - new_dir_fd: posix.fd_t, + new_dir_fd: linux.fd_t, new_path: [*:0]const u8, flags: u32, ) !*linux.io_uring_sqe { @@ -1144,7 +1144,7 @@ pub fn waitid( /// Registering file descriptors will wait for the ring to idle. /// Files are automatically unregistered by the kernel when the ring is torn down. /// An application need unregister only if it wants to register a new array of file descriptors. -pub fn register_files(self: *IoUring, fds: []const posix.fd_t) !void { +pub fn register_files(self: *IoUring, fds: []const linux.fd_t) !void { assert(self.fd >= 0); const res = linux.io_uring_register( self.fd, @@ -1163,7 +1163,7 @@ pub fn register_files(self: *IoUring, fds: []const posix.fd_t) !void { /// * removing an existing entry (set the fd to -1) /// * replacing an existing entry with a new fd /// Adding new file descriptors must be done with `register_files`. -pub fn register_files_update(self: *IoUring, offset: u32, fds: []const posix.fd_t) !void { +pub fn register_files_update(self: *IoUring, offset: u32, fds: []const linux.fd_t) !void { assert(self.fd >= 0); const FilesUpdate = extern struct { @@ -1232,7 +1232,7 @@ pub fn register_file_alloc_range(self: *IoUring, offset: u32, len: u32) !void { /// Registers the file descriptor for an eventfd that will be notified of completion events on /// an io_uring instance. /// Only a single a eventfd can be registered at any given point in time. -pub fn register_eventfd(self: *IoUring, fd: posix.fd_t) !void { +pub fn register_eventfd(self: *IoUring, fd: linux.fd_t) !void { assert(self.fd >= 0); const res = linux.io_uring_register( self.fd, @@ -1247,7 +1247,7 @@ pub fn register_eventfd(self: *IoUring, fd: posix.fd_t) !void { /// an io_uring instance. Notifications are only posted for events that complete in an async manner. /// This means that events that complete inline while being submitted do not trigger a notification event. /// Only a single eventfd can be registered at any given point in time. -pub fn register_eventfd_async(self: *IoUring, fd: posix.fd_t) !void { +pub fn register_eventfd_async(self: *IoUring, fd: linux.fd_t) !void { assert(self.fd >= 0); const res = linux.io_uring_register( self.fd, @@ -1405,7 +1405,7 @@ pub fn socket_direct_alloc( pub fn bind( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, addr: *const posix.sockaddr, addrlen: posix.socklen_t, flags: u32, @@ -1422,7 +1422,7 @@ pub fn bind( pub fn listen( self: *IoUring, user_data: u64, - fd: posix.fd_t, + fd: linux.fd_t, backlog: usize, flags: u32, ) !*linux.io_uring_sqe { @@ -1513,7 +1513,7 @@ pub const SubmissionQueue = struct { sqe_head: u32 = 0, sqe_tail: u32 = 0, - pub fn init(fd: posix.fd_t, p: linux.io_uring_params) !SubmissionQueue { + pub fn init(fd: linux.fd_t, p: linux.io_uring_params) !SubmissionQueue { assert(fd >= 0); assert((p.features & linux.IORING_FEAT_SINGLE_MMAP) != 0); const size = @max( @@ -1576,7 +1576,7 @@ pub const CompletionQueue = struct { overflow: *u32, cqes: []linux.io_uring_cqe, - pub fn init(fd: posix.fd_t, p: linux.io_uring_params, sq: SubmissionQueue) !CompletionQueue { + pub fn init(fd: linux.fd_t, p: linux.io_uring_params, sq: SubmissionQueue) !CompletionQueue { assert(fd >= 0); assert((p.features & linux.IORING_FEAT_SINGLE_MMAP) != 0); const mmap = sq.mmap; @@ -1677,7 +1677,7 @@ pub const BufferGroup = struct { } // Prepare recv operation which will select buffer from this group. - pub fn recv(self: *BufferGroup, user_data: u64, fd: posix.fd_t, flags: u32) !*linux.io_uring_sqe { + pub fn recv(self: *BufferGroup, user_data: u64, fd: linux.fd_t, flags: u32) !*linux.io_uring_sqe { var sqe = try self.ring.get_sqe(); sqe.prep_rw(.RECV, fd, 0, 0, 0); sqe.rw_flags = flags; @@ -1688,7 +1688,7 @@ pub const BufferGroup = struct { } // Prepare multishot recv operation which will select buffer from this group. - pub fn recv_multishot(self: *BufferGroup, user_data: u64, fd: posix.fd_t, flags: u32) !*linux.io_uring_sqe { + pub fn recv_multishot(self: *BufferGroup, user_data: u64, fd: linux.fd_t, flags: u32) !*linux.io_uring_sqe { var sqe = try self.recv(user_data, fd, flags); sqe.ioprio |= linux.IORING_RECV_MULTISHOT; return sqe; @@ -1732,7 +1732,7 @@ pub const BufferGroup = struct { /// `entries` is the number of entries requested in the buffer ring, must be power of 2. /// `group_id` is the chosen buffer group ID, unique in IO_Uring. pub fn setup_buf_ring( - fd: posix.fd_t, + fd: linux.fd_t, entries: u16, group_id: u16, flags: linux.io_uring_buf_reg.Flags, @@ -1758,7 +1758,7 @@ pub fn setup_buf_ring( } fn register_buf_ring( - fd: posix.fd_t, + fd: linux.fd_t, addr: u64, entries: u32, group_id: u16, @@ -1780,7 +1780,7 @@ fn register_buf_ring( try handle_register_buf_ring_result(res); } -fn unregister_buf_ring(fd: posix.fd_t, group_id: u16) !void { +fn unregister_buf_ring(fd: linux.fd_t, group_id: u16) !void { var reg = mem.zeroInit(linux.io_uring_buf_reg, .{ .bgid = group_id, }); @@ -1802,7 +1802,7 @@ fn handle_register_buf_ring_result(res: usize) !void { } // Unregisters a previously registered shared buffer ring, returned from io_uring_setup_buf_ring. -pub fn free_buf_ring(fd: posix.fd_t, br: *align(page_size_min) linux.io_uring_buf_ring, entries: u32, group_id: u16) void { +pub fn free_buf_ring(fd: linux.fd_t, br: *align(page_size_min) linux.io_uring_buf_ring, entries: u32, group_id: u16) void { unregister_buf_ring(fd, group_id) catch {}; var mmap: []align(page_size_min) u8 = undefined; mmap.ptr = @ptrCast(br); @@ -1873,7 +1873,7 @@ test "nop" { }; defer { ring.deinit(); - testing.expectEqual(@as(posix.fd_t, -1), ring.fd) catch @panic("test failed"); + testing.expectEqual(@as(linux.fd_t, -1), ring.fd) catch @panic("test failed"); } const sqe = try ring.nop(0xaaaaaaaa); @@ -1949,7 +1949,7 @@ test "readv" { // https://github.com/torvalds/linux/blob/v5.4/fs/io_uring.c#L3119-L3124 vs // https://github.com/torvalds/linux/blob/v5.8/fs/io_uring.c#L6687-L6691 // We therefore avoid stressing sparse fd sets here: - var registered_fds = [_]posix.fd_t{0} ** 1; + var registered_fds = [_]linux.fd_t{0} ** 1; const fd_index = 0; registered_fds[fd_index] = fd; try ring.register_files(registered_fds[0..]); @@ -2383,7 +2383,7 @@ test "sendmsg/recvmsg" { const iovecs_send = [_]posix.iovec_const{ posix.iovec_const{ .base = &buffer_send, .len = buffer_send.len }, }; - const msg_send: posix.msghdr_const = .{ + const msg_send: linux.msghdr_const = .{ .name = addrAny(&address_server), .namelen = @sizeOf(linux.sockaddr.in), .iov = &iovecs_send, @@ -2405,7 +2405,7 @@ test "sendmsg/recvmsg" { .port = 0, .addr = 0, }; - var msg_recv: posix.msghdr = .{ + var msg_recv: linux.msghdr = .{ .name = addrAny(&address_recv), .namelen = @sizeOf(linux.sockaddr.in), .iov = &iovecs_recv, @@ -2785,7 +2785,7 @@ test "register_files_update" { const fd = try posix.openZ("/dev/zero", .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0); defer posix.close(fd); - var registered_fds = [_]posix.fd_t{0} ** 2; + var registered_fds = [_]linux.fd_t{0} ** 2; const fd_index = 0; const fd_index2 = 1; registered_fds[fd_index] = fd; @@ -3764,7 +3764,7 @@ test "accept_direct" { }; // register direct file descriptors - var registered_fds = [_]posix.fd_t{-1} ** 2; + var registered_fds = [_]linux.fd_t{-1} ** 2; try ring.register_files(registered_fds[0..]); const listener_socket = try createListenerSocket(&address); @@ -3847,7 +3847,7 @@ test "accept_multishot_direct" { .addr = @bitCast([4]u8{ 127, 0, 0, 1 }), }; - var registered_fds = [_]posix.fd_t{-1} ** 2; + var registered_fds = [_]linux.fd_t{-1} ** 2; try ring.register_files(registered_fds[0..]); const listener_socket = try createListenerSocket(&address); @@ -3910,7 +3910,7 @@ test "socket" { // test completion var cqe = try ring.copy_cqe(); try testing.expectEqual(posix.E.SUCCESS, cqe.err()); - const fd: posix.fd_t = @intCast(cqe.res); + const fd: linux.fd_t = @intCast(cqe.res); try testing.expect(fd > 2); posix.close(fd); @@ -3926,7 +3926,7 @@ test "socket_direct/socket_direct_alloc/close_direct" { }; defer ring.deinit(); - var registered_fds = [_]posix.fd_t{-1} ** 3; + var registered_fds = [_]linux.fd_t{-1} ** 3; try ring.register_files(registered_fds[0..]); // create socket in registered file descriptor at index 0 (last param) @@ -4007,7 +4007,7 @@ test "openat_direct/close_direct" { }; defer ring.deinit(); - var registered_fds = [_]posix.fd_t{-1} ** 3; + var registered_fds = [_]linux.fd_t{-1} ** 3; try ring.register_files(registered_fds[0..]); var tmp = std.testing.tmpDir(.{}); @@ -4394,7 +4394,7 @@ test "ring mapped buffers multishot recv" { fn buf_grp_recv_submit_get_cqe( ring: *IoUring, buf_grp: *BufferGroup, - fd: posix.fd_t, + fd: linux.fd_t, user_data: u64, ) !linux.io_uring_cqe { // prepare and submit recv @@ -4507,7 +4507,7 @@ test "bind/listen/connect" { var cqe = try ring.copy_cqe(); try testing.expectEqual(1, cqe.user_data); try testing.expectEqual(posix.E.SUCCESS, cqe.err()); - const listen_fd: posix.fd_t = @intCast(cqe.res); + const listen_fd: linux.fd_t = @intCast(cqe.res); try testing.expect(listen_fd > 2); // Prepare: set socket option * 2, bind, listen @@ -4549,7 +4549,7 @@ test "bind/listen/connect" { try testing.expectEqual(6, cqe.user_data); try testing.expectEqual(posix.E.SUCCESS, cqe.err()); // Get connect socket fd - const connect_fd: posix.fd_t = @intCast(cqe.res); + const connect_fd: linux.fd_t = @intCast(cqe.res); try testing.expect(connect_fd > 2 and connect_fd != listen_fd); break :brk connect_fd; }; From ecdc00466c34424ec8467d05efe0421f868c739a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 20:33:34 -0700 Subject: [PATCH 194/244] std.Io.net: make it easier to use netReceiveMany correctly --- lib/std/Io/Threaded.zig | 2 +- lib/std/Io/net.zig | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index be9803adbf..bbbb6a03eb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5009,7 +5009,7 @@ fn lookupDns( } }; while (true) { - var message_buffer: [max_messages]Io.net.IncomingMessage = undefined; + var message_buffer: [max_messages]Io.net.IncomingMessage = @splat(.init); const buf = answer_buffer[answer_buffer_i..]; const recv_err, const recv_n = socket.receiveManyTimeout(t_io, &message_buffer, buf, .{}, timeout); for (message_buffer[0..recv_n]) |*received_message| { diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index fc7482f7cb..362ff424a8 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -904,10 +904,18 @@ pub const IncomingMessage = struct { data: []u8, /// Supplied by caller before calling receive functions; mutated by receive /// functions. - control: []u8 = &.{}, + control: []u8, /// Populated by receive functions. flags: Flags, + /// Useful for initializing before calling `receiveManyTimeout`. + pub const init: IncomingMessage = .{ + .from = undefined, + .data = undefined, + .control = &.{}, + .flags = undefined, + }; + pub const Flags = packed struct(u8) { /// indicates end-of-record; the data returned completed a record /// (generally used with sockets of type SOCK_SEQPACKET). @@ -1146,6 +1154,8 @@ pub const Socket = struct { pub fn receiveManyTimeout( s: *const Socket, io: Io, + /// Function assumes each element has initialized `control` field. + /// Initializing with `IncomingMessage.init` may be helpful. message_buffer: []IncomingMessage, data_buffer: []u8, flags: ReceiveFlags, From ae86c0f529d720ed4cae9e5f99064d6dba635144 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 20:55:46 -0700 Subject: [PATCH 195/244] std.Io: adjust concurrent error set Now std.Io.Threaded can return error.ConcurrencyUnavailable rather than asserting. This is handy for logic that wants to try a concurrent implementation but then fall back to a synchronous one. --- lib/std/Io.zig | 12 ++++++++++-- lib/std/Io/IoUring.zig | 2 +- lib/std/Io/Kqueue.zig | 4 ++-- lib/std/Io/Threaded.zig | 18 ++++++++++-------- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index d89f2d23af..45736d39c4 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -552,6 +552,8 @@ test { _ = Reader; _ = Writer; _ = tty; + _ = Evented; + _ = Threaded; _ = @import("Io/test.zig"); } @@ -596,7 +598,7 @@ pub const VTable = struct { context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, - ) error{OutOfMemory}!*AnyFuture, + ) ConcurrentError!*AnyFuture, /// This function is only called when `async` returns a non-null value. /// /// Thread-safe. @@ -1557,6 +1559,12 @@ pub fn async( return future; } +pub const ConcurrentError = error{ + /// May occur due to a temporary condition such as resource exhaustion, or + /// to the Io implementation not supporting concurrency. + ConcurrencyUnavailable, +}; + /// Calls `function` with `args`, such that the return value of the function is /// not guaranteed to be available until `await` is called, allowing the caller /// to progress while waiting for any `Io` operations. @@ -1568,7 +1576,7 @@ pub fn concurrent( io: Io, function: anytype, args: std.meta.ArgsTuple(@TypeOf(function)), -) error{OutOfMemory}!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { +) ConcurrentError!Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { const Result = @typeInfo(@TypeOf(function)).@"fn".return_type.?; const Args = @TypeOf(args); const TypeErased = struct { diff --git a/lib/std/Io/IoUring.zig b/lib/std/Io/IoUring.zig index 9ec1dafb31..5561cdebd2 100644 --- a/lib/std/Io/IoUring.zig +++ b/lib/std/Io/IoUring.zig @@ -866,7 +866,7 @@ fn concurrent( context: []const u8, context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) error{OutOfMemory}!*std.Io.AnyFuture { +) Io.ConcurrentError!*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 diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index b41a0260e0..fd5baaddde 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -933,14 +933,14 @@ fn concurrent( context: []const u8, context_alignment: Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) error{OutOfMemory}!*Io.AnyFuture { +) Io.ConcurrentError!*Io.AnyFuture { const k: *Kqueue = @ptrCast(@alignCast(userdata)); 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 fiber = try Fiber.allocate(k); + const fiber = Fiber.allocate(k) catch return error.ConcurrencyUnavailable; std.log.debug("allocated {*}", .{fiber}); const closure: *AsyncClosure = .fromFiber(fiber); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index bbbb6a03eb..da8b7fb211 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -91,9 +91,9 @@ pub fn init( return t; } -/// Statically initialize such that any call to the following functions will -/// fail with `error.OutOfMemory`: -/// * `Io.VTable.concurrent` +/// Statically initialize such that calls to `Io.VTable.concurrent` will fail +/// with `error.ConcurrencyUnavailable`. +/// /// When initialized this way, `deinit` is safe, but unnecessary to call. pub const init_single_threaded: Threaded = .{ .allocator = .failing, @@ -481,8 +481,8 @@ fn concurrent( context: []const u8, context_alignment: std.mem.Alignment, start: *const fn (context: *const anyopaque, result: *anyopaque) void, -) error{OutOfMemory}!*Io.AnyFuture { - if (builtin.single_threaded) unreachable; +) Io.ConcurrentError!*Io.AnyFuture { + if (builtin.single_threaded) return error.ConcurrencyUnavailable; const t: *Threaded = @ptrCast(@alignCast(userdata)); const cpu_count = t.cpu_count catch 1; @@ -490,7 +490,9 @@ fn concurrent( const context_offset = context_alignment.forward(@sizeOf(AsyncClosure)); const result_offset = result_alignment.forward(context_offset + context.len); const n = result_offset + result_len; - const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), n))); + const ac_bytes = gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch + return error.ConcurrencyUnavailable; + const ac: *AsyncClosure = @ptrCast(@alignCast(ac_bytes)); ac.* = .{ .closure = .{ @@ -515,7 +517,7 @@ fn concurrent( t.threads.ensureTotalCapacity(gpa, thread_capacity) catch { t.mutex.unlock(); ac.free(gpa, result_len); - return error.OutOfMemory; + return error.ConcurrencyUnavailable; }; t.run_queue.prepend(&ac.closure.node); @@ -525,7 +527,7 @@ fn concurrent( assert(t.run_queue.popFirst() == &ac.closure.node); t.mutex.unlock(); ac.free(gpa, result_len); - return error.OutOfMemory; + return error.ConcurrencyUnavailable; }; t.threads.appendAssumeCapacity(thread); } From e6b4e1a5d0bb7aa8360060ca88cc10a7edc7df5e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 23 Oct 2025 21:47:45 -0700 Subject: [PATCH 196/244] disable self-hosted wasm test-cases Tracked by #25684 --- test/src/Cases.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/src/Cases.zig b/test/src/Cases.zig index 9f134075b4..2fdcc45195 100644 --- a/test/src/Cases.zig +++ b/test/src/Cases.zig @@ -370,6 +370,10 @@ fn addFromDirInner( const resolved_target = b.resolveTargetQuery(target_query); const target = &resolved_target.result; for (backends) |backend| { + if (backend == .selfhosted and target.cpu.arch == .wasm32) { + // https://github.com/ziglang/zig/issues/25684 + continue; + } if (backend == .selfhosted and target.cpu.arch != .aarch64 and target.cpu.arch != .wasm32 and target.cpu.arch != .x86_64 and target.cpu.arch != .spirv64) { From ed7067a690444a362d289fe5b0a75eefa0de4f08 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 24 Oct 2025 06:23:07 -0700 Subject: [PATCH 197/244] std.Io: more convenient sleep --- lib/std/Io.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 45736d39c4..1649341afc 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -1604,6 +1604,13 @@ pub fn cancelRequested(io: Io) bool { pub const SleepError = error{UnsupportedClock} || UnexpectedError || Cancelable; +pub fn sleep(io: Io, duration: Duration, clock: Clock) SleepError!void { + return io.vtable.sleep(io.userdata, .{ .duration = .{ + .raw = duration, + .clock = clock, + } }); +} + /// Given a struct with each field a `*Future`, returns a union with the same /// fields, each field type the future's result. pub fn SelectUnion(S: type) type { From 85e159e652afe976a6f720e041e88b196f062a9f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 24 Oct 2025 06:23:23 -0700 Subject: [PATCH 198/244] std.Io.Threaded: closures must always be run even when canceled --- lib/std/Io/Threaded.zig | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index da8b7fb211..a7ab4f2f91 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -337,8 +337,6 @@ const AsyncClosure = struct { select_condition: ?*ResetEvent, context_alignment: std.mem.Alignment, result_offset: usize, - /// Whether the task has a return type with nonzero bits. - has_result: bool, const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent)); @@ -348,12 +346,8 @@ const AsyncClosure = struct { if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, 0, tid, .acq_rel, .acquire)) |cancel_tid| { assert(cancel_tid == Closure.canceling_tid); // Even though we already know the task is canceled, we must still - // run the closure in order to make the return value valid - that - // is, unless the result is zero bytes! - if (!ac.has_result) { - ac.reset_event.set(); - return; - } + // run the closure in order to make the return value valid and in + // case there are side effects. } current_closure = closure; ac.func(ac.contextPointer(), ac.resultPointer()); @@ -389,7 +383,6 @@ const AsyncClosure = struct { } fn free(ac: *AsyncClosure, gpa: Allocator, result_len: usize) void { - if (!ac.has_result) assert(result_len == 0); const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac); gpa.free(base[0 .. ac.result_offset + result_len]); } @@ -432,7 +425,6 @@ fn async( .func = start, .context_alignment = context_alignment, .result_offset = result_offset, - .has_result = result.len != 0, .reset_event = .unset, .select_condition = null, }; @@ -503,7 +495,6 @@ fn concurrent( .func = start, .context_alignment = context_alignment, .result_offset = result_offset, - .has_result = result_len != 0, .reset_event = .unset, .select_condition = null, }; From a8f95e5176ebd734ccd2fd4d92cced6ab4cc2c07 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 24 Oct 2025 13:05:13 -0700 Subject: [PATCH 199/244] std.Io.Threaded: implement cancellation for pthreads not to be confused with pthread_cancel, which is a useless API. --- lib/std/Io/Threaded.zig | 73 +++++++++++++++++++++++++---------------- lib/std/c.zig | 2 ++ 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a7ab4f2f91..9aef333e8a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -36,30 +36,47 @@ comptime { if (@TypeOf(posix.IOV_MAX) != void) assert(max_iovecs_len <= posix.IOV_MAX); } +const CancelId = enum(usize) { + none = 0, + canceling = std.math.maxInt(usize), + _, + + const ThreadId = if (std.Thread.use_pthreads) std.c.pthread_t else std.Thread.Id; + + fn currentThread() CancelId { + if (std.Thread.use_pthreads) { + return @enumFromInt(@intFromPtr(std.c.pthread_self())); + } else { + return @enumFromInt(std.Thread.getCurrentId()); + } + } + + fn toThreadId(cancel_id: CancelId) ThreadId { + if (std.Thread.use_pthreads) { + return @ptrFromInt(@intFromEnum(cancel_id)); + } else { + return @intCast(@intFromEnum(cancel_id)); + } + } +}; + const Closure = struct { start: Start, node: std.SinglyLinkedList.Node = .{}, - cancel_tid: std.Thread.Id, + cancel_tid: CancelId, /// Whether this task bumps minimum number of threads in the pool. is_concurrent: bool, const Start = *const fn (*Closure) void; - const canceling_tid: std.Thread.Id = switch (@typeInfo(std.Thread.Id)) { - .int => |int_info| switch (int_info.signedness) { - .signed => -1, - .unsigned => std.math.maxInt(std.Thread.Id), - }, - .pointer => @ptrFromInt(std.math.maxInt(usize)), - else => @compileError("unsupported std.Thread.Id: " ++ @typeName(std.Thread.Id)), - }; - fn requestCancel(closure: *Closure) void { - switch (@atomicRmw(std.Thread.Id, &closure.cancel_tid, .Xchg, canceling_tid, .acq_rel)) { - 0, canceling_tid => {}, + switch (@atomicRmw(CancelId, &closure.cancel_tid, .Xchg, .canceling, .acq_rel)) { + .none, .canceling => {}, else => |tid| switch (native_os) { - .linux => _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid), posix.SIG.IO), - else => {}, + .linux => _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid.toThreadId()), posix.SIG.IO), + else => if (std.Thread.use_pthreads) { + assert(std.c.pthread_kill(tid.toThreadId(), posix.SIG.IO) == 0); + }, }, } } @@ -342,9 +359,9 @@ const AsyncClosure = struct { fn start(closure: *Closure) void { const ac: *AsyncClosure = @alignCast(@fieldParentPtr("closure", closure)); - const tid = std.Thread.getCurrentId(); - if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, 0, tid, .acq_rel, .acquire)) |cancel_tid| { - assert(cancel_tid == Closure.canceling_tid); + const tid: CancelId = .currentThread(); + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, .none, tid, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); // Even though we already know the task is canceled, we must still // run the closure in order to make the return value valid and in // case there are side effects. @@ -355,8 +372,8 @@ const AsyncClosure = struct { // In case a cancel happens after successful task completion, prevents // signal from being delivered to the thread in `requestCancel`. - if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, tid, 0, .acq_rel, .acquire)) |cancel_tid| { - assert(cancel_tid == Closure.canceling_tid); + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, tid, .none, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); } if (@atomicRmw(?*ResetEvent, &ac.select_condition, .Xchg, done_reset_event, .release)) |select_reset| { @@ -418,7 +435,7 @@ fn async( ac.* = .{ .closure = .{ - .cancel_tid = 0, + .cancel_tid = .none, .start = AsyncClosure.start, .is_concurrent = false, }, @@ -488,7 +505,7 @@ fn concurrent( ac.* = .{ .closure = .{ - .cancel_tid = 0, + .cancel_tid = .none, .start = AsyncClosure.start, .is_concurrent = true, }, @@ -540,12 +557,12 @@ const GroupClosure = struct { fn start(closure: *Closure) void { const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure)); - const tid = std.Thread.getCurrentId(); + const tid: CancelId = .currentThread(); const group = gc.group; const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); - if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, 0, tid, .acq_rel, .acquire)) |cancel_tid| { - assert(cancel_tid == Closure.canceling_tid); + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, .none, tid, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); // We already know the task is canceled before running the callback. Since all closures // in a Group have void return type, we can return early. syncFinish(group_state, reset_event); @@ -557,8 +574,8 @@ const GroupClosure = struct { // In case a cancel happens after successful task completion, prevents // signal from being delivered to the thread in `requestCancel`. - if (@cmpxchgStrong(std.Thread.Id, &closure.cancel_tid, tid, 0, .acq_rel, .acquire)) |cancel_tid| { - assert(cancel_tid == Closure.canceling_tid); + if (@cmpxchgStrong(CancelId, &closure.cancel_tid, tid, .none, .acq_rel, .acquire)) |cancel_tid| { + assert(cancel_tid == .canceling); } syncFinish(group_state, reset_event); @@ -626,7 +643,7 @@ fn groupAsync( })); gc.* = .{ .closure = .{ - .cancel_tid = 0, + .cancel_tid = .none, .start = GroupClosure.start, .is_concurrent = false, }, @@ -771,7 +788,7 @@ fn cancelRequested(userdata: ?*anyopaque) bool { const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; const closure = current_closure orelse return false; - return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == Closure.canceling_tid; + return @atomicLoad(CancelId, &closure.cancel_tid, .acquire) == .canceling; } fn checkCancel(t: *Threaded) error{Canceled}!void { diff --git a/lib/std/c.zig b/lib/std/c.zig index 877b9ae4e3..024c76ddc4 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -10763,6 +10763,8 @@ pub const pthread_setname_np = switch (native_os) { }; pub extern "c" fn pthread_getname_np(thread: pthread_t, name: [*:0]u8, len: usize) c_int; +pub extern "c" fn pthread_kill(pthread_t, signal: c_int) c_int; + pub const pthread_threadid_np = switch (native_os) { .macos, .ios, .tvos, .watchos, .visionos => private.pthread_threadid_np, else => {}, From 6cff32c7ee5be4e10ea768a20c724e348fc90ba3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 26 Oct 2025 12:04:52 -0700 Subject: [PATCH 200/244] std.Io.Threaded: implement netRead for Windows --- lib/std/Io/Threaded.zig | 94 +++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 14 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 9aef333e8a..563e634162 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2976,7 +2976,7 @@ fn netListenIpWindows( if (rc != ws2_32.SOCKET_ERROR) break; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -2998,7 +2998,7 @@ fn netListenIpWindows( if (rc != ws2_32.SOCKET_ERROR) break; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3231,7 +3231,7 @@ fn wsaGetSockName(t: *Threaded, handle: ws2_32.SOCKET, addr: *ws2_32.sockaddr, a if (rc != ws2_32.SOCKET_ERROR) break; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3270,7 +3270,7 @@ fn setSocketOptionWsa(t: *Threaded, socket: Io.net.Socket.Handle, level: i32, op if (rc != ws2_32.SOCKET_ERROR) return; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3331,7 +3331,7 @@ fn netConnectIpWindows( if (rc != ws2_32.SOCKET_ERROR) break; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3454,7 +3454,7 @@ fn netBindIpWindows( if (rc != ws2_32.SOCKET_ERROR) break; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3560,7 +3560,7 @@ fn openSocketWsa(t: *Threaded, family: posix.sa_family_t, options: IpAddress.Bin if (rc != ws2_32.INVALID_SOCKET) return rc; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3639,7 +3639,7 @@ fn netAcceptWindows(userdata: ?*anyopaque, listen_handle: net.Socket.Handle) net } }; switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -3728,10 +3728,76 @@ fn netReadPosix(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net. fn netReadWindows(userdata: ?*anyopaque, handle: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { if (!have_networking) return error.NetworkDown; const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; - _ = handle; - _ = data; - @panic("TODO implement netReadWindows"); + + const bufs = b: { + var iovec_buffer: [max_iovecs_len]ws2_32.WSABUF = undefined; + var i: usize = 0; + var n: usize = 0; + for (data) |buf| { + if (iovec_buffer.len - i == 0) break; + if (buf.len == 0) continue; + if (std.math.cast(u32, buf.len)) |len| { + iovec_buffer[i] = .{ .buf = buf.ptr, .len = len }; + i += 1; + n += len; + continue; + } + iovec_buffer[i] = .{ .buf = buf.ptr, .len = std.math.maxInt(u32) }; + i += 1; + n += std.math.maxInt(u32); + break; + } + + const bufs = iovec_buffer[0..i]; + assert(bufs[0].len != 0); + + break :b bufs; + }; + + while (true) { + try t.checkCancel(); + + var flags: u32 = 0; + var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); + var n: u32 = undefined; + const rc = ws2_32.WSARecv(handle, bufs.ptr, @intCast(bufs.len), &n, &flags, &overlapped, null); + if (rc != ws2_32.SOCKET_ERROR) return n; + const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) { + .IO_PENDING => e: { + var result_flags: u32 = undefined; + const overlapped_rc = ws2_32.WSAGetOverlappedResult( + handle, + &overlapped, + &n, + windows.TRUE, + &result_flags, + ); + if (overlapped_rc == windows.FALSE) { + break :e ws2_32.WSAGetLastError(); + } else { + return n; + } + }, + else => |err| err, + }; + switch (wsa_error) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .ECONNRESET => return error.ConnectionResetByPeer, + .EFAULT => unreachable, // a pointer is not completely contained in user address space. + .EINVAL => |err| return wsaErrorBug(err), + .EMSGSIZE => |err| return wsaErrorBug(err), + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENOTCONN => return error.SocketUnconnected, + else => |err| return windows.unexpectedWSAError(err), + } + } } fn netReadUnavailable(userdata: ?*anyopaque, fd: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize { @@ -3823,7 +3889,7 @@ fn netSendOne( if (rc == ws2_32.SOCKET_ERROR) { switch (ws2_32.WSAGetLastError()) { .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; @@ -4422,7 +4488,7 @@ fn netLookupFallible( switch (rc) { @as(ws2_32.WinsockError, @enumFromInt(0)) => break, .EINTR => continue, - .ECANCELLED, .E_CANCELLED => return error.Canceled, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, .NOTINITIALISED => { try initializeWsa(t); continue; From fd7475c8b2e4ee0d3a12f8517720facc407379b5 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 26 Oct 2025 12:05:10 -0700 Subject: [PATCH 201/244] std.Io.Threaded: implement netWrite for Windows --- lib/std/Io/Threaded.zig | 103 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 8 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 563e634162..cd885ea4c5 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -3909,8 +3909,7 @@ fn netSendOne( .ENETRESET => return error.ConnectionResetByPeer, .ENETUNREACH => return error.NetworkUnreachable, .ENOTCONN => return error.SocketUnconnected, - .ESHUTDOWN => unreachable, // The socket has been shut down; it is not possible to WSASendTo on a socket after shutdown has been invoked with how set to SD_SEND or SD_BOTH. - .NOTINITIALISED => unreachable, // A successful WSAStartup call must occur before using this function. + .ESHUTDOWN => |err| return wsaErrorBug(err), else => |err| return windows.unexpectedWSAError(err), } } else { @@ -4275,12 +4274,100 @@ fn netWriteWindows( splat: usize, ) net.Stream.Writer.Error!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; - _ = handle; - _ = header; - _ = data; - _ = splat; - @panic("TODO implement netWriteWindows"); + comptime assert(native_os == .windows); + + var iovecs: [max_iovecs_len]ws2_32.WSABUF = undefined; + var len: u32 = 0; + addWsaBuf(&iovecs, &len, header); + for (data[0 .. data.len - 1]) |bytes| addWsaBuf(&iovecs, &len, bytes); + const pattern = data[data.len - 1]; + if (iovecs.len - len != 0) switch (splat) { + 0 => {}, + 1 => addWsaBuf(&iovecs, &len, pattern), + else => switch (pattern.len) { + 0 => {}, + 1 => { + var backup_buffer: [64]u8 = undefined; + const splat_buffer = &backup_buffer; + const memset_len = @min(splat_buffer.len, splat); + const buf = splat_buffer[0..memset_len]; + @memset(buf, pattern[0]); + addWsaBuf(&iovecs, &len, buf); + var remaining_splat = splat - buf.len; + while (remaining_splat > splat_buffer.len and len < iovecs.len) { + addWsaBuf(&iovecs, &len, splat_buffer); + remaining_splat -= splat_buffer.len; + } + addWsaBuf(&iovecs, &len, splat_buffer[0..remaining_splat]); + }, + else => for (0..@min(splat, iovecs.len - len)) |_| { + addWsaBuf(&iovecs, &len, pattern); + }, + }, + }; + + while (true) { + try t.checkCancel(); + + var n: u32 = undefined; + var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); + const rc = ws2_32.WSASend(handle, &iovecs, len, &n, 0, &overlapped, null); + if (rc != ws2_32.SOCKET_ERROR) return n; + const wsa_error: ws2_32.WinsockError = switch (ws2_32.WSAGetLastError()) { + .IO_PENDING => e: { + var result_flags: u32 = undefined; + const overlapped_rc = ws2_32.WSAGetOverlappedResult( + handle, + &overlapped, + &n, + windows.TRUE, + &result_flags, + ); + if (overlapped_rc == windows.FALSE) { + break :e ws2_32.WSAGetLastError(); + } else { + return n; + } + }, + else => |err| err, + }; + switch (wsa_error) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .ECONNABORTED => return error.ConnectionResetByPeer, + .ECONNRESET => return error.ConnectionResetByPeer, + .EINVAL => return error.SocketUnconnected, + .ENETDOWN => return error.NetworkDown, + .ENETRESET => return error.ConnectionResetByPeer, + .ENOBUFS => return error.SystemResources, + .ENOTCONN => return error.SocketUnconnected, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EOPNOTSUPP => |err| return wsaErrorBug(err), + .ESHUTDOWN => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } +} + +fn addWsaBuf(v: []ws2_32.WSABUF, i: *u32, bytes: []const u8) void { + const cap = std.math.maxInt(u32); + var remaining = bytes; + while (remaining.len > cap) { + if (v.len - i.* == 0) return; + v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = cap }; + i.* += 1; + remaining = remaining[cap..]; + } else { + @branchHint(.likely); + if (v.len - i.* == 0) return; + v[i.*] = .{ .buf = @constCast(remaining.ptr), .len = @intCast(remaining.len) }; + i.* += 1; + } } fn netWriteUnavailable( From f6c5525c8476a1a11c7ebbcdca764010eb10f9a6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 07:32:06 -0700 Subject: [PATCH 202/244] std.Io.Threaded: fix compilation on pthreads linux --- lib/std/Io/Threaded.zig | 44 +++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index cd885ea4c5..46ca70beb2 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5,6 +5,7 @@ const native_os = builtin.os.tag; const is_windows = native_os == .windows; const windows = std.os.windows; const ws2_32 = std.os.windows.ws2_32; +const is_debug = builtin.mode == .Debug; const std = @import("../std.zig"); const Io = std.Io; @@ -72,11 +73,13 @@ const Closure = struct { fn requestCancel(closure: *Closure) void { switch (@atomicRmw(CancelId, &closure.cancel_tid, .Xchg, .canceling, .acq_rel)) { .none, .canceling => {}, - else => |tid| switch (native_os) { - .linux => _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid.toThreadId()), posix.SIG.IO), - else => if (std.Thread.use_pthreads) { - assert(std.c.pthread_kill(tid.toThreadId(), posix.SIG.IO) == 0); - }, + else => |tid| { + if (std.Thread.use_pthreads) { + const rc = std.c.pthread_kill(tid.toThreadId(), posix.SIG.IO); + if (is_debug) assert(rc == 0); + } else if (native_os == .linux) { + _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid.toThreadId()), posix.SIG.IO); + } }, } } @@ -4883,17 +4886,13 @@ fn address6ToPosix(a: *const net.Ip6Address) posix.sockaddr.in6 { } pub fn errnoBug(err: posix.E) Io.UnexpectedError { - switch (builtin.mode) { - .Debug => std.debug.panic("programmer bug caused syscall error: {t}", .{err}), - else => return error.Unexpected, - } + if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err}); + return error.Unexpected; } fn wsaErrorBug(err: ws2_32.WinsockError) Io.UnexpectedError { - switch (builtin.mode) { - .Debug => std.debug.panic("programmer bug caused syscall error: {t}", .{err}), - else => return error.Unexpected, - } + if (is_debug) std.debug.panic("programmer bug caused syscall error: {t}", .{err}); + return error.Unexpected; } pub fn posixSocketMode(mode: net.Socket.Mode) u32 { @@ -4911,7 +4910,7 @@ pub fn posixProtocol(protocol: ?net.Protocol) u32 { } fn recoverableOsBugDetected() void { - if (builtin.mode == .Debug) unreachable; + if (is_debug) unreachable; } fn clockToPosix(clock: Io.Clock) posix.clockid_t { @@ -5424,7 +5423,7 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca const linux = std.os.linux; try t.checkCancel(); const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); - if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + if (is_debug) switch (linux.E.init(rc)) { .SUCCESS => {}, // notified by `wake()` .INTR => {}, // gives caller a chance to check cancellation .AGAIN => {}, // ptr.* != expect @@ -5447,7 +5446,7 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca if (status >= 0) return; - if (builtin.mode == .Debug) switch (@as(c.E, @enumFromInt(-status))) { + if (is_debug) switch (@as(c.E, @enumFromInt(-status))) { // Wait was interrupted by the OS or other spurious signalling. .INTR => {}, // Address of the futex was paged out. This is unlikely, but possible in theory, and @@ -5473,7 +5472,6 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca [expected] "r" (signed_expect), [timeout] "r" (timeout), ); - const is_debug = builtin.mode == .Debug; switch (result) { 0 => {}, // ok 1 => {}, // expected != loaded @@ -5498,7 +5496,7 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi if (native_os == .linux) { const linux = std.os.linux; const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); - if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + if (is_debug) switch (linux.E.init(rc)) { .SUCCESS => {}, // notified by `wake()` .INTR => {}, // gives caller a chance to check cancellation .AGAIN => {}, // ptr.* != expect @@ -5520,7 +5518,7 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi if (status >= 0) return; - if (builtin.mode == .Debug) switch (@as(c.E, @enumFromInt(-status))) { + if (is_debug) switch (@as(c.E, @enumFromInt(-status))) { // Wait was interrupted by the OS or other spurious signalling. .INTR => {}, // Address of the futex was paged out. This is unlikely, but possible in theory, and @@ -5545,7 +5543,6 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi [expected] "r" (signed_expect), [timeout] "r" (timeout), ); - const is_debug = builtin.mode == .Debug; switch (result) { 0 => {}, // ok 1 => {}, // expected != loaded @@ -5569,7 +5566,7 @@ pub fn futexWaitDurationUncancelable(ptr: *const std.atomic.Value(u32), expect: const linux = std.os.linux; var ts = timestampToPosix(timeout.toNanoseconds()); const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, &ts); - if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + if (is_debug) switch (linux.E.init(rc)) { .SUCCESS => {}, // notified by `wake()` .INTR => {}, // gives caller a chance to check cancellation .AGAIN => {}, // ptr.* != expect @@ -5594,7 +5591,7 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { .{ .cmd = .WAKE, .private = true }, @min(max_waiters, std.math.maxInt(i32)), ); - if (builtin.mode == .Debug) switch (linux.E.init(rc)) { + if (is_debug) switch (linux.E.init(rc)) { .SUCCESS => {}, // successful wake up .INVAL => {}, // invalid futex_wait() on ptr done elsewhere .FAULT => {}, // pointer became invalid while doing the wake @@ -5607,7 +5604,6 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { .NO_ERRNO = true, .WAKE_ALL = max_waiters > 1, }; - const is_debug = builtin.mode == .Debug; while (true) { const status = c.__ulock_wake(flags, ptr, 0); if (status >= 0) return; @@ -5756,7 +5752,7 @@ pub const ResetEvent = enum(u32) { fn closeSocketWindows(s: ws2_32.SOCKET) void { const rc = ws2_32.closesocket(s); - if (builtin.mode == .Debug) switch (rc) { + if (is_debug) switch (rc) { 0 => {}, ws2_32.SOCKET_ERROR => switch (ws2_32.WSAGetLastError()) { else => recoverableOsBugDetected(), From f9de83c90ee7bcc13e78f0998ae1a9a46bbeb67e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 08:29:26 -0700 Subject: [PATCH 203/244] std.Io.net: skip testing netInterfaceNameResolve on Windows let's handle this in a follow-up change. implementation needs to use ConvertInterfaceNameToLuidW and the additional dependency on Iphlpapi.dll poses some challenges. --- lib/std/Io/net/test.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/net/test.zig b/lib/std/Io/net/test.zig index 65f857ea46..60ca66349f 100644 --- a/lib/std/Io/net/test.zig +++ b/lib/std/Io/net/test.zig @@ -86,10 +86,8 @@ test "IPv6 address parse failures" { test "invalid but parseable IPv6 scope ids" { const io = testing.io; - if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin() and builtin.os.tag != .windows) { - // Currently, resolveIp6 with alphanumerical scope IDs only works on Linux. - // TODO Make this test pass on other operating systems. - return error.SkipZigTest; + if (builtin.os.tag != .linux and comptime !builtin.os.tag.isDarwin()) { + return error.SkipZigTest; // TODO } try testing.expectError(error.InterfaceNotFound, net.IpAddress.resolveIp6(io, "ff01::fb%123s45678901234", 0)); From 441d0c4272e42b35951cc5e0bcfd2139f73edec8 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 09:32:54 -0700 Subject: [PATCH 204/244] std.Io.net.HostName: fix missing group cancel --- lib/std/Io/net/HostName.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index ea1ffc4834..9a10b36239 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -269,6 +269,7 @@ pub fn connectMany( var lookup_buffer: [32]HostName.LookupResult = undefined; var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer); var group: Io.Group = .init; + defer group.cancel(io); group.async(io, lookup, .{ host_name, io, &lookup_queue, .{ .port = port, From 6ccb53bff130a473f9d2cc3d0c1ad3a8c6399b2d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 10:07:29 -0700 Subject: [PATCH 205/244] std.Io.Threaded: fix openSelfExe for Windows missing a call to wToPrefixedFileW --- lib/std/Io/Threaded.zig | 121 +++++++++++++++++++++++++------- lib/std/os/windows.zig | 2 +- lib/std/os/windows/kernel32.zig | 7 +- 3 files changed, 101 insertions(+), 29 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 46ca70beb2..7b5d765bbf 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2044,26 +2044,97 @@ fn dirOpenFileWindows( const t: *Threaded = @ptrCast(@alignCast(userdata)); const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); - return dirOpenFileWindowsInner(t, dir, sub_path_w, flags); + const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; + return dirOpenFileWindowsInner(t, dir_handle, sub_path_w, flags); } fn dirOpenFileWindowsInner( t: *Threaded, - dir: Io.Dir, + dir_handle: ?windows.HANDLE, sub_path_w: [:0]const u16, flags: Io.File.OpenFlags, ) Io.File.OpenError!Io.File { - try t.checkCancel(); + if (std.mem.eql(u16, sub_path_w, &.{'.'})) return error.IsDir; + if (std.mem.eql(u16, sub_path_w, &.{ '.', '.' })) return error.IsDir; + const path_len_bytes = std.math.cast(u16, sub_path_w.len * 2) orelse return error.NameTooLong; + const w = windows; - const handle = try w.OpenFile(sub_path_w, .{ - .dir = dir.handle, - .access_mask = w.SYNCHRONIZE | - (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | - (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), - .creation = w.FILE_OPEN, - }); - errdefer w.CloseHandle(handle); + + var nt_name: w.UNICODE_STRING = .{ + .Length = path_len_bytes, + .MaximumLength = path_len_bytes, + .Buffer = @constCast(sub_path_w.ptr), + }; + var attr: w.OBJECT_ATTRIBUTES = .{ + .Length = @sizeOf(w.OBJECT_ATTRIBUTES), + .RootDirectory = dir_handle, + .Attributes = 0, + .ObjectName = &nt_name, + .SecurityDescriptor = null, + .SecurityQualityOfService = null, + }; var io_status_block: w.IO_STATUS_BLOCK = undefined; + const blocking_flag: w.ULONG = w.FILE_SYNCHRONOUS_IO_NONALERT; + const file_or_dir_flag: w.ULONG = w.FILE_NON_DIRECTORY_FILE; + // If we're not following symlinks, we need to ensure we don't pass in any + // synchronization flags such as FILE_SYNCHRONOUS_IO_NONALERT. + const create_file_flags: w.ULONG = file_or_dir_flag | + if (flags.follow_symlinks) blocking_flag else w.FILE_OPEN_REPARSE_POINT; + + const handle = while (true) { + try t.checkCancel(); + + var result: w.HANDLE = undefined; + const rc = w.ntdll.NtCreateFile( + &result, + w.SYNCHRONIZE | + (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | + (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), + &attr, + &io_status_block, + null, + w.FILE_ATTRIBUTE_NORMAL, + w.FILE_SHARE_WRITE | w.FILE_SHARE_READ | w.FILE_SHARE_DELETE, + w.FILE_OPEN, + create_file_flags, + null, + 0, + ); + switch (rc) { + .SUCCESS => break result, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .BAD_NETWORK_PATH => return error.NetworkNotFound, // \\server was not found + .BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't + .NO_MEDIA_IN_DEVICE => return error.NoDevice, + .INVALID_PARAMETER => |err| return w.statusBug(err), + .SHARING_VIOLATION => return error.AccessDenied, + .ACCESS_DENIED => return error.AccessDenied, + .PIPE_BUSY => return error.PipeBusy, + .PIPE_NOT_AVAILABLE => return error.NoDevice, + .OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err), + .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, + .FILE_IS_A_DIRECTORY => return error.IsDir, + .NOT_A_DIRECTORY => return error.NotDir, + .USER_MAPPED_FILE => return error.AccessDenied, + .INVALID_HANDLE => |err| return w.statusBug(err), + .DELETE_PENDING => { + // This error means that there *was* a file in this location on + // the file system, but it was deleted. However, the OS is not + // finished with the deletion operation, and so this CreateFile + // call has failed. There is not really a sane way to handle + // this other than retrying the creation after the OS finishes + // the deletion. + _ = w.kernel32.SleepEx(1, w.FALSE); + continue; + }, + .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, + else => return w.unexpectedStatus(rc), + } + }; + errdefer w.CloseHandle(handle); + const range_off: w.LARGE_INTEGER = 0; const range_len: w.LARGE_INTEGER = 1; const exclusive = switch (flags.lock) { @@ -2691,20 +2762,19 @@ fn fileSeekTo(userdata: ?*anyopaque, file: Io.File, offset: u64) Io.File.SeekErr fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelfExeError!Io.File { const t: *Threaded = @ptrCast(@alignCast(userdata)); - if (native_os == .linux or native_os == .serenity) { - return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags); + switch (native_os) { + .linux, .serenity => return dirOpenFilePosix(t, .{ .handle = posix.AT.FDCWD }, "/proc/self/exe", flags), + .windows => { + // If ImagePathName is a symlink, then it will contain the path of the symlink, + // not the path that the symlink points to. However, because we are opening + // the file, we can let the openFileW call follow the symlink for us. + const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; + const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; + const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); + return dirOpenFileWindowsInner(t, null, prefixed_path_w.span(), flags); + }, + else => @panic("TODO implement openSelfExe"), } - if (is_windows) { - // If ImagePathName is a symlink, then it will contain the path of the symlink, - // not the path that the symlink points to. However, because we are opening - // the file, we can let the openFileW call follow the symlink for us. - const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; - const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; - const cwd_handle = std.os.windows.peb().ProcessParameters.CurrentDirectory.Handle; - - return dirOpenFileWindowsInner(t, .{ .handle = cwd_handle }, image_path_name, flags); - } - @panic("TODO implement openSelfExe"); } fn fileWritePositional( @@ -2823,7 +2893,8 @@ fn sleepWindows(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { break :ms std.math.maxInt(windows.DWORD); break :ms std.math.lossyCast(windows.DWORD, d.raw.toMilliseconds()); }; - windows.kernel32.Sleep(ms); + // TODO: alertable true with checkCancel in a loop plus deadline + _ = windows.kernel32.SleepEx(ms, windows.FALSE); } fn sleepWasi(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void { diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 26e1b7cfaa..19b6f8169d 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -148,7 +148,7 @@ pub fn OpenFile(sub_path_w: []const u16, options: OpenFileOptions) OpenError!HAN // call has failed. There is not really a sane way to handle // this other than retrying the creation after the OS finishes // the deletion. - kernel32.Sleep(1); + _ = kernel32.SleepEx(1, TRUE); continue; }, .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, diff --git a/lib/std/os/windows/kernel32.zig b/lib/std/os/windows/kernel32.zig index 2ef08dd97a..c93304d82b 100644 --- a/lib/std/os/windows/kernel32.zig +++ b/lib/std/os/windows/kernel32.zig @@ -326,10 +326,11 @@ pub extern "kernel32" fn ExitProcess( exit_code: UINT, ) callconv(.winapi) noreturn; -// TODO: SleepEx with bAlertable=false. -pub extern "kernel32" fn Sleep( +// TODO: implement via ntdll instead +pub extern "kernel32" fn SleepEx( dwMilliseconds: DWORD, -) callconv(.winapi) void; + bAlertable: BOOL, +) callconv(.winapi) DWORD; // TODO: Wrapper around NtQueryInformationProcess with `PROCESS_BASIC_INFORMATION`. pub extern "kernel32" fn GetExitCodeProcess( From 02119362d70bb16ae6ab00124928bde89834c83f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 12:11:10 -0700 Subject: [PATCH 206/244] wasm linking: handle unreachable call_indirect The compiler crashed when we tried to call a function pointer for which the type signature does not match any function body or function import in the entire wasm executable, because there is no way to create a reference to a function without it being in the function table or import table. Solution is to make this instruction lower to unreachable. --- src/codegen/wasm/Emit.zig | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/codegen/wasm/Emit.zig b/src/codegen/wasm/Emit.zig index 272d5519a6..e18bd12213 100644 --- a/src/codegen/wasm/Emit.zig +++ b/src/codegen/wasm/Emit.zig @@ -188,8 +188,8 @@ pub fn lowerToCode(emit: *Emit) Error!void { .fromInterned(fn_info.return_type), target, ).?; - code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.call_indirect)); if (is_obj) { + code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.call_indirect)); try wasm.out_relocs.append(gpa, .{ .offset = @intCast(code.items.len), .pointee = .{ .type_index = func_ty_index }, @@ -198,7 +198,19 @@ pub fn lowerToCode(emit: *Emit) Error!void { }); code.appendNTimesAssumeCapacity(0, 5); } else { - const index: Wasm.Flush.FuncTypeIndex = .fromTypeIndex(func_ty_index, &wasm.flush_buffer); + const index: Wasm.Flush.FuncTypeIndex = @enumFromInt(wasm.flush_buffer.func_types.getIndex(func_ty_index) orelse { + // In this case we tried to call a function pointer for + // which the type signature does not match any function + // body or function import in the entire wasm executable. + // + // Since there is no way to create a reference to a + // function without it being in the function table or + // import table, this instruction is unreachable. + code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.@"unreachable")); + inst += 1; + continue :loop tags[inst]; + }); + code.appendAssumeCapacity(@intFromEnum(std.wasm.Opcode.call_indirect)); writeUleb128(code, @intFromEnum(index)); } writeUleb128(code, @as(u32, 0)); // table index From df4c30ca1631c26fce3bec1cba6a15a688745433 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 13:21:49 -0700 Subject: [PATCH 207/244] link: move the windows kernel bug workaround to Io implementation --- lib/std/Io/Threaded.zig | 25 ++++++++++++++++++++----- src/link.zig | 31 +++++++++++-------------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 7b5d765bbf..2af24dbf17 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2081,6 +2081,10 @@ fn dirOpenFileWindowsInner( const create_file_flags: w.ULONG = file_or_dir_flag | if (flags.follow_symlinks) blocking_flag else w.FILE_OPEN_REPARSE_POINT; + // There are multiple kernel bugs being worked around with retries. + const max_attempts = 13; + var attempt: u5 = 0; + const handle = while (true) { try t.checkCancel(); @@ -2110,7 +2114,17 @@ fn dirOpenFileWindowsInner( .NO_MEDIA_IN_DEVICE => return error.NoDevice, .INVALID_PARAMETER => |err| return w.statusBug(err), .SHARING_VIOLATION => return error.AccessDenied, - .ACCESS_DENIED => return error.AccessDenied, + .ACCESS_DENIED => { + // This occurs if the file attempting to be opened is a running + // executable. However, there's a kernel bug: the error may be + // incorrectly returned for an indeterminate amount of time + // after an executable file is closed. Here we work around the + // kernel bug with retry attempts. + if (attempt - max_attempts == 0) return error.AccessDenied; + _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE); + attempt += 1; + continue; + }, .PIPE_BUSY => return error.PipeBusy, .PIPE_NOT_AVAILABLE => return error.NoDevice, .OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err), @@ -2123,10 +2137,11 @@ fn dirOpenFileWindowsInner( // This error means that there *was* a file in this location on // the file system, but it was deleted. However, the OS is not // finished with the deletion operation, and so this CreateFile - // call has failed. There is not really a sane way to handle - // this other than retrying the creation after the OS finishes - // the deletion. - _ = w.kernel32.SleepEx(1, w.FALSE); + // call has failed. Here, we simulate the kernel bug being + // fixed by sleeping and retrying until the error goes away. + if (attempt - max_attempts == 0) return error.AccessDenied; + _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE); + attempt += 1; continue; }, .VIRUS_INFECTED, .VIRUS_DELETED => return error.AntivirusInterference, diff --git a/src/link.zig b/src/link.zig index a0acce1073..485384e05d 100644 --- a/src/link.zig +++ b/src/link.zig @@ -1,19 +1,22 @@ -const std = @import("std"); -const build_options = @import("build_options"); const builtin = @import("builtin"); +const build_options = @import("build_options"); + +const std = @import("std"); +const Io = std.Io; const assert = std.debug.assert; const fs = std.fs; const mem = std.mem; const log = std.log.scoped(.link); -const trace = @import("tracy.zig").trace; -const wasi_libc = @import("libs/wasi_libc.zig"); - const Allocator = std.mem.Allocator; const Cache = std.Build.Cache; const Path = std.Build.Cache.Path; const Directory = std.Build.Cache.Directory; const Compilation = @import("Compilation.zig"); const LibCInstallation = std.zig.LibCInstallation; + +const trace = @import("tracy.zig").trace; +const wasi_libc = @import("libs/wasi_libc.zig"); + const Zcu = @import("Zcu.zig"); const InternPool = @import("InternPool.zig"); const Type = @import("Type.zig"); @@ -572,6 +575,7 @@ pub const File = struct { dev.check(.make_writable); const comp = base.comp; const gpa = comp.gpa; + const io = comp.io; switch (base.tag) { .lld => assert(base.file == null), .elf, .macho, .wasm, .goff, .xcoff => { @@ -616,22 +620,9 @@ pub const File = struct { &coff.mf else unreachable; - var attempt: u5 = 0; - mf.file = while (true) break base.emit.root_dir.handle.openFile(base.emit.sub_path, .{ + mf.file = .adaptFromNewApi(try Io.Dir.openFile(base.emit.root_dir.handle.adaptToNewApi(), io, base.emit.sub_path, .{ .mode = .read_write, - }) catch |err| switch (err) { - error.AccessDenied => switch (builtin.os.tag) { - .windows => { - if (attempt == 13) return error.AccessDenied; - // give the kernel a chance to finish closing the executable handle - std.os.windows.kernel32.Sleep(@as(u32, 1) << attempt >> 1); - attempt += 1; - continue; - }, - else => return error.AccessDenied, - }, - else => |e| return e, - }; + })); base.file = mf.file; try mf.ensureTotalCapacity(@intCast(mf.nodes.items[0].location().resolve(mf)[1])); }, From 0caf286a1a9f1f7fe13483d7adbf3f1357b12a1c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 14:08:51 -0700 Subject: [PATCH 208/244] std.Io.Threaded: don't skip executing canceled group closures --- lib/std/Io/Threaded.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 2af24dbf17..2bb88cbb78 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -566,10 +566,8 @@ const GroupClosure = struct { const reset_event: *ResetEvent = @ptrCast(&group.context); if (@cmpxchgStrong(CancelId, &closure.cancel_tid, .none, tid, .acq_rel, .acquire)) |cancel_tid| { assert(cancel_tid == .canceling); - // We already know the task is canceled before running the callback. Since all closures - // in a Group have void return type, we can return early. - syncFinish(group_state, reset_event); - return; + // Even though we already know the task is canceled, we must still + // run the closure in case there are side effects. } current_closure = closure; gc.func(group, gc.contextPointer()); From a87fd37bf59a975ba057bec408d2908d6168a084 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 13:51:14 -0700 Subject: [PATCH 209/244] std.Io: make Evented equal void when unimplemented This allows conditional compilation checks. --- lib/std/Io.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 1649341afc..0f69f8ed9c 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -560,8 +560,14 @@ test { const Io = @This(); pub const Evented = switch (builtin.os.tag) { - .linux => @import("Io/IoUring.zig"), - .dragonfly, .freebsd, .netbsd, .openbsd, .macos, .ios, .tvos, .visionos, .watchos => @import("Io/Kqueue.zig"), + .linux => switch (builtin.cpu.arch) { + .x86_64, .aarch64 => @import("Io/IoUring.zig"), + else => void, // context-switching code not implemented yet + }, + .dragonfly, .freebsd, .netbsd, .openbsd, .macos, .ios, .tvos, .visionos, .watchos => switch (builtin.cpu.arch) { + .x86_64, .aarch64 => @import("Io/Kqueue.zig"), + else => void, // context-switching code not implemented yet + }, else => void, }; pub const Threaded = @import("Io/Threaded.zig"); From 4e95c2eb1b996d7f4dfeaad8112b62733e077273 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 14:34:42 -0700 Subject: [PATCH 210/244] std.Io.Threaded: implement futexes for freebsd --- lib/std/Io/Threaded.zig | 336 +++++++++++++++++++++++----------------- 1 file changed, 192 insertions(+), 144 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 2bb88cbb78..449f2a3a32 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5047,7 +5047,7 @@ fn statFromLinux(stx: *const std.os.linux.Statx) Io.File.Stat { }; } -fn statFromPosix(st: *const std.posix.Stat) Io.File.Stat { +fn statFromPosix(st: *const posix.Stat) Io.File.Stat { const atime = st.atime(); const mtime = st.mtime(); const ctime = st.ctime(); @@ -5056,20 +5056,20 @@ fn statFromPosix(st: *const std.posix.Stat) Io.File.Stat { .size = @bitCast(st.size), .mode = st.mode, .kind = k: { - const m = st.mode & std.posix.S.IFMT; + const m = st.mode & posix.S.IFMT; switch (m) { - std.posix.S.IFBLK => break :k .block_device, - std.posix.S.IFCHR => break :k .character_device, - std.posix.S.IFDIR => break :k .directory, - std.posix.S.IFIFO => break :k .named_pipe, - std.posix.S.IFLNK => break :k .sym_link, - std.posix.S.IFREG => break :k .file, - std.posix.S.IFSOCK => break :k .unix_domain_socket, + posix.S.IFBLK => break :k .block_device, + posix.S.IFCHR => break :k .character_device, + posix.S.IFDIR => break :k .directory, + posix.S.IFIFO => break :k .named_pipe, + posix.S.IFLNK => break :k .sym_link, + posix.S.IFREG => break :k .file, + posix.S.IFSOCK => break :k .unix_domain_socket, else => {}, } if (native_os == .illumos) switch (m) { - std.posix.S.IFDOOR => break :k .door, - std.posix.S.IFPORT => break :k .event_port, + posix.S.IFDOOR => break :k .door, + posix.S.IFPORT => break :k .event_port, else => {}, }; @@ -5101,11 +5101,11 @@ fn statFromWasi(st: *const std.os.wasi.filestat_t) Io.File.Stat { }; } -fn timestampFromPosix(timespec: *const std.posix.timespec) Io.Timestamp { +fn timestampFromPosix(timespec: *const posix.timespec) Io.Timestamp { return .{ .nanoseconds = @intCast(@as(i128, timespec.sec) * std.time.ns_per_s + timespec.nsec) }; } -fn timestampToPosix(nanoseconds: i96) std.posix.timespec { +fn timestampToPosix(nanoseconds: i96) posix.timespec { return .{ .sec = @intCast(@divFloor(nanoseconds, std.time.ns_per_s)), .nsec = @intCast(@mod(nanoseconds, std.time.ns_per_s)), @@ -5503,44 +5503,7 @@ const darwin_supports_ulock_wait2 = builtin.os.version_range.semver.min.major >= fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Cancelable!void { @branchHint(.cold); - if (native_os == .linux) { - const linux = std.os.linux; - try t.checkCancel(); - const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); - if (is_debug) switch (linux.E.init(rc)) { - .SUCCESS => {}, // notified by `wake()` - .INTR => {}, // gives caller a chance to check cancellation - .AGAIN => {}, // ptr.* != expect - .INVAL => {}, // possibly timeout overflow - .TIMEDOUT => unreachable, - .FAULT => unreachable, // ptr was invalid - else => unreachable, - }; - } else if (native_os.isDarwin()) { - const c = std.c; - const flags: c.UL = .{ - .op = .COMPARE_AND_WAIT, - .NO_ERRNO = true, - }; - try t.checkCancel(); - const status = if (darwin_supports_ulock_wait2) - c.__ulock_wait2(flags, ptr, expect, 0, 0) - else - c.__ulock_wait(flags, ptr, expect, 0); - - if (status >= 0) return; - - if (is_debug) switch (@as(c.E, @enumFromInt(-status))) { - // Wait was interrupted by the OS or other spurious signalling. - .INTR => {}, - // Address of the futex was paged out. This is unlikely, but possible in theory, and - // pthread/libdispatch on darwin bother to handle it. In this case we'll return - // without waiting, but the caller should retry anyway. - .FAULT => {}, - .TIMEDOUT => unreachable, - else => unreachable, - }; - } else if (builtin.cpu.arch.isWasm()) { + if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); try t.checkCancel(); const timeout: i64 = -1; @@ -5562,57 +5525,74 @@ fn futexWait(t: *Threaded, ptr: *const std.atomic.Value(u32), expect: u32) Io.Ca 2 => assert(!is_debug), // timeout else => assert(!is_debug), } - } else if (is_windows) { - try t.checkCancel(); - switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { - .SUCCESS => {}, - .CANCELLED => return error.Canceled, - else => recoverableOsBugDetected(), - } - } else { - @compileError("TODO"); + } else switch (native_os) { + .linux => { + const linux = std.os.linux; + try t.checkCancel(); + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); + if (is_debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .INVAL => {}, // possibly timeout overflow + .TIMEDOUT => unreachable, + .FAULT => unreachable, // ptr was invalid + else => unreachable, + }; + }, + .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + }; + try t.checkCancel(); + const status = if (darwin_supports_ulock_wait2) + c.__ulock_wait2(flags, ptr, expect, 0, 0) + else + c.__ulock_wait(flags, ptr, expect, 0); + + if (status >= 0) return; + + if (is_debug) switch (@as(c.E, @enumFromInt(-status))) { + .INTR => {}, // spurious wake + // Address of the futex was paged out. This is unlikely, but possible in theory, and + // pthread/libdispatch on darwin bother to handle it. In this case we'll return + // without waiting, but the caller should retry anyway. + .FAULT => {}, + .TIMEDOUT => unreachable, + else => unreachable, + }; + }, + .windows => { + try t.checkCancel(); + switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { + .SUCCESS => {}, + .CANCELLED => return error.Canceled, + else => recoverableOsBugDetected(), + } + }, + .freebsd => { + const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE); + try t.checkCancel(); + const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0); + if (is_debug) switch (posix.errno(rc)) { + .SUCCESS => {}, + .FAULT => unreachable, // one of the args points to invalid memory + .INVAL => unreachable, // arguments should be correct + .TIMEDOUT => unreachable, // no timeout provided + .INTR => {}, // spurious wake + else => unreachable, + }; + }, + else => @compileError("unimplemented: futexWait"), } } pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) void { @branchHint(.cold); - if (native_os == .linux) { - const linux = std.os.linux; - const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); - if (is_debug) switch (linux.E.init(rc)) { - .SUCCESS => {}, // notified by `wake()` - .INTR => {}, // gives caller a chance to check cancellation - .AGAIN => {}, // ptr.* != expect - .INVAL => {}, // possibly timeout overflow - .TIMEDOUT => unreachable, - .FAULT => unreachable, // ptr was invalid - else => unreachable, - }; - } else if (native_os.isDarwin()) { - const c = std.c; - const flags: c.UL = .{ - .op = .COMPARE_AND_WAIT, - .NO_ERRNO = true, - }; - const status = if (darwin_supports_ulock_wait2) - c.__ulock_wait2(flags, ptr, expect, 0, 0) - else - c.__ulock_wait(flags, ptr, expect, 0); - - if (status >= 0) return; - - if (is_debug) switch (@as(c.E, @enumFromInt(-status))) { - // Wait was interrupted by the OS or other spurious signalling. - .INTR => {}, - // Address of the futex was paged out. This is unlikely, but possible in theory, and - // pthread/libdispatch on darwin bother to handle it. In this case we'll return - // without waiting, but the caller should retry anyway. - .FAULT => {}, - .TIMEDOUT => unreachable, - else => unreachable, - }; - } else if (builtin.cpu.arch.isWasm()) { + if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); const timeout: i64 = -1; const signed_expect: i32 = @bitCast(expect); @@ -5630,16 +5610,66 @@ pub fn futexWaitUncancelable(ptr: *const std.atomic.Value(u32), expect: u32) voi switch (result) { 0 => {}, // ok 1 => {}, // expected != loaded - 2 => assert(!is_debug), // timeout - else => assert(!is_debug), - } - } else if (is_windows) { - switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { - .SUCCESS, .CANCELLED => {}, + 2 => recoverableOsBugDetected(), // timeout else => recoverableOsBugDetected(), } - } else { - @compileError("TODO"); + } else switch (native_os) { + .linux => { + const linux = std.os.linux; + const rc = linux.futex_4arg(ptr, .{ .cmd = .WAIT, .private = true }, expect, null); + switch (linux.E.init(rc)) { + .SUCCESS => {}, // notified by `wake()` + .INTR => {}, // gives caller a chance to check cancellation + .AGAIN => {}, // ptr.* != expect + .INVAL => {}, // possibly timeout overflow + .TIMEDOUT => recoverableOsBugDetected(), + .FAULT => recoverableOsBugDetected(), // ptr was invalid + else => recoverableOsBugDetected(), + } + }, + .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + }; + const status = if (darwin_supports_ulock_wait2) + c.__ulock_wait2(flags, ptr, expect, 0, 0) + else + c.__ulock_wait(flags, ptr, expect, 0); + + if (status >= 0) return; + + switch (@as(c.E, @enumFromInt(-status))) { + // Wait was interrupted by the OS or other spurious signalling. + .INTR => {}, + // Address of the futex was paged out. This is unlikely, but possible in theory, and + // pthread/libdispatch on darwin bother to handle it. In this case we'll return + // without waiting, but the caller should retry anyway. + .FAULT => {}, + .TIMEDOUT => recoverableOsBugDetected(), + else => recoverableOsBugDetected(), + } + }, + .windows => { + switch (windows.ntdll.RtlWaitOnAddress(ptr, &expect, @sizeOf(@TypeOf(expect)), null)) { + .SUCCESS, .CANCELLED => {}, + else => recoverableOsBugDetected(), + } + }, + .freebsd => { + const flags = @intFromEnum(std.c.UMTX_OP.WAIT_UINT_PRIVATE); + const rc = std.c._umtx_op(@intFromPtr(&ptr.raw), flags, @as(c_ulong, expect), 0, 0); + switch (posix.errno(rc)) { + .SUCCESS => {}, + .INTR => {}, // spurious wake + .FAULT => recoverableOsBugDetected(), // one of the args points to invalid memory + .INVAL => recoverableOsBugDetected(), // arguments should be correct + .TIMEDOUT => recoverableOsBugDetected(), // no timeout provided + else => recoverableOsBugDetected(), + } + }, + else => @compileError("unimplemented: futexWaitUncancelable"), } } @@ -5668,38 +5698,7 @@ pub fn futexWaitDurationUncancelable(ptr: *const std.atomic.Value(u32), expect: pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { @branchHint(.cold); - if (native_os == .linux) { - const linux = std.os.linux; - const rc = linux.futex_3arg( - &ptr.raw, - .{ .cmd = .WAKE, .private = true }, - @min(max_waiters, std.math.maxInt(i32)), - ); - if (is_debug) switch (linux.E.init(rc)) { - .SUCCESS => {}, // successful wake up - .INVAL => {}, // invalid futex_wait() on ptr done elsewhere - .FAULT => {}, // pointer became invalid while doing the wake - else => unreachable, - }; - } else if (native_os.isDarwin()) { - const c = std.c; - const flags: c.UL = .{ - .op = .COMPARE_AND_WAIT, - .NO_ERRNO = true, - .WAKE_ALL = max_waiters > 1, - }; - while (true) { - const status = c.__ulock_wake(flags, ptr, 0); - if (status >= 0) return; - switch (@as(c.E, @enumFromInt(-status))) { - .INTR, .CANCELED => continue, // spurious wake() - .FAULT => assert(!is_debug), // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t - .NOENT => return, // nothing was woken up - .ALREADY => assert(!is_debug), // only for UL.Op.WAKE_THREAD - else => assert(!is_debug), - } - } - } else if (builtin.cpu.arch.isWasm()) { + if (builtin.cpu.arch.isWasm()) { comptime assert(builtin.cpu.has(.wasm, .atomics)); assert(max_waiters != 0); const woken_count = asm volatile ( @@ -5712,14 +5711,63 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { [waiters] "r" (max_waiters), ); _ = woken_count; // can be 0 when linker flag 'shared-memory' is not enabled - } else if (is_windows) { - assert(max_waiters != 0); - switch (max_waiters) { - 1 => windows.ntdll.RtlWakeAddressSingle(ptr), - else => windows.ntdll.RtlWakeAddressAll(ptr), - } - } else { - @compileError("TODO"); + } else switch (native_os) { + .linux => { + const linux = std.os.linux; + const rc = linux.futex_3arg( + &ptr.raw, + .{ .cmd = .WAKE, .private = true }, + @min(max_waiters, std.math.maxInt(i32)), + ); + if (is_debug) switch (linux.E.init(rc)) { + .SUCCESS => {}, // successful wake up + .INVAL => {}, // invalid futex_wait() on ptr done elsewhere + .FAULT => {}, // pointer became invalid while doing the wake + else => unreachable, // deadlock due to operating system bug + }; + }, + .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { + const c = std.c; + const flags: c.UL = .{ + .op = .COMPARE_AND_WAIT, + .NO_ERRNO = true, + .WAKE_ALL = max_waiters > 1, + }; + while (true) { + const status = c.__ulock_wake(flags, ptr, 0); + if (status >= 0) return; + switch (@as(c.E, @enumFromInt(-status))) { + .INTR, .CANCELED => continue, // spurious wake() + .FAULT => unreachable, // __ulock_wake doesn't generate EFAULT according to darwin pthread_cond_t + .NOENT => return, // nothing was woken up + .ALREADY => unreachable, // only for UL.Op.WAKE_THREAD + else => unreachable, // deadlock due to operating system bug + } + } + }, + .windows => { + assert(max_waiters != 0); + switch (max_waiters) { + 1 => windows.ntdll.RtlWakeAddressSingle(ptr), + else => windows.ntdll.RtlWakeAddressAll(ptr), + } + }, + .freebsd => { + const rc = std.c._umtx_op( + @intFromPtr(&ptr.raw), + @intFromEnum(std.c.UMTX_OP.WAKE_PRIVATE), + @as(c_ulong, max_waiters), + 0, // there is no timeout struct + 0, // there is no timeout struct pointer + ); + switch (posix.errno(rc)) { + .SUCCESS => {}, + .FAULT => {}, // it's ok if the ptr doesn't point to valid memory + .INVAL => unreachable, // arguments should be correct + else => unreachable, // deadlock due to operating system bug + } + }, + else => @compileError("unimplemented: futexWake"), } } From bbc1c075382b3fa9f275fac097267b7c61b5a899 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 14:54:51 -0700 Subject: [PATCH 211/244] std.zig.system: fix error set of abiAndDynamicLinkerFromFile --- lib/std/zig/system.zig | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index a2315b3ab9..fc433d9936 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -543,6 +543,34 @@ fn detectNativeCpuAndFeatures(io: Io, cpu_arch: Target.Cpu.Arch, os: Target.Os, return null; } +pub const AbiAndDynamicLinkerFromFileError = error{ + Canceled, + AccessDenied, + Unexpected, + Unseekable, + ReadFailed, + EndOfStream, + NameTooLong, + StaticElfFile, + InvalidElfFile, + StreamTooLong, + Timeout, + SymLinkLoop, + SystemResources, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + ProcessNotFound, + IsDir, + WouldBlock, + InputOutput, + BrokenPipe, + ConnectionResetByPeer, + NotOpenForReading, + SocketUnconnected, + LockViolation, + FileSystem, +}; + fn abiAndDynamicLinkerFromFile( file_reader: *Io.File.Reader, header: *const elf.Header, @@ -550,7 +578,7 @@ fn abiAndDynamicLinkerFromFile( os: Target.Os, ld_info_list: []const LdInfo, query: Target.Query, -) !Target { +) AbiAndDynamicLinkerFromFileError!Target { const io = file_reader.io; var result: Target = .{ .cpu = cpu, From ef55dcae675dd2048e5e404eb8bb6833d153f653 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 14:57:01 -0700 Subject: [PATCH 212/244] start: fix logic for signal hanlding when SIG.POLL does not exist fixes a compilation failure on FreeBSD --- lib/std/start.zig | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/std/start.zig b/lib/std/start.zig index 09c1f3b5c4..20dd981707 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -784,9 +784,19 @@ fn maybeIgnoreSignals() void { .mask = posix.sigemptyset(), .flags = 0, }; - if (!std.options.keep_sigpoll) posix.sigaction(posix.SIG.POLL, &act, null); - if (@hasField(posix.SIG, "IO") and posix.SIG.IO != posix.SIG.POLL and !std.options.keep_sigio) posix.sigaction(posix.SIG.IO, &act, null); - if (!std.options.keep_sigpipe) posix.sigaction(posix.SIG.PIPE, &act, null); + + if (@hasField(posix.SIG, "POLL") and !std.options.keep_sigpoll) + posix.sigaction(posix.SIG.POLL, &act, null); + + if (@hasField(posix.SIG, "IO") and + (!@hasField(posix.SIG, "POLL") or posix.SIG.IO != posix.SIG.POLL) and + !std.options.keep_sigio) + { + posix.sigaction(posix.SIG.IO, &act, null); + } + + if (@hasField(posix.SIG, "PIPE") and !std.options.keep_sigpipe) + posix.sigaction(posix.SIG.PIPE, &act, null); } fn noopSigHandler(_: i32) callconv(.c) void {} From cc751c01f13658280bc48905261f316b9b58b718 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 14:59:42 -0700 Subject: [PATCH 213/244] std.Io.Threaded: correct clockToPosix for FreeBSD --- lib/std/Io/Threaded.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 449f2a3a32..c078852899 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5006,6 +5006,9 @@ fn clockToPosix(clock: Io.Clock) posix.clockid_t { }, .boot => switch (native_os) { .macos, .ios, .watchos, .tvos => posix.CLOCK.MONOTONIC_RAW, + // On freebsd derivatives, use MONOTONIC_FAST as currently there's + // no precision tradeoff. + .freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST, else => posix.CLOCK.BOOTTIME, }, .cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID, From 8b269f7e18ee1e0874b7cbf7cf2853b7e34b6a36 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 16:16:49 -0700 Subject: [PATCH 214/244] std: make signal numbers into an enum fixes start logic for checking whether IO/POLL exist --- lib/std/Io/Threaded.zig | 4 +- lib/std/Progress.zig | 6 +- lib/std/c.zig | 699 ++++++++++++++-------------- lib/std/debug.zig | 20 +- lib/std/os/emscripten.zig | 45 +- lib/std/os/linux.zig | 276 +++++------ lib/std/os/linux/test.zig | 70 +-- lib/std/posix.zig | 22 +- lib/std/posix/test.zig | 22 +- lib/std/start.zig | 39 +- lib/std/std.zig | 6 +- test/standalone/posix/sigaction.zig | 24 +- 12 files changed, 578 insertions(+), 655 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c078852899..c3d1102fcc 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -75,10 +75,10 @@ const Closure = struct { .none, .canceling => {}, else => |tid| { if (std.Thread.use_pthreads) { - const rc = std.c.pthread_kill(tid.toThreadId(), posix.SIG.IO); + const rc = std.c.pthread_kill(tid.toThreadId(), .IO); if (is_debug) assert(rc == 0); } else if (native_os == .linux) { - _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid.toThreadId()), posix.SIG.IO); + _ = std.os.linux.tgkill(std.os.linux.getpid(), @bitCast(tid.toThreadId()), .IO); } }, } diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index d9ec89b893..d2e962a8c2 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -493,7 +493,7 @@ pub fn start(options: Options) Node { .mask = posix.sigemptyset(), .flags = (posix.SA.SIGINFO | posix.SA.RESTART), }; - posix.sigaction(posix.SIG.WINCH, &act, null); + posix.sigaction(.WINCH, &act, null); } if (switch (global_progress.terminal_mode) { @@ -1535,10 +1535,10 @@ fn maybeUpdateSize(resize_flag: bool) void { } } -fn handleSigWinch(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { +fn handleSigWinch(sig: posix.SIG, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = info; _ = ctx_ptr; - assert(sig == posix.SIG.WINCH); + assert(sig == .WINCH); global_progress.redraw_event.set(); } diff --git a/lib/std/c.zig b/lib/std/c.zig index 024c76ddc4..89b28105ae 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -2587,25 +2587,24 @@ pub const SHUT = switch (native_os) { /// Signal types pub const SIG = switch (native_os) { - .linux => linux.SIG, - .emscripten => emscripten.SIG, - .windows => struct { + .linux, .emscripten => linux.SIG, + .windows => enum(u32) { /// interrupt - pub const INT = 2; + INT = 2, /// illegal instruction - invalid function image - pub const ILL = 4; + ILL = 4, /// floating point exception - pub const FPE = 8; + FPE = 8, /// segment violation - pub const SEGV = 11; + SEGV = 11, /// Software termination signal from kill - pub const TERM = 15; + TERM = 15, /// Ctrl-Break sequence - pub const BREAK = 21; + BREAK = 21, /// abnormal termination triggered by abort call - pub const ABRT = 22; + ABRT = 22, /// SIGABRT compatible with other platforms, same as SIGABRT - pub const ABRT_COMPAT = 6; + ABRT_COMPAT = 6, // Signal action codes /// default signal action @@ -2621,7 +2620,7 @@ pub const SIG = switch (native_os) { /// Signal error value (returned by signal call on error) pub const ERR = -1; }, - .macos, .ios, .tvos, .watchos, .visionos => struct { + .macos, .ios, .tvos, .watchos, .visionos => enum(u32) { pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); @@ -2633,113 +2632,74 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; /// set specified signal set pub const SETMASK = 3; + + pub const IOT: SIG = .ABRT; + pub const POLL: SIG = .EMT; + /// hangup - pub const HUP = 1; + HUP = 1, /// interrupt - pub const INT = 2; + INT = 2, /// quit - pub const QUIT = 3; + QUIT = 3, /// illegal instruction (not reset when caught) - pub const ILL = 4; + ILL = 4, /// trace trap (not reset when caught) - pub const TRAP = 5; + TRAP = 5, /// abort() - pub const ABRT = 6; - /// pollable event ([XSR] generated, not supported) - pub const POLL = 7; - /// compatibility - pub const IOT = ABRT; + ABRT = 6, /// EMT instruction - pub const EMT = 7; + EMT = 7, /// floating point exception - pub const FPE = 8; + FPE = 8, /// kill (cannot be caught or ignored) - pub const KILL = 9; + KILL = 9, /// bus error - pub const BUS = 10; + BUS = 10, /// segmentation violation - pub const SEGV = 11; + SEGV = 11, /// bad argument to system call - pub const SYS = 12; + SYS = 12, /// write on a pipe with no one to read it - pub const PIPE = 13; + PIPE = 13, /// alarm clock - pub const ALRM = 14; + ALRM = 14, /// software termination signal from kill - pub const TERM = 15; + TERM = 15, /// urgent condition on IO channel - pub const URG = 16; + URG = 16, /// sendable stop signal not from tty - pub const STOP = 17; + STOP = 17, /// stop signal from tty - pub const TSTP = 18; + TSTP = 18, /// continue a stopped process - pub const CONT = 19; + CONT = 19, /// to parent on child stop or exit - pub const CHLD = 20; + CHLD = 20, /// to readers pgrp upon background tty read - pub const TTIN = 21; + TTIN = 21, /// like TTIN for output if (tp->t_local<OSTOP) - pub const TTOU = 22; + TTOU = 22, /// input/output possible signal - pub const IO = 23; + IO = 23, /// exceeded CPU time limit - pub const XCPU = 24; + XCPU = 24, /// exceeded file size limit - pub const XFSZ = 25; + XFSZ = 25, /// virtual time alarm - pub const VTALRM = 26; + VTALRM = 26, /// profiling time alarm - pub const PROF = 27; + PROF = 27, /// window size changes - pub const WINCH = 28; + WINCH = 28, /// information request - pub const INFO = 29; + INFO = 29, /// user defined signal 1 - pub const USR1 = 30; + USR1 = 30, /// user defined signal 2 - pub const USR2 = 31; + USR2 = 31, }, - .freebsd => struct { - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const THR = 32; - pub const LWP = THR; - pub const LIBRT = 33; - - pub const RTMIN = 65; - pub const RTMAX = 126; - + .freebsd => enum(u32) { pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; @@ -2763,8 +2723,48 @@ pub const SIG = switch (native_os) { pub inline fn VALID(sig: usize) usize { return sig <= MAXSIG and sig > 0; } + + pub const IOT: SIG = .ABRT; + pub const LWP: SIG = .THR; + + pub const RTMIN = 65; + pub const RTMAX = 126; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + THR = 32, + LIBRT = 33, }, - .illumos => struct { + .illumos => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); @@ -2773,54 +2773,9 @@ pub const SIG = switch (native_os) { pub const WORDS = 4; pub const MAXSIG = 75; - pub const SIG_BLOCK = 1; - pub const SIG_UNBLOCK = 2; - pub const SIG_SETMASK = 3; - - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const IOT = 6; - pub const ABRT = 6; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const USR1 = 16; - pub const USR2 = 17; - pub const CLD = 18; - pub const CHLD = 18; - pub const PWR = 19; - pub const WINCH = 20; - pub const URG = 21; - pub const POLL = 22; - pub const IO = .POLL; - pub const STOP = 23; - pub const TSTP = 24; - pub const CONT = 25; - pub const TTIN = 26; - pub const TTOU = 27; - pub const VTALRM = 28; - pub const PROF = 29; - pub const XCPU = 30; - pub const XFSZ = 31; - pub const WAITING = 32; - pub const LWP = 33; - pub const FREEZE = 34; - pub const THAW = 35; - pub const CANCEL = 36; - pub const LOST = 37; - pub const XRES = 38; - pub const JVM1 = 39; - pub const JVM2 = 40; - pub const INFO = 41; + pub const BLOCK = 1; + pub const UNBLOCK = 2; + pub const SETMASK = 3; pub const RTMIN = 42; pub const RTMAX = 74; @@ -2837,8 +2792,54 @@ pub const SIG = switch (native_os) { pub inline fn VALID(sig: usize) usize { return sig <= MAXSIG and sig > 0; } + + pub const POLL: SIG = .IO; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + IOT = 6, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + USR1 = 16, + USR2 = 17, + CLD = 18, + CHLD = 18, + PWR = 19, + WINCH = 20, + URG = 21, + IO = 22, + STOP = 23, + TSTP = 24, + CONT = 25, + TTIN = 26, + TTOU = 27, + VTALRM = 28, + PROF = 29, + XCPU = 30, + XFSZ = 31, + WAITING = 32, + LWP = 33, + FREEZE = 34, + THAW = 35, + CANCEL = 36, + LOST = 37, + XRES = 38, + JVM1 = 39, + JVM2 = 40, + INFO = 41, }, - .netbsd => struct { + .netbsd => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); @@ -2850,40 +2851,6 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; pub const SETMASK = 3; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const PWR = 32; - pub const RTMIN = 33; pub const RTMAX = 63; @@ -2899,8 +2866,43 @@ pub const SIG = switch (native_os) { pub inline fn VALID(sig: usize) usize { return sig <= MAXSIG and sig > 0; } + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + PWR = 32, }, - .dragonfly => struct { + .dragonfly => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); @@ -2909,137 +2911,140 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; pub const SETMASK = 3; - pub const IOT = ABRT; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const THR = 32; - pub const CKPT = 33; - pub const CKPTEXIT = 34; - pub const WORDS = 4; + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + THR = 32, + CKPT = 33, + CKPTEXIT = 34, }, - .haiku => struct { + .haiku => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const HOLD: ?Sigaction.handler_fn = @ptrFromInt(3); - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const CHLD = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const PIPE = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const STOP = 10; - pub const SEGV = 11; - pub const CONT = 12; - pub const TSTP = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const TTIN = 16; - pub const TTOU = 17; - pub const USR1 = 18; - pub const USR2 = 19; - pub const WINCH = 20; - pub const KILLTHR = 21; - pub const TRAP = 22; - pub const POLL = 23; - pub const PROF = 24; - pub const SYS = 25; - pub const URG = 26; - pub const VTALRM = 27; - pub const XCPU = 28; - pub const XFSZ = 29; - pub const BUS = 30; - pub const RESERVED1 = 31; - pub const RESERVED2 = 32; - pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + CHLD = 5, + ABRT = 6, + PIPE = 7, + FPE = 8, + KILL = 9, + STOP = 10, + SEGV = 11, + CONT = 12, + TSTP = 13, + ALRM = 14, + TERM = 15, + TTIN = 16, + TTOU = 17, + USR1 = 18, + USR2 = 19, + WINCH = 20, + KILLTHR = 21, + TRAP = 22, + POLL = 23, + PROF = 24, + SYS = 25, + URG = 26, + VTALRM = 27, + XCPU = 28, + XFSZ = 29, + BUS = 30, + RESERVED1 = 31, + RESERVED2 = 32, }, - .openbsd => struct { + .openbsd => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const CATCH: ?Sigaction.handler_fn = @ptrFromInt(2); pub const HOLD: ?Sigaction.handler_fn = @ptrFromInt(3); - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const IO = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const INFO = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const PWR = 32; - pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; + + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + INFO = 29, + USR1 = 30, + USR2 = 31, + PWR = 32, }, // https://github.com/SerenityOS/serenity/blob/046c23f567a17758d762a33bdf04bacbfd088f9f/Kernel/API/POSIX/signal.h // https://github.com/SerenityOS/serenity/blob/046c23f567a17758d762a33bdf04bacbfd088f9f/Kernel/API/POSIX/signal_numbers.h - .serenity => struct { + .serenity => enum(u32) { pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); @@ -3048,39 +3053,39 @@ pub const SIG = switch (native_os) { pub const UNBLOCK = 2; pub const SETMASK = 3; - pub const INVAL = 0; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const BUS = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const USR1 = 10; - pub const SEGV = 11; - pub const USR2 = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const STKFLT = 16; - pub const CHLD = 17; - pub const CONT = 18; - pub const STOP = 19; - pub const TSTP = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const URG = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const IO = 29; - pub const INFO = 30; - pub const SYS = 31; - pub const CANCEL = 32; + INVAL = 0, + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + BUS = 7, + FPE = 8, + KILL = 9, + USR1 = 10, + SEGV = 11, + USR2 = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + STKFLT = 16, + CHLD = 17, + CONT = 18, + STOP = 19, + TSTP = 20, + TTIN = 21, + TTOU = 22, + URG = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + IO = 29, + INFO = 30, + SYS = 31, + CANCEL = 32, }, else => void, }; @@ -3117,8 +3122,8 @@ pub const SYS = switch (native_os) { /// A common format for the Sigaction struct across a variety of Linux flavors. const common_linux_Sigaction = extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -3139,8 +3144,8 @@ pub const Sigaction = switch (native_os) { => if (builtin.target.abi.isMusl()) common_linux_Sigaction else if (builtin.target.ptrBitWidth() == 64) extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; flags: c_uint, handler: extern union { @@ -3150,8 +3155,8 @@ pub const Sigaction = switch (native_os) { mask: sigset_t, restorer: ?*const fn () callconv(.c) void = null, } else extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; flags: c_uint, handler: extern union { @@ -3163,8 +3168,8 @@ pub const Sigaction = switch (native_os) { __resv: [1]c_int = .{0}, }, .s390x => if (builtin.abi == .gnu) extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -3179,8 +3184,8 @@ pub const Sigaction = switch (native_os) { }, .emscripten => emscripten.Sigaction, .netbsd, .macos, .ios, .tvos, .watchos, .visionos => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -3190,8 +3195,8 @@ pub const Sigaction = switch (native_os) { flags: c_uint, }, .dragonfly, .freebsd => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal handler handler: extern union { @@ -3204,8 +3209,8 @@ pub const Sigaction = switch (native_os) { mask: sigset_t, }, .illumos => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal options flags: c_uint, @@ -3218,8 +3223,8 @@ pub const Sigaction = switch (native_os) { mask: sigset_t, }, .haiku => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal handler handler: extern union { @@ -3237,8 +3242,8 @@ pub const Sigaction = switch (native_os) { userdata: *allowzero anyopaque = undefined, }, .openbsd => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; /// signal handler handler: extern union { @@ -3252,8 +3257,8 @@ pub const Sigaction = switch (native_os) { }, // https://github.com/SerenityOS/serenity/blob/ec492a1a0819e6239ea44156825c4ee7234ca3db/Kernel/API/POSIX/signal.h#L39-L46 .serenity => extern struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -4433,7 +4438,7 @@ pub const siginfo_t = switch (native_os) { .linux => linux.siginfo_t, .emscripten => emscripten.siginfo_t, .driverkit, .macos, .ios, .tvos, .watchos, .visionos => extern struct { - signo: c_int, + signo: SIG, errno: c_int, code: c_int, pid: pid_t, @@ -4449,7 +4454,7 @@ pub const siginfo_t = switch (native_os) { }, .freebsd => extern struct { // Signal number. - signo: c_int, + signo: SIG, // Errno association. errno: c_int, /// Signal code. @@ -4492,7 +4497,7 @@ pub const siginfo_t = switch (native_os) { }, }, .illumos => extern struct { - signo: c_int, + signo: SIG, code: c_int, errno: c_int, // 64bit architectures insert 4bytes of padding here, this is done by @@ -4549,7 +4554,7 @@ pub const siginfo_t = switch (native_os) { info: netbsd._ksiginfo, }, .dragonfly => extern struct { - signo: c_int, + signo: SIG, errno: c_int, code: c_int, pid: c_int, @@ -4561,7 +4566,7 @@ pub const siginfo_t = switch (native_os) { __spare__: [7]c_int, }, .haiku => extern struct { - signo: i32, + signo: SIG, code: i32, errno: i32, @@ -4570,7 +4575,7 @@ pub const siginfo_t = switch (native_os) { addr: *allowzero anyopaque, }, .openbsd => extern struct { - signo: c_int, + signo: SIG, code: c_int, errno: c_int, data: extern union { @@ -4605,7 +4610,7 @@ pub const siginfo_t = switch (native_os) { }, // https://github.com/SerenityOS/serenity/blob/ec492a1a0819e6239ea44156825c4ee7234ca3db/Kernel/API/POSIX/signal.h#L27-L37 .serenity => extern struct { - signo: c_int, + signo: SIG, code: c_int, errno: c_int, pid: pid_t, @@ -10581,7 +10586,7 @@ pub extern "c" fn lseek(fd: fd_t, offset: off_t, whence: whence_t) off_t; pub extern "c" fn open(path: [*:0]const u8, oflag: O, ...) c_int; pub extern "c" fn openat(fd: c_int, path: [*:0]const u8, oflag: O, ...) c_int; pub extern "c" fn ftruncate(fd: c_int, length: off_t) c_int; -pub extern "c" fn raise(sig: c_int) c_int; +pub extern "c" fn raise(sig: SIG) c_int; pub extern "c" fn read(fd: fd_t, buf: [*]u8, nbyte: usize) isize; pub extern "c" fn readv(fd: c_int, iov: [*]const iovec, iovcnt: c_uint) isize; pub extern "c" fn pread(fd: fd_t, buf: [*]u8, nbyte: usize, offset: off_t) isize; @@ -10699,7 +10704,7 @@ pub const recvmsg = switch (native_os) { else => private.recvmsg, }; -pub extern "c" fn kill(pid: pid_t, sig: c_int) c_int; +pub extern "c" fn kill(pid: pid_t, sig: SIG) c_int; pub extern "c" fn setuid(uid: uid_t) c_int; pub extern "c" fn setgid(gid: gid_t) c_int; @@ -10763,7 +10768,7 @@ pub const pthread_setname_np = switch (native_os) { }; pub extern "c" fn pthread_getname_np(thread: pthread_t, name: [*:0]u8, len: usize) c_int; -pub extern "c" fn pthread_kill(pthread_t, signal: c_int) c_int; +pub extern "c" fn pthread_kill(pthread_t, signal: SIG) c_int; pub const pthread_threadid_np = switch (native_os) { .macos, .ios, .tvos, .watchos, .visionos => private.pthread_threadid_np, @@ -11356,12 +11361,12 @@ const private = struct { extern "c" fn recvmsg(sockfd: fd_t, msg: *msghdr, flags: u32) isize; extern "c" fn sched_yield() c_int; extern "c" fn sendfile(out_fd: fd_t, in_fd: fd_t, offset: ?*off_t, count: usize) isize; - extern "c" fn sigaction(sig: c_int, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; - extern "c" fn sigdelset(set: ?*sigset_t, signo: c_int) c_int; - extern "c" fn sigaddset(set: ?*sigset_t, signo: c_int) c_int; + extern "c" fn sigaction(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; + extern "c" fn sigdelset(set: ?*sigset_t, signo: SIG) c_int; + extern "c" fn sigaddset(set: ?*sigset_t, signo: SIG) c_int; extern "c" fn sigfillset(set: ?*sigset_t) c_int; extern "c" fn sigemptyset(set: ?*sigset_t) c_int; - extern "c" fn sigismember(set: ?*const sigset_t, signo: c_int) c_int; + extern "c" fn sigismember(set: ?*const sigset_t, signo: SIG) c_int; extern "c" fn sigprocmask(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int; extern "c" fn socket(domain: c_uint, sock_type: c_uint, protocol: c_uint) c_int; extern "c" fn socketpair(domain: c_uint, sock_type: c_uint, protocol: c_uint, sv: *[2]fd_t) c_int; @@ -11413,7 +11418,7 @@ const private = struct { extern "c" fn __libc_thr_yield() c_int; extern "c" fn __msync13(addr: *align(page_size) const anyopaque, len: usize, flags: c_int) c_int; extern "c" fn __nanosleep50(rqtp: *const timespec, rmtp: ?*timespec) c_int; - extern "c" fn __sigaction14(sig: c_int, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; + extern "c" fn __sigaction14(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) c_int; extern "c" fn __sigemptyset14(set: ?*sigset_t) c_int; extern "c" fn __sigfillset14(set: ?*sigset_t) c_int; extern "c" fn __sigprocmask14(how: c_int, noalias set: ?*const sigset_t, noalias oset: ?*sigset_t) c_int; diff --git a/lib/std/debug.zig b/lib/std/debug.zig index 616a524011..1a5546fadf 100644 --- a/lib/std/debug.zig +++ b/lib/std/debug.zig @@ -1409,10 +1409,10 @@ pub fn maybeEnableSegfaultHandler() void { var windows_segfault_handle: ?windows.HANDLE = null; pub fn updateSegfaultHandler(act: ?*const posix.Sigaction) void { - posix.sigaction(posix.SIG.SEGV, act, null); - posix.sigaction(posix.SIG.ILL, act, null); - posix.sigaction(posix.SIG.BUS, act, null); - posix.sigaction(posix.SIG.FPE, act, null); + posix.sigaction(.SEGV, act, null); + posix.sigaction(.ILL, act, null); + posix.sigaction(.BUS, act, null); + posix.sigaction(.FPE, act, null); } /// Attaches a global handler for several signals which, when triggered, prints output to stderr @@ -1457,7 +1457,7 @@ fn resetSegfaultHandler() void { updateSegfaultHandler(&act); } -fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { +fn handleSegfaultPosix(sig: posix.SIG, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { if (use_trap_panic) @trap(); const addr: ?usize, const name: []const u8 = info: { if (native_os == .linux and native_arch == .x86_64) { @@ -1469,7 +1469,7 @@ fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopa // for example when reading/writing model-specific registers // by executing `rdmsr` or `wrmsr` in user-space (unprivileged mode). const SI_KERNEL = 0x80; - if (sig == posix.SIG.SEGV and info.code == SI_KERNEL) { + if (sig == .SEGV and info.code == SI_KERNEL) { break :info .{ null, "General protection exception" }; } } @@ -1496,10 +1496,10 @@ fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopa else => comptime unreachable, }; const name = switch (sig) { - posix.SIG.SEGV => "Segmentation fault", - posix.SIG.ILL => "Illegal instruction", - posix.SIG.BUS => "Bus error", - posix.SIG.FPE => "Arithmetic exception", + .SEGV => "Segmentation fault", + .ILL => "Illegal instruction", + .BUS => "Bus error", + .FPE => "Arithmetic exception", else => unreachable, }; break :info .{ addr, name }; diff --git a/lib/std/os/emscripten.zig b/lib/std/os/emscripten.zig index 1ecb4f6bb0..cb444b360d 100644 --- a/lib/std/os/emscripten.zig +++ b/lib/std/os/emscripten.zig @@ -479,50 +479,7 @@ pub const SHUT = struct { pub const RDWR = 2; }; -pub const SIG = struct { - pub const BLOCK = 0; - pub const UNBLOCK = 1; - pub const SETMASK = 2; - - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const BUS = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const USR1 = 10; - pub const SEGV = 11; - pub const USR2 = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const STKFLT = 16; - pub const CHLD = 17; - pub const CONT = 18; - pub const STOP = 19; - pub const TSTP = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const URG = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const IO = 29; - pub const POLL = 29; - pub const PWR = 30; - pub const SYS = 31; - pub const UNUSED = SIG.SYS; - - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(std.math.maxInt(usize)); - pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); - pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); -}; +pub const SIG = linux.SIG; pub const Sigaction = extern struct { pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 96010fb203..882de1f458 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -622,7 +622,7 @@ pub fn fork() usize { } else if (@hasField(SYS, "fork")) { return syscall0(.fork); } else { - return syscall2(.clone, SIG.CHLD, 0); + return syscall2(.clone, @intFromEnum(SIG.CHLD), 0); } } @@ -1532,16 +1532,16 @@ pub fn getrandom(buf: [*]u8, count: usize, flags: u32) usize { return syscall3(.getrandom, @intFromPtr(buf), count, flags); } -pub fn kill(pid: pid_t, sig: i32) usize { - return syscall2(.kill, @as(usize, @bitCast(@as(isize, pid))), @as(usize, @bitCast(@as(isize, sig)))); +pub fn kill(pid: pid_t, sig: SIG) usize { + return syscall2(.kill, @as(usize, @bitCast(@as(isize, pid))), @intFromEnum(sig)); } -pub fn tkill(tid: pid_t, sig: i32) usize { - return syscall2(.tkill, @as(usize, @bitCast(@as(isize, tid))), @as(usize, @bitCast(@as(isize, sig)))); +pub fn tkill(tid: pid_t, sig: SIG) usize { + return syscall2(.tkill, @as(usize, @bitCast(@as(isize, tid))), @intFromEnum(sig)); } -pub fn tgkill(tgid: pid_t, tid: pid_t, sig: i32) usize { - return syscall3(.tgkill, @as(usize, @bitCast(@as(isize, tgid))), @as(usize, @bitCast(@as(isize, tid))), @as(usize, @bitCast(@as(isize, sig)))); +pub fn tgkill(tgid: pid_t, tid: pid_t, sig: SIG) usize { + return syscall3(.tgkill, @as(usize, @bitCast(@as(isize, tgid))), @as(usize, @bitCast(@as(isize, tid))), @intFromEnum(sig)); } pub fn link(oldpath: [*:0]const u8, newpath: [*:0]const u8) usize { @@ -1923,11 +1923,11 @@ pub fn sigprocmask(flags: u32, noalias set: ?*const sigset_t, noalias oldset: ?* return syscall4(.rt_sigprocmask, flags, @intFromPtr(set), @intFromPtr(oldset), NSIG / 8); } -pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) usize { - assert(sig > 0); - assert(sig < NSIG); - assert(sig != SIG.KILL); - assert(sig != SIG.STOP); +pub fn sigaction(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) usize { + assert(@intFromEnum(sig) > 0); + assert(@intFromEnum(sig) < NSIG); + assert(sig != .KILL); + assert(sig != .STOP); var ksa: k_sigaction = undefined; var oldksa: k_sigaction = undefined; @@ -1958,8 +1958,8 @@ pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigact const result = switch (native_arch) { // The sparc version of rt_sigaction needs the restorer function to be passed as an argument too. - .sparc, .sparc64 => syscall5(.rt_sigaction, sig, ksa_arg, oldksa_arg, @intFromPtr(ksa.restorer), mask_size), - else => syscall4(.rt_sigaction, sig, ksa_arg, oldksa_arg, mask_size), + .sparc, .sparc64 => syscall5(.rt_sigaction, @intFromEnum(sig), ksa_arg, oldksa_arg, @intFromPtr(ksa.restorer), mask_size), + else => syscall4(.rt_sigaction, @intFromEnum(sig), ksa_arg, oldksa_arg, mask_size), }; if (E.init(result) != .SUCCESS) return result; @@ -2009,27 +2009,27 @@ pub fn sigfillset() sigset_t { return [_]SigsetElement{~@as(SigsetElement, 0)} ** sigset_len; } -fn sigset_bit_index(sig: usize) struct { word: usize, mask: SigsetElement } { - assert(sig > 0); - assert(sig < NSIG); - const bit = sig - 1; +fn sigset_bit_index(sig: SIG) struct { word: usize, mask: SigsetElement } { + assert(@intFromEnum(sig) > 0); + assert(@intFromEnum(sig) < NSIG); + const bit = @intFromEnum(sig) - 1; return .{ .word = bit / @bitSizeOf(SigsetElement), .mask = @as(SigsetElement, 1) << @truncate(bit % @bitSizeOf(SigsetElement)), }; } -pub fn sigaddset(set: *sigset_t, sig: usize) void { +pub fn sigaddset(set: *sigset_t, sig: SIG) void { const index = sigset_bit_index(sig); (set.*)[index.word] |= index.mask; } -pub fn sigdelset(set: *sigset_t, sig: usize) void { +pub fn sigdelset(set: *sigset_t, sig: SIG) void { const index = sigset_bit_index(sig); (set.*)[index.word] ^= index.mask; } -pub fn sigismember(set: *const sigset_t, sig: usize) bool { +pub fn sigismember(set: *const sigset_t, sig: SIG) bool { const index = sigset_bit_index(sig); return ((set.*)[index.word] & index.mask) != 0; } @@ -2635,11 +2635,11 @@ pub fn pidfd_getfd(pidfd: fd_t, targetfd: fd_t, flags: u32) usize { ); } -pub fn pidfd_send_signal(pidfd: fd_t, sig: i32, info: ?*siginfo_t, flags: u32) usize { +pub fn pidfd_send_signal(pidfd: fd_t, sig: SIG, info: ?*siginfo_t, flags: u32) usize { return syscall4( .pidfd_send_signal, @as(usize, @bitCast(@as(isize, pidfd))), - @as(usize, @bitCast(@as(isize, sig))), + @intFromEnum(sig), @intFromPtr(info), flags, ); @@ -3736,136 +3736,138 @@ pub const SA = if (is_mips) struct { pub const RESTORER = 0x04000000; }; -pub const SIG = if (is_mips) struct { +pub const SIG = if (is_mips) enum(u32) { pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 3; - // https://github.com/torvalds/linux/blob/ca91b9500108d4cf083a635c2e11c884d5dd20ea/arch/mips/include/uapi/asm/signal.h#L25 - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const USR1 = 16; - pub const USR2 = 17; - pub const CHLD = 18; - pub const PWR = 19; - pub const WINCH = 20; - pub const URG = 21; - pub const IO = 22; - pub const POLL = IO; - pub const STOP = 23; - pub const TSTP = 24; - pub const CONT = 25; - pub const TTIN = 26; - pub const TTOU = 27; - pub const VTALRM = 28; - pub const PROF = 29; - pub const XCPU = 30; - pub const XFZ = 31; - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); -} else if (is_sparc) struct { + + pub const IOT: SIG = .ABRT; + pub const POLL: SIG = .IO; + + // /arch/mips/include/uapi/asm/signal.h#L25 + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + USR1 = 16, + USR2 = 17, + CHLD = 18, + PWR = 19, + WINCH = 20, + URG = 21, + IO = 22, + STOP = 23, + TSTP = 24, + CONT = 25, + TTIN = 26, + TTOU = 27, + VTALRM = 28, + PROF = 29, + XCPU = 30, + XFZ = 31, +} else if (is_sparc) enum(u32) { pub const BLOCK = 1; pub const UNBLOCK = 2; pub const SETMASK = 4; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const EMT = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const BUS = 10; - pub const SEGV = 11; - pub const SYS = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const URG = 16; - pub const STOP = 17; - pub const TSTP = 18; - pub const CONT = 19; - pub const CHLD = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const POLL = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const LOST = 29; - pub const USR1 = 30; - pub const USR2 = 31; - pub const IOT = ABRT; - pub const CLD = CHLD; - pub const PWR = LOST; - pub const IO = SIG.POLL; - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); -} else struct { + + pub const IOT: SIG = .ABRT; + pub const CLD: SIG = .CHLD; + pub const PWR: SIG = .LOST; + pub const POLL: SIG = .IO; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + EMT = 7, + FPE = 8, + KILL = 9, + BUS = 10, + SEGV = 11, + SYS = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + URG = 16, + STOP = 17, + TSTP = 18, + CONT = 19, + CHLD = 20, + TTIN = 21, + TTOU = 22, + IO = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + LOST = 29, + USR1 = 30, + USR2 = 31, +} else enum(u32) { pub const BLOCK = 0; pub const UNBLOCK = 1; pub const SETMASK = 2; - pub const HUP = 1; - pub const INT = 2; - pub const QUIT = 3; - pub const ILL = 4; - pub const TRAP = 5; - pub const ABRT = 6; - pub const IOT = ABRT; - pub const BUS = 7; - pub const FPE = 8; - pub const KILL = 9; - pub const USR1 = 10; - pub const SEGV = 11; - pub const USR2 = 12; - pub const PIPE = 13; - pub const ALRM = 14; - pub const TERM = 15; - pub const STKFLT = 16; - pub const CHLD = 17; - pub const CONT = 18; - pub const STOP = 19; - pub const TSTP = 20; - pub const TTIN = 21; - pub const TTOU = 22; - pub const URG = 23; - pub const XCPU = 24; - pub const XFSZ = 25; - pub const VTALRM = 26; - pub const PROF = 27; - pub const WINCH = 28; - pub const IO = 29; - pub const POLL = 29; - pub const PWR = 30; - pub const SYS = 31; - pub const UNUSED = SIG.SYS; - pub const ERR: ?Sigaction.handler_fn = @ptrFromInt(maxInt(usize)); pub const DFL: ?Sigaction.handler_fn = @ptrFromInt(0); pub const IGN: ?Sigaction.handler_fn = @ptrFromInt(1); + + pub const POLL: SIG = .IO; + pub const IOT: SIG = .ABRT; + + HUP = 1, + INT = 2, + QUIT = 3, + ILL = 4, + TRAP = 5, + ABRT = 6, + BUS = 7, + FPE = 8, + KILL = 9, + USR1 = 10, + SEGV = 11, + USR2 = 12, + PIPE = 13, + ALRM = 14, + TERM = 15, + STKFLT = 16, + CHLD = 17, + CONT = 18, + STOP = 19, + TSTP = 20, + TTIN = 21, + TTOU = 22, + URG = 23, + XCPU = 24, + XFSZ = 25, + VTALRM = 26, + PROF = 27, + WINCH = 28, + IO = 29, + PWR = 30, + SYS = 31, }; pub const kernel_rwf = u32; @@ -5786,7 +5788,7 @@ pub const TFD = switch (native_arch) { }; const k_sigaction_funcs = struct { - const handler = ?*align(1) const fn (i32) callconv(.c) void; + const handler = ?*align(1) const fn (SIG) callconv(.c) void; const restorer = *const fn () callconv(.c) void; }; @@ -5817,8 +5819,8 @@ pub const k_sigaction = switch (native_arch) { /// /// Renamed from `sigaction` to `Sigaction` to avoid conflict with the syscall. pub const Sigaction = struct { - pub const handler_fn = *align(1) const fn (i32) callconv(.c) void; - pub const sigaction_fn = *const fn (i32, *const siginfo_t, ?*anyopaque) callconv(.c) void; + pub const handler_fn = *align(1) const fn (SIG) callconv(.c) void; + pub const sigaction_fn = *const fn (SIG, *const siginfo_t, ?*anyopaque) callconv(.c) void; handler: extern union { handler: ?handler_fn, @@ -6260,14 +6262,14 @@ const siginfo_fields_union = extern union { pub const siginfo_t = if (is_mips) extern struct { - signo: i32, + signo: SIG, code: i32, errno: i32, fields: siginfo_fields_union, } else extern struct { - signo: i32, + signo: SIG, errno: i32, code: i32, fields: siginfo_fields_union, diff --git a/lib/std/os/linux/test.zig b/lib/std/os/linux/test.zig index e38687dbde..6b658b87c0 100644 --- a/lib/std/os/linux/test.zig +++ b/lib/std/os/linux/test.zig @@ -1,5 +1,7 @@ -const std = @import("../../std.zig"); const builtin = @import("builtin"); + +const std = @import("../../std.zig"); +const assert = std.debug.assert; const linux = std.os.linux; const mem = std.mem; const elf = std.elf; @@ -128,58 +130,32 @@ test "fadvise" { } test "sigset_t" { - std.debug.assert(@sizeOf(linux.sigset_t) == (linux.NSIG / 8)); + const SIG = linux.SIG; + assert(@sizeOf(linux.sigset_t) == (linux.NSIG / 8)); var sigset = linux.sigemptyset(); // See that none are set, then set each one, see that they're all set, then // remove them all, and then see that none are set. for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), false); + const sig = std.meta.intToEnum(SIG, i) catch continue; + try expectEqual(false, linux.sigismember(&sigset, sig)); } for (1..linux.NSIG) |i| { - linux.sigaddset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(SIG, i) catch continue; + linux.sigaddset(&sigset, sig); } for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), true); + const sig = std.meta.intToEnum(SIG, i) catch continue; + try expectEqual(true, linux.sigismember(&sigset, sig)); } for (1..linux.NSIG) |i| { - linux.sigdelset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(SIG, i) catch continue; + linux.sigdelset(&sigset, sig); } for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), false); - } - - // Kernel sigset_t is either 2+ 32-bit values or 1+ 64-bit value(s). - const sigset_len = @typeInfo(linux.sigset_t).array.len; - const sigset_elemis64 = 64 == @bitSizeOf(@typeInfo(linux.sigset_t).array.child); - - linux.sigaddset(&sigset, 1); - try expectEqual(sigset[0], 1); - if (sigset_len > 1) { - try expectEqual(sigset[1], 0); - } - - linux.sigaddset(&sigset, 31); - try expectEqual(sigset[0], 0x4000_0001); - if (sigset_len > 1) { - try expectEqual(sigset[1], 0); - } - - linux.sigaddset(&sigset, 36); - if (sigset_elemis64) { - try expectEqual(sigset[0], 0x8_4000_0001); - } else { - try expectEqual(sigset[0], 0x4000_0001); - try expectEqual(sigset[1], 0x8); - } - - linux.sigaddset(&sigset, 64); - if (sigset_elemis64) { - try expectEqual(sigset[0], 0x8000_0008_4000_0001); - } else { - try expectEqual(sigset[0], 0x4000_0001); - try expectEqual(sigset[1], 0x8000_0008); + const sig = std.meta.intToEnum(SIG, i) catch continue; + try expectEqual(false, linux.sigismember(&sigset, sig)); } } @@ -187,14 +163,16 @@ test "sigfillset" { // unlike the C library, all the signals are set in the kernel-level fillset const sigset = linux.sigfillset(); for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), true); + const sig = std.meta.intToEnum(linux.SIG, i) catch continue; + try expectEqual(true, linux.sigismember(&sigset, sig)); } } test "sigemptyset" { const sigset = linux.sigemptyset(); for (1..linux.NSIG) |i| { - try expectEqual(linux.sigismember(&sigset, @truncate(i)), false); + const sig = std.meta.intToEnum(linux.SIG, i) catch continue; + try expectEqual(false, linux.sigismember(&sigset, sig)); } } @@ -208,14 +186,14 @@ test "sysinfo" { } comptime { - std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = true, .realtime = false }))); - std.debug.assert(256 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = false, .realtime = true }))); + assert(128 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = true, .realtime = false }))); + assert(256 == @as(u32, @bitCast(linux.FUTEX_OP{ .cmd = @enumFromInt(0), .private = false, .realtime = true }))); // Check futex_param4 union is packed correctly const param_union = linux.futex_param4{ .val2 = 0xaabbcc, }; - std.debug.assert(@intFromPtr(param_union.timeout) == 0xaabbcc); + assert(@intFromPtr(param_union.timeout) == 0xaabbcc); } test "futex v1" { @@ -298,8 +276,8 @@ test "futex v1" { } comptime { - std.debug.assert(2 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = .U32, .private = false }))); - std.debug.assert(128 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = @enumFromInt(0), .private = true }))); + assert(2 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = .U32, .private = false }))); + assert(128 == @as(u32, @bitCast(linux.FUTEX2_FLAGS{ .size = @enumFromInt(0), .private = true }))); } test "futex2_waitv" { diff --git a/lib/std/posix.zig b/lib/std/posix.zig index b240a6583d..56c2bad3c3 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -708,7 +708,7 @@ pub fn abort() noreturn { // for user-defined signal handlers that want to restore some state in // some program sections and crash in others. // So, the user-installed SIGABRT handler is run, if present. - raise(SIG.ABRT) catch {}; + raise(.ABRT) catch {}; // Disable all signal handlers. const filledset = linux.sigfillset(); @@ -728,17 +728,17 @@ pub fn abort() noreturn { .mask = sigemptyset(), .flags = 0, }; - sigaction(SIG.ABRT, &sigact, null); + sigaction(.ABRT, &sigact, null); - _ = linux.tkill(linux.gettid(), SIG.ABRT); + _ = linux.tkill(linux.gettid(), .ABRT); var sigabrtmask = sigemptyset(); - sigaddset(&sigabrtmask, SIG.ABRT); + sigaddset(&sigabrtmask, .ABRT); sigprocmask(SIG.UNBLOCK, &sigabrtmask, null); // Beyond this point should be unreachable. @as(*allowzero volatile u8, @ptrFromInt(0)).* = 0; - raise(SIG.KILL) catch {}; + raise(.KILL) catch {}; exit(127); // Pid 1 might not be signalled in some containers. } switch (native_os) { @@ -749,7 +749,7 @@ pub fn abort() noreturn { pub const RaiseError = UnexpectedError; -pub fn raise(sig: u8) RaiseError!void { +pub fn raise(sig: SIG) RaiseError!void { if (builtin.link_libc) { switch (errno(system.raise(sig))) { .SUCCESS => return, @@ -777,7 +777,7 @@ pub fn raise(sig: u8) RaiseError!void { pub const KillError = error{ ProcessNotFound, PermissionDenied } || UnexpectedError; -pub fn kill(pid: pid_t, sig: u8) KillError!void { +pub fn kill(pid: pid_t, sig: SIG) KillError!void { switch (errno(system.kill(pid, sig))) { .SUCCESS => return, .INVAL => unreachable, // invalid signal @@ -5235,7 +5235,7 @@ pub fn sigemptyset() sigset_t { return system.sigemptyset(); } -pub fn sigaddset(set: *sigset_t, sig: u8) void { +pub fn sigaddset(set: *sigset_t, sig: SIG) void { if (builtin.link_libc) { switch (errno(system.sigaddset(set, sig))) { .SUCCESS => return, @@ -5245,7 +5245,7 @@ pub fn sigaddset(set: *sigset_t, sig: u8) void { system.sigaddset(set, sig); } -pub fn sigdelset(set: *sigset_t, sig: u8) void { +pub fn sigdelset(set: *sigset_t, sig: SIG) void { if (builtin.link_libc) { switch (errno(system.sigdelset(set, sig))) { .SUCCESS => return, @@ -5255,7 +5255,7 @@ pub fn sigdelset(set: *sigset_t, sig: u8) void { system.sigdelset(set, sig); } -pub fn sigismember(set: *const sigset_t, sig: u8) bool { +pub fn sigismember(set: *const sigset_t, sig: SIG) bool { if (builtin.link_libc) { const rc = system.sigismember(set, sig); switch (errno(rc)) { @@ -5267,7 +5267,7 @@ pub fn sigismember(set: *const sigset_t, sig: u8) bool { } /// Examine and change a signal action. -pub fn sigaction(sig: u8, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) void { +pub fn sigaction(sig: SIG, noalias act: ?*const Sigaction, noalias oact: ?*Sigaction) void { switch (errno(system.sigaction(sig, act, oact))) { .SUCCESS => return, // EINVAL means the signal is either invalid or some signal that cannot have its action diff --git a/lib/std/posix/test.zig b/lib/std/posix/test.zig index 3e077fe300..946dd90027 100644 --- a/lib/std/posix/test.zig +++ b/lib/std/posix/test.zig @@ -536,14 +536,15 @@ test "sigset empty/full" { var set: posix.sigset_t = posix.sigemptyset(); for (1..posix.NSIG) |i| { - try expectEqual(false, posix.sigismember(&set, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(false, posix.sigismember(&set, sig)); } // The C library can reserve some (unnamed) signals, so can't check the full // NSIG set is defined, but just test a couple: set = posix.sigfillset(); - try expectEqual(true, posix.sigismember(&set, @truncate(posix.SIG.CHLD))); - try expectEqual(true, posix.sigismember(&set, @truncate(posix.SIG.INT))); + try expectEqual(true, posix.sigismember(&set, .CHLD)); + try expectEqual(true, posix.sigismember(&set, .INT)); } // Some signals (i.e., 32 - 34 on glibc/musl) are not allowed to be added to a @@ -564,25 +565,30 @@ test "sigset add/del" { // See that none are set, then set each one, see that they're all set, then // remove them all, and then see that none are set. for (1..posix.NSIG) |i| { - try expectEqual(false, posix.sigismember(&sigset, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(false, posix.sigismember(&sigset, sig)); } for (1..posix.NSIG) |i| { if (!reserved_signo(i)) { - posix.sigaddset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + posix.sigaddset(&sigset, sig); } } for (1..posix.NSIG) |i| { if (!reserved_signo(i)) { - try expectEqual(true, posix.sigismember(&sigset, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(true, posix.sigismember(&sigset, sig)); } } for (1..posix.NSIG) |i| { if (!reserved_signo(i)) { - posix.sigdelset(&sigset, @truncate(i)); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + posix.sigdelset(&sigset, sig); } } for (1..posix.NSIG) |i| { - try expectEqual(false, posix.sigismember(&sigset, @truncate(i))); + const sig = std.meta.intToEnum(posix.SIG, i) catch continue; + try expectEqual(false, posix.sigismember(&sigset, sig)); } } diff --git a/lib/std/start.zig b/lib/std/start.zig index 20dd981707..2a064f309d 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -758,45 +758,24 @@ pub fn call_wWinMain() std.os.windows.INT { } fn maybeIgnoreSignals() void { - switch (builtin.os.tag) { - .linux, - .plan9, - .illumos, - .netbsd, - .openbsd, - .haiku, - .macos, - .ios, - .watchos, - .tvos, - .visionos, - .dragonfly, - .freebsd, - .serenity, - => {}, - else => return, - } const posix = std.posix; + if (posix.Sigaction == void) return; const act: posix.Sigaction = .{ - // Set handler to a noop function instead of `SIG.IGN` to prevent + // Set handler to a noop function instead of `IGN` to prevent // leaking signal disposition to a child process. .handler = .{ .handler = noopSigHandler }, .mask = posix.sigemptyset(), .flags = 0, }; - if (@hasField(posix.SIG, "POLL") and !std.options.keep_sigpoll) - posix.sigaction(posix.SIG.POLL, &act, null); + if (@hasField(posix.SIG, "IO") and !std.options.keep_sig_io) + posix.sigaction(.IO, &act, null); - if (@hasField(posix.SIG, "IO") and - (!@hasField(posix.SIG, "POLL") or posix.SIG.IO != posix.SIG.POLL) and - !std.options.keep_sigio) - { - posix.sigaction(posix.SIG.IO, &act, null); - } + if (@hasField(posix.SIG, "POLL") and !std.options.keep_sig_poll) + posix.sigaction(.POLL, &act, null); - if (@hasField(posix.SIG, "PIPE") and !std.options.keep_sigpipe) - posix.sigaction(posix.SIG.PIPE, &act, null); + if (@hasField(posix.SIG, "PIPE") and !std.options.keep_sig_pipe) + posix.sigaction(.PIPE, &act, null); } -fn noopSigHandler(_: i32) callconv(.c) void {} +fn noopSigHandler(_: std.posix.SIG) callconv(.c) void {} diff --git a/lib/std/std.zig b/lib/std/std.zig index d1727ef3be..d47c1a6218 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -144,8 +144,8 @@ pub const Options = struct { crypto_fork_safety: bool = true, - keep_sigpoll: bool = false, - keep_sigio: bool = false, + keep_sig_poll: bool = false, + keep_sig_io: bool = false, /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option /// to `true` to prevent that. @@ -158,7 +158,7 @@ pub const Options = struct { /// cases it's unclear why the process was terminated. By capturing SIGPIPE instead, functions that /// write to broken pipes will return the EPIPE error (error.BrokenPipe) and the program can handle /// it like any other error. - keep_sigpipe: bool = false, + keep_sig_pipe: bool = false, /// By default, std.http.Client will support HTTPS connections. Set this option to `true` to /// disable TLS support. diff --git a/test/standalone/posix/sigaction.zig b/test/standalone/posix/sigaction.zig index 4f5f81ad66..8b2a6f3b98 100644 --- a/test/standalone/posix/sigaction.zig +++ b/test/standalone/posix/sigaction.zig @@ -17,12 +17,12 @@ fn test_sigaction() !void { return; // https://github.com/ziglang/zig/issues/15381 } - const test_signo = std.posix.SIG.URG; // URG only because it is ignored by default in debuggers + const test_signo: std.posix.SIG = .URG; // URG only because it is ignored by default in debuggers const S = struct { var handler_called_count: u32 = 0; - fn handler(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { + fn handler(sig: std.posix.SIG, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = ctx_ptr; // Check that we received the correct signal. const info_sig = switch (native_os) { @@ -80,20 +80,18 @@ fn test_sigaction() !void { } fn test_sigset_bits() !void { - const NO_SIG: i32 = 0; - const S = struct { - var expected_sig: i32 = undefined; - var seen_sig: i32 = NO_SIG; + var expected_sig: std.posix.SIG = undefined; + var seen_sig: ?std.posix.SIG = null; - fn handler(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { + fn handler(sig: std.posix.SIG, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) void { _ = ctx_ptr; const info_sig = switch (native_os) { .netbsd => info.info.signo, else => info.signo, }; - if (seen_sig == NO_SIG and sig == expected_sig and sig == info_sig) { + if (seen_sig == null and sig == expected_sig and sig == info_sig) { seen_sig = sig; } } @@ -107,11 +105,9 @@ fn test_sigset_bits() !void { // big-endian), try sending a blocked signal to make sure the mask matches the // signal. (Send URG and CHLD because they're ignored by default in the // debugger, vs. USR1 or other named signals) - inline for ([_]i32{ std.posix.SIG.URG, std.posix.SIG.CHLD, 62, 94, 126 }) |test_signo| { - if (test_signo >= std.posix.NSIG) continue; - + inline for ([_]std.posix.SIG{ .URG, .CHLD }) |test_signo| { S.expected_sig = test_signo; - S.seen_sig = NO_SIG; + S.seen_sig = null; const sa: std.posix.Sigaction = .{ .handler = .{ .sigaction = &S.handler }, @@ -135,14 +131,14 @@ fn test_sigset_bits() !void { switch (std.posix.errno(rc)) { .SUCCESS => { // See that the signal is blocked, then unblocked - try std.testing.expectEqual(NO_SIG, S.seen_sig); + try std.testing.expectEqual(null, S.seen_sig); std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &block_one, null); try std.testing.expectEqual(test_signo, S.seen_sig); }, .INVAL => { // Signal won't get delviered. Just clean up. std.posix.sigprocmask(std.posix.SIG.UNBLOCK, &block_one, null); - try std.testing.expectEqual(NO_SIG, S.seen_sig); + try std.testing.expectEqual(null, S.seen_sig); }, else => |errno| return std.posix.unexpectedErrno(errno), } From 80a341b4114ac171a2592423027e0ea4015fcf81 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 16:18:14 -0700 Subject: [PATCH 215/244] std: remove awareness of POLL signal this signal seems to be deprecated and/or useless on every target --- lib/std/start.zig | 3 --- lib/std/std.zig | 1 - 2 files changed, 4 deletions(-) diff --git a/lib/std/start.zig b/lib/std/start.zig index 2a064f309d..79e2d93134 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -771,9 +771,6 @@ fn maybeIgnoreSignals() void { if (@hasField(posix.SIG, "IO") and !std.options.keep_sig_io) posix.sigaction(.IO, &act, null); - if (@hasField(posix.SIG, "POLL") and !std.options.keep_sig_poll) - posix.sigaction(.POLL, &act, null); - if (@hasField(posix.SIG, "PIPE") and !std.options.keep_sig_pipe) posix.sigaction(.PIPE, &act, null); } diff --git a/lib/std/std.zig b/lib/std/std.zig index d47c1a6218..ff1976ae04 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -144,7 +144,6 @@ pub const Options = struct { crypto_fork_safety: bool = true, - keep_sig_poll: bool = false, keep_sig_io: bool = false, /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option From 3de1b6c9a98993e6735098f94a7d5214d5fbcfcf Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 16:34:35 -0700 Subject: [PATCH 216/244] std.Io.Threaded: better clock lowering for BSDs --- lib/std/Io/Threaded.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index c3d1102fcc..dae9089e90 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5009,7 +5009,12 @@ fn clockToPosix(clock: Io.Clock) posix.clockid_t { // On freebsd derivatives, use MONOTONIC_FAST as currently there's // no precision tradeoff. .freebsd, .dragonfly => posix.CLOCK.MONOTONIC_FAST, - else => posix.CLOCK.BOOTTIME, + // On linux, use BOOTTIME instead of MONOTONIC as it ticks while + // suspended. + .linux => posix.CLOCK.BOOTTIME, + // On other posix systems, MONOTONIC is generally the fastest and + // ticks while suspended. + else => posix.CLOCK.MONOTONIC, }, .cpu_process => posix.CLOCK.PROCESS_CPUTIME_ID, .cpu_thread => posix.CLOCK.THREAD_CPUTIME_ID, From c8739d6953b62de30827a484f94cc8add8c22dd9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 16:59:31 -0700 Subject: [PATCH 217/244] std.Io.Group: fix leak when wait is canceled Only when the operation succeeds should the token field be set to null. --- lib/std/Io.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 0f69f8ed9c..6ab366d484 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -1040,11 +1040,14 @@ pub const Group = struct { /// Blocks until all tasks of the group finish. /// - /// Idempotent. Not threadsafe. + /// On success, further calls to `wait`, `waitUncancelable`, and `cancel` + /// do nothing. + /// + /// Not threadsafe. pub fn wait(g: *Group, io: Io) Cancelable!void { const token = g.token orelse return; + try io.vtable.groupWait(io.userdata, g, token); g.token = null; - return io.vtable.groupWait(io.userdata, g, token); } /// Equivalent to `wait` except uninterruptible. From cc4931b3258ec764f0769ee573ed8a2989664d3a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 17:03:16 -0700 Subject: [PATCH 218/244] std.Io.Threaded: fix 32-bit overflow in lookupDns be a little more careful with nanoseconds --- lib/std/Io/Threaded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index dae9089e90..b3646cb88e 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5235,7 +5235,7 @@ fn lookupDns( var now_ts = try clock.now(t_io); const final_ts = now_ts.addDuration(.fromSeconds(rc.timeout_seconds)); const attempt_duration: Io.Duration = .{ - .nanoseconds = std.time.ns_per_s * @as(usize, rc.timeout_seconds) / rc.attempts, + .nanoseconds = (std.time.ns_per_s / rc.attempts) * @as(i96, rc.timeout_seconds), }; send: while (now_ts.nanoseconds < final_ts.nanoseconds) : (now_ts = try clock.now(t_io)) { From 6b8972dd22d13957fd0c844a8359c82cedcfe939 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 17:05:29 -0700 Subject: [PATCH 219/244] resinator: update for Io API --- lib/compiler/resinator/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compiler/resinator/main.zig b/lib/compiler/resinator/main.zig index 108b914377..b32237b06b 100644 --- a/lib/compiler/resinator/main.zig +++ b/lib/compiler/resinator/main.zig @@ -617,7 +617,7 @@ fn getIncludePaths( .cpu_arch = includes_arch, .abi = .msvc, }; - const target = std.zig.resolveTargetQueryOrFatal(target_query); + const target = std.zig.resolveTargetQueryOrFatal(io, target_query); const is_native_abi = target_query.isNativeAbi(); const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, &target, is_native_abi, true, null) catch { if (includes == .any) { From 135ec79f50ff36d48563be126c3e49793b04f302 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 17:49:21 -0700 Subject: [PATCH 220/244] std.Io.File: fix stat for Windows --- lib/std/Io/File.zig | 4 ---- lib/std/Io/Threaded.zig | 34 +++++++++++++++++----------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 29500ffb1d..22a904cc6a 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -395,10 +395,6 @@ pub const Reader = struct { pub fn getSize(r: *Reader) SizeError!u64 { return r.size orelse { if (r.size_err) |err| return err; - if (std.posix.Stat == void) { - r.size_err = error.Streaming; - return error.Streaming; - } if (stat(r.file, r.io)) |st| { if (st.kind == .file) { r.size = st.size; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index b3646cb88e..81b3d55a1f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2557,19 +2557,21 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); const DWORD = windows.DWORD; var index: usize = 0; while (data[index].len == 0) index += 1; - const buffer = data[index]; const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); - var n: DWORD = undefined; - if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) == 0) { + + while (true) { + try t.checkCancel(); + var n: DWORD = undefined; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) != 0) + return n; switch (windows.GetLastError()) { .IO_PENDING => |err| return windows.errorBug(err), - .OPERATION_ABORTED => return error.Canceled, + .OPERATION_ABORTED => continue, .BROKEN_PIPE => return 0, .HANDLE_EOF => return 0, .NETNAME_DELETED => return error.ConnectionResetByPeer, @@ -2579,7 +2581,6 @@ fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) else => |err| return windows.unexpectedError(err), } } - return n; } fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { @@ -2661,33 +2662,34 @@ const fileReadPositional = switch (native_os) { fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, offset: u64) Io.File.ReadPositionalError!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); const DWORD = windows.DWORD; - const OVERLAPPED = windows.OVERLAPPED; var index: usize = 0; while (data[index].len == 0) index += 1; - const buffer = data[index]; const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); - var n: DWORD = undefined; - var overlapped: OVERLAPPED = .{ + + var overlapped: windows.OVERLAPPED = .{ .Internal = 0, .InternalHigh = 0, .DUMMYUNIONNAME = .{ .DUMMYSTRUCTNAME = .{ - .Offset = @as(u32, @truncate(offset)), - .OffsetHigh = @as(u32, @truncate(offset >> 32)), + .Offset = @truncate(offset), + .OffsetHigh = @truncate(offset >> 32), }, }, .hEvent = null, }; - if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, &overlapped) == 0) { + while (true) { + try t.checkCancel(); + var n: DWORD = undefined; + if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, &overlapped) != 0) + return n; switch (windows.GetLastError()) { .IO_PENDING => |err| return windows.errorBug(err), - .OPERATION_ABORTED => return error.Canceled, + .OPERATION_ABORTED => continue, .BROKEN_PIPE => return 0, .HANDLE_EOF => return 0, .NETNAME_DELETED => return error.ConnectionResetByPeer, @@ -2697,8 +2699,6 @@ fn fileReadPositionalWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8, else => |err| return windows.unexpectedError(err), } } - - return n; } fn fileSeekBy(userdata: ?*anyopaque, file: Io.File, offset: i64) Io.File.SeekError!void { From 030ddc79528d41571b8329252c5f853d477a0f7b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 19:47:20 -0700 Subject: [PATCH 221/244] update standalone tests for ws2_32 dependency --- test/standalone/test_obj_link_run/build.zig | 1 + test/standalone/windows_spawn/main.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/standalone/test_obj_link_run/build.zig b/test/standalone/test_obj_link_run/build.zig index 45d35865c6..19d52ce68c 100644 --- a/test/standalone/test_obj_link_run/build.zig +++ b/test/standalone/test_obj_link_run/build.zig @@ -11,6 +11,7 @@ pub fn build(b: *std.Build) void { if (is_windows) { test_obj.linkSystemLibrary("ntdll"); test_obj.linkSystemLibrary("kernel32"); + test_obj.linkSystemLibrary("ws2_32"); } const test_exe_mod = b.createModule(.{ diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig index 8802f6efff..10ee35f4df 100644 --- a/test/standalone/windows_spawn/main.zig +++ b/test/standalone/windows_spawn/main.zig @@ -224,7 +224,7 @@ fn renameExe(dir: std.fs.Dir, old_sub_path: []const u8, new_sub_path: []const u8 error.AccessDenied => { if (attempt == 13) return error.AccessDenied; // give the kernel a chance to finish closing the executable handle - std.os.windows.kernel32.Sleep(@as(u32, 1) << attempt >> 1); + _ = std.os.windows.kernel32.SleepEx(@as(u32, 1) << attempt >> 1, std.os.windows.FALSE); attempt += 1; continue; }, From 30448d92af596724dc227c96803961bc41de9253 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 20:23:21 -0700 Subject: [PATCH 222/244] std.Io.Threaded: fix netLookup for Windows * respect address family option * fix port number using wrong buffer --- lib/std/Io/Threaded.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 81b3d55a1f..9becb59a7a 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4635,13 +4635,17 @@ fn netLookupFallible( var port_buffer_wide: [8]u16 = undefined; const port = std.fmt.bufPrint(&port_buffer, "{d}", .{options.port}) catch unreachable; // `port_buffer` is big enough for decimal u16. - for (port, port_buffer[0..port.len]) |byte, *wide| wide.* = byte; + for (port, port_buffer_wide[0..port.len]) |byte, *wide| + wide.* = std.mem.nativeToLittle(u16, byte); port_buffer_wide[port.len] = 0; const port_w = port_buffer_wide[0..port.len :0]; const hints: ws2_32.ADDRINFOEXW = .{ .flags = .{ .NUMERICSERV = true }, - .family = posix.AF.UNSPEC, + .family = if (options.family) |f| switch (f) { + .ip4 => posix.AF.INET, + .ip6 => posix.AF.INET6, + } else posix.AF.UNSPEC, .socktype = posix.SOCK.STREAM, .protocol = posix.IPPROTO.TCP, .canonname = null, From a28d3059e60ccbed2b2b159c41899488330bc671 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 21:17:44 -0700 Subject: [PATCH 223/244] std.Io.Threaded: implement ResetEvent in terms of pthreads needed for NetBSD --- lib/std/Io/Threaded.zig | 153 +++++++++++++++++++++++++++++++--------- lib/std/c.zig | 4 +- 2 files changed, 123 insertions(+), 34 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 9becb59a7a..35fecbedd9 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -5787,7 +5787,13 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { /// /// It can also block threads until the value is set with cancelation via timed /// waits. Statically initializable; four bytes on all targets. -pub const ResetEvent = enum(u32) { +pub const ResetEvent = switch (native_os) { + .netbsd => ResetEventPosix, + else => ResetEventFutex, +}; + +/// A `ResetEvent` implementation based on futexes. +const ResetEventFutex = enum(u32) { unset = 0, waiting = 1, is_set = 2, @@ -5798,15 +5804,15 @@ pub const ResetEvent = enum(u32) { /// /// The memory accesses before the `set` can be said to happen before /// `isSet` returns true. - pub fn isSet(re: *const ResetEvent) bool { - if (builtin.single_threaded) return switch (re.*) { + pub fn isSet(ref: *const ResetEventFutex) bool { + if (builtin.single_threaded) return switch (ref.*) { .unset => false, .waiting => unreachable, .is_set => true, }; // Acquire barrier ensures memory accesses before `set` happen before // returning true. - return @atomicLoad(ResetEvent, re, .acquire) == .is_set; + return @atomicLoad(ResetEventFutex, ref, .acquire) == .is_set; } /// Blocks the calling thread until `set` is called. @@ -5814,51 +5820,51 @@ pub const ResetEvent = enum(u32) { /// This is effectively a more efficient version of `while (!isSet()) {}`. /// /// The memory accesses before the `set` can be said to happen before `wait` returns. - pub fn wait(re: *ResetEvent, t: *Threaded) Io.Cancelable!void { - if (builtin.single_threaded) switch (re.*) { + pub fn wait(ref: *ResetEventFutex, t: *Threaded) Io.Cancelable!void { + if (builtin.single_threaded) switch (ref.*) { .unset => unreachable, // Deadlock, no other threads to wake us up. .waiting => unreachable, // Invalid state. .is_set => return, }; - if (re.isSet()) { + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEventFutex. Avoid using + // any strict barriers until we know the ResetEventFutex is set. + var state = @atomicLoad(ResetEventFutex, ref, .acquire); + if (state == .is_set) { @branchHint(.likely); return; } - // Try to set the state from `unset` to `waiting` to indicate to the - // `set` thread that others are blocked on the ResetEvent. Avoid using - // any strict barriers until we know the ResetEvent is set. - var state = @atomicLoad(ResetEvent, re, .acquire); if (state == .unset) { - state = @cmpxchgStrong(ResetEvent, re, state, .waiting, .acquire, .acquire) orelse .waiting; + state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting; } while (state == .waiting) { - try futexWait(t, @ptrCast(re), @intFromEnum(ResetEvent.waiting)); - state = @atomicLoad(ResetEvent, re, .acquire); + try futexWait(t, @ptrCast(ref), @intFromEnum(ResetEventFutex.waiting)); + state = @atomicLoad(ResetEventFutex, ref, .acquire); } assert(state == .is_set); } /// Same as `wait` except uninterruptible. - pub fn waitUncancelable(re: *ResetEvent) void { - if (builtin.single_threaded) switch (re.*) { + pub fn waitUncancelable(ref: *ResetEventFutex) void { + if (builtin.single_threaded) switch (ref.*) { .unset => unreachable, // Deadlock, no other threads to wake us up. .waiting => unreachable, // Invalid state. .is_set => return, }; - if (re.isSet()) { + // Try to set the state from `unset` to `waiting` to indicate to the + // `set` thread that others are blocked on the ResetEventFutex. Avoid using + // any strict barriers until we know the ResetEventFutex is set. + var state = @atomicLoad(ResetEventFutex, ref, .acquire); + if (state == .is_set) { @branchHint(.likely); return; } - // Try to set the state from `unset` to `waiting` to indicate to the - // `set` thread that others are blocked on the ResetEvent. Avoid using - // any strict barriers until we know the ResetEvent is set. - var state = @atomicLoad(ResetEvent, re, .acquire); if (state == .unset) { - state = @cmpxchgStrong(ResetEvent, re, state, .waiting, .acquire, .acquire) orelse .waiting; + state = @cmpxchgStrong(ResetEventFutex, ref, state, .waiting, .acquire, .acquire) orelse .waiting; } while (state == .waiting) { - futexWaitUncancelable(@ptrCast(re), @intFromEnum(ResetEvent.waiting)); - state = @atomicLoad(ResetEvent, re, .acquire); + futexWaitUncancelable(@ptrCast(ref), @intFromEnum(ResetEventFutex.waiting)); + state = @atomicLoad(ResetEventFutex, ref, .acquire); } assert(state == .is_set); } @@ -5871,26 +5877,109 @@ pub const ResetEvent = enum(u32) { /// /// The memory accesses before `set` can be said to happen before `isSet` /// returns true or `wait`/`timedWait` return successfully. - pub fn set(re: *ResetEvent) void { + pub fn set(ref: *ResetEventFutex) void { if (builtin.single_threaded) { - re.* = .is_set; + ref.* = .is_set; return; } - if (@atomicRmw(ResetEvent, re, .Xchg, .is_set, .release) == .waiting) { - futexWake(@ptrCast(re), std.math.maxInt(u32)); + if (@atomicRmw(ResetEventFutex, ref, .Xchg, .is_set, .release) == .waiting) { + futexWake(@ptrCast(ref), std.math.maxInt(u32)); } } - /// Unmarks the ResetEvent as if `set` was never called. + /// Unmarks the ResetEventFutex as if `set` was never called. /// /// Assumes no threads are blocked in `wait` or `timedWait`. Concurrent /// calls to `set`, `isSet` and `reset` are allowed. - pub fn reset(re: *ResetEvent) void { + pub fn reset(ref: *ResetEventFutex) void { if (builtin.single_threaded) { - re.* = .unset; + ref.* = .unset; return; } - @atomicStore(ResetEvent, re, .unset, .monotonic); + @atomicStore(ResetEventFutex, ref, .unset, .monotonic); + } +}; + +/// A `ResetEvent` implementation based on pthreads API. +const ResetEventPosix = struct { + cond: std.c.pthread_cond_t, + mutex: std.c.pthread_mutex_t, + state: ResetEventFutex, + + pub const unset: ResetEventPosix = .{ + .cond = std.c.PTHREAD_COND_INITIALIZER, + .mutex = std.c.PTHREAD_MUTEX_INITIALIZER, + .state = .unset, + }; + + pub fn isSet(rep: *const ResetEventPosix) bool { + if (builtin.single_threaded) return switch (rep.state) { + .unset => false, + .waiting => unreachable, + .is_set => true, + }; + return @atomicLoad(ResetEventFutex, &rep.state, .acquire) == .is_set; + } + + pub fn wait(rep: *ResetEventPosix, t: *Threaded) Io.Cancelable!void { + if (builtin.single_threaded) switch (rep.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS); + defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS); + sw: switch (rep.state) { + .unset => { + rep.state = .waiting; + continue :sw .waiting; + }, + .waiting => { + try t.checkCancel(); + assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS); + continue :sw rep.state; + }, + .is_set => return, + } + } + + pub fn waitUncancelable(rep: *ResetEventPosix) void { + if (builtin.single_threaded) switch (rep.*) { + .unset => unreachable, // Deadlock, no other threads to wake us up. + .waiting => unreachable, // Invalid state. + .is_set => return, + }; + assert(std.c.pthread_mutex_lock(&rep.mutex) == .SUCCESS); + defer assert(std.c.pthread_mutex_unlock(&rep.mutex) == .SUCCESS); + sw: switch (rep.state) { + .unset => { + rep.state = .waiting; + continue :sw .waiting; + }, + .waiting => { + assert(std.c.pthread_cond_wait(&rep.cond, &rep.mutex) == .SUCCESS); + continue :sw rep.state; + }, + .is_set => return, + } + } + + pub fn set(rep: *ResetEventPosix) void { + if (builtin.single_threaded) { + rep.* = .is_set; + return; + } + if (@atomicRmw(ResetEventFutex, &rep.state, .Xchg, .is_set, .release) == .waiting) { + assert(std.c.pthread_cond_broadcast(&rep.cond) == .SUCCESS); + } + } + + pub fn reset(rep: *ResetEventPosix) void { + if (builtin.single_threaded) { + rep.* = .unset; + return; + } + @atomicStore(ResetEventFutex, &rep.state, .unset, .monotonic); } }; diff --git a/lib/std/c.zig b/lib/std/c.zig index 89b28105ae..3eb2d76b3f 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -10874,13 +10874,13 @@ pub extern "c" fn dn_expand( length: c_int, ) c_int; -pub const PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t{}; +pub const PTHREAD_MUTEX_INITIALIZER: pthread_mutex_t = .{}; pub extern "c" fn pthread_mutex_lock(mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_mutex_unlock(mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_mutex_trylock(mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_mutex_destroy(mutex: *pthread_mutex_t) E; -pub const PTHREAD_COND_INITIALIZER = pthread_cond_t{}; +pub const PTHREAD_COND_INITIALIZER: pthread_cond_t = .{}; pub extern "c" fn pthread_cond_wait(noalias cond: *pthread_cond_t, noalias mutex: *pthread_mutex_t) E; pub extern "c" fn pthread_cond_timedwait(noalias cond: *pthread_cond_t, noalias mutex: *pthread_mutex_t, noalias abstime: *const timespec) E; pub extern "c" fn pthread_cond_signal(cond: *pthread_cond_t) E; From c0c20105354f05a354bf28fd5c05c19e7723b2a7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 21:49:45 -0700 Subject: [PATCH 224/244] std.c: fix msghdr struct on big endian targets --- lib/std/c.zig | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/std/c.zig b/lib/std/c.zig index 3eb2d76b3f..2f96404451 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -1,12 +1,14 @@ -const std = @import("std"); const builtin = @import("builtin"); +const native_abi = builtin.abi; +const native_arch = builtin.cpu.arch; +const native_os = builtin.os.tag; +const native_endian = builtin.cpu.arch.endian(); + +const std = @import("std"); const c = @This(); const maxInt = std.math.maxInt; const assert = std.debug.assert; const page_size = std.heap.page_size_min; -const native_abi = builtin.abi; -const native_arch = builtin.cpu.arch; -const native_os = builtin.os.tag; const linux = std.os.linux; const emscripten = std.os.emscripten; const wasi = std.os.wasi; @@ -4118,9 +4120,13 @@ const posix_msghdr = extern struct { name: ?*sockaddr, namelen: socklen_t, iov: [*]iovec, + pad0: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, iovlen: u32, + pad1: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, control: ?*anyopaque, + pad2: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, controllen: socklen_t, + pad3: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, flags: u32, }; @@ -4148,9 +4154,13 @@ const posix_msghdr_const = extern struct { name: ?*const sockaddr, namelen: socklen_t, iov: [*]const iovec_const, + pad0: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, iovlen: u32, + pad1: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, control: ?*const anyopaque, + pad2: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, controllen: socklen_t, + pad3: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, flags: u32, }; @@ -4193,7 +4203,9 @@ pub const cmsghdr = switch (native_os) { }; const posix_cmsghdr = extern struct { + pad0: if (@sizeOf(usize) == 8 and native_endian == .big) u32 else u0 = 0, len: socklen_t, + pad1: if (@sizeOf(usize) == 8 and native_endian == .little) u32 else u0 = 0, level: c_int, type: c_int, }; From 94b987498185af7af38f424964266daac6533ec3 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 22:16:03 -0700 Subject: [PATCH 225/244] std.Io.Threaded: stub out netbsd mutex and condition --- lib/std/Io/Threaded.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 35fecbedd9..63db82558e 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -798,6 +798,7 @@ fn checkCancel(t: *Threaded) error{Canceled}!void { fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) Io.Cancelable!void { if (builtin.single_threaded) unreachable; // Interface should have prevented this. + if (native_os == .netbsd) @panic("TODO"); const t: *Threaded = @ptrCast(@alignCast(userdata)); if (prev_state == .contended) { try futexWait(t, @ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); @@ -809,6 +810,7 @@ fn mutexLock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { if (builtin.single_threaded) unreachable; // Interface should have prevented this. + if (native_os == .netbsd) @panic("TODO"); _ = userdata; if (prev_state == .contended) { futexWaitUncancelable(@ptrCast(&mutex.state), @intFromEnum(Io.Mutex.State.contended)); @@ -820,6 +822,7 @@ fn mutexLockUncancelable(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mute fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mutex) void { if (builtin.single_threaded) unreachable; // Interface should have prevented this. + if (native_os == .netbsd) @panic("TODO"); _ = userdata; _ = prev_state; if (@atomicRmw(Io.Mutex.State, &mutex.state, .Xchg, .unlocked, .release) == .contended) { @@ -829,6 +832,7 @@ fn mutexUnlock(userdata: ?*anyopaque, prev_state: Io.Mutex.State, mutex: *Io.Mut fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) void { if (builtin.single_threaded) unreachable; // Deadlock. + if (native_os == .netbsd) @panic("TODO"); const t: *Threaded = @ptrCast(@alignCast(userdata)); const t_io = ioBasic(t); comptime assert(@TypeOf(cond.state) == u64); @@ -860,6 +864,7 @@ fn conditionWaitUncancelable(userdata: ?*anyopaque, cond: *Io.Condition, mutex: fn conditionWait(userdata: ?*anyopaque, cond: *Io.Condition, mutex: *Io.Mutex) Io.Cancelable!void { if (builtin.single_threaded) unreachable; // Deadlock. + if (native_os == .netbsd) @panic("TODO"); const t: *Threaded = @ptrCast(@alignCast(userdata)); const t_io = ioBasic(t); comptime assert(@TypeOf(cond.state) == u64); @@ -960,6 +965,7 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) _ = cond_epoch.fetchAdd(1, .release); + if (native_os == .netbsd) @panic("TODO"); futexWake(cond_epoch, to_wake); return; }; From f5870b267e96613fa31883dc89d469d132b1b5cd Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 27 Oct 2025 22:21:07 -0700 Subject: [PATCH 226/244] std.Io.Threaded: fix typo in panic message --- lib/std/Io/Threaded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 63db82558e..58317e4d18 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1226,7 +1226,7 @@ fn dirMakeOpenPathWasi( _ = dir; _ = sub_path; _ = mode; - @panic("TODO implement dirMakeOpenPathWindows"); + @panic("TODO implement dirMakeOpenPathWasi"); } fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { From 4114392369c28a2455a508e4da67d945ebad2b44 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 05:50:53 -0700 Subject: [PATCH 227/244] std: fix definition of ws2_32.GetAddrInfoExW There was a missing parameter. --- lib/std/Io/Threaded.zig | 18 +++++++++--------- lib/std/os/windows/ws2_32.zig | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 58317e4d18..36b53f1a4d 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -4662,13 +4662,14 @@ fn netLookupFallible( .provider = null, .next = null, }; + const cancel_handle: ?*windows.HANDLE = null; var res: *ws2_32.ADDRINFOEXW = undefined; const timeout: ?*ws2_32.timeval = null; while (true) { try t.checkCancel(); // TODO make requestCancel call GetAddrInfoExCancel // TODO make this append to the queue eagerly rather than blocking until // the whole thing finishes - const rc: ws2_32.WinsockError = @enumFromInt(ws2_32.GetAddrInfoExW(name_w, port_w, .DNS, null, &hints, &res, timeout, null, null)); + const rc: ws2_32.WinsockError = @enumFromInt(ws2_32.GetAddrInfoExW(name_w, port_w, .DNS, null, &hints, &res, timeout, null, null, cancel_handle)); switch (rc) { @as(ws2_32.WinsockError, @enumFromInt(0)) => break, .EINTR => continue, @@ -5732,17 +5733,16 @@ pub fn futexWake(ptr: *const std.atomic.Value(u32), max_waiters: u32) void { } else switch (native_os) { .linux => { const linux = std.os.linux; - const rc = linux.futex_3arg( + switch (linux.E.init(linux.futex_3arg( &ptr.raw, .{ .cmd = .WAKE, .private = true }, @min(max_waiters, std.math.maxInt(i32)), - ); - if (is_debug) switch (linux.E.init(rc)) { - .SUCCESS => {}, // successful wake up - .INVAL => {}, // invalid futex_wait() on ptr done elsewhere - .FAULT => {}, // pointer became invalid while doing the wake - else => unreachable, // deadlock due to operating system bug - }; + ))) { + .SUCCESS => return, // successful wake up + .INVAL => return, // invalid futex_wait() on ptr done elsewhere + .FAULT => return, // pointer became invalid while doing the wake + else => return recoverableOsBugDetected(), // deadlock due to operating system bug + } }, .driverkit, .ios, .macos, .tvos, .visionos, .watchos => { const c = std.c; diff --git a/lib/std/os/windows/ws2_32.zig b/lib/std/os/windows/ws2_32.zig index 5d5e29c00e..d5c74e2212 100644 --- a/lib/std/os/windows/ws2_32.zig +++ b/lib/std/os/windows/ws2_32.zig @@ -2138,6 +2138,7 @@ pub extern "ws2_32" fn GetAddrInfoExW( timeout: ?*timeval, lpOverlapped: ?*OVERLAPPED, lpCompletionRoutine: ?LPLOOKUPSERVICE_COMPLETION_ROUTINE, + lpNameHandle: ?*HANDLE, ) callconv(.winapi) i32; pub extern "ws2_32" fn GetAddrInfoExCancel( From b39f3d294da3ce9075bbc08651965615839219f2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 06:33:30 -0700 Subject: [PATCH 228/244] std.Io.Threaded: implement dirMakeOpenPath for WASI and fix error code when file operation occurs on director handle --- lib/std/Io.zig | 2 +- lib/std/Io/File.zig | 48 ++++++++++++++++++++--------------------- lib/std/Io/Kqueue.zig | 2 +- lib/std/Io/Threaded.zig | 23 +++++++++++--------- lib/std/posix.zig | 2 +- 5 files changed, 40 insertions(+), 37 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 6ab366d484..cf07d8526f 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -684,7 +684,7 @@ pub const VTable = struct { fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize, fileWritePositional: *const fn (?*anyopaque, File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize, /// Returns 0 on end of stream. - fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.ReadStreamingError!usize, + fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.Reader.Error!usize, /// Returns 0 on end of stream. fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, fileSeekBy: *const fn (?*anyopaque, File, relative_offset: i64) File.SeekError!void, diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 22a904cc6a..ada57344e4 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -213,29 +213,7 @@ pub fn openSelfExe(io: Io, flags: OpenFlags) OpenSelfExeError!File { return io.vtable.openSelfExe(io.userdata, flags); } -pub const ReadStreamingError = error{ - InputOutput, - SystemResources, - IsDir, - BrokenPipe, - ConnectionResetByPeer, - Timeout, - NotOpenForReading, - SocketUnconnected, - /// This error occurs when no global event loop is configured, - /// and reading from the file descriptor would block. - WouldBlock, - /// In WASI, this error occurs when the file descriptor does - /// not hold the required rights to read from it. - AccessDenied, - /// This error occurs in Linux if the process to be read from - /// no longer exists. - ProcessNotFound, - /// Unable to read file due to lock. - LockViolation, -} || Io.Cancelable || Io.UnexpectedError; - -pub const ReadPositionalError = ReadStreamingError || error{Unseekable}; +pub const ReadPositionalError = Reader.Error || error{Unseekable}; pub fn readPositional(file: File, io: Io, buffer: []u8, offset: u64) ReadPositionalError!usize { return io.vtable.fileReadPositional(io.userdata, file, buffer, offset); @@ -301,7 +279,29 @@ pub const Reader = struct { seek_err: ?Reader.SeekError = null, interface: Io.Reader, - pub const Error = std.posix.ReadError || Io.Cancelable; + pub const Error = error{ + InputOutput, + SystemResources, + IsDir, + BrokenPipe, + ConnectionResetByPeer, + Timeout, + /// In WASI, EBADF is mapped to this error because it is returned when + /// trying to read a directory file descriptor as if it were a file. + NotOpenForReading, + SocketUnconnected, + /// This error occurs when no global event loop is configured, + /// and reading from the file descriptor would block. + WouldBlock, + /// In WASI, this error occurs when the file descriptor does + /// not hold the required rights to read from it. + AccessDenied, + /// This error occurs in Linux if the process to be read from + /// no longer exists. + ProcessNotFound, + /// Unable to read file due to lock. + LockViolation, + } || Io.Cancelable || Io.UnexpectedError; pub const SizeError = std.os.windows.GetFileSizeError || StatError || error{ /// Occurs if, for example, the file handle is a network socket and therefore does not have a size. diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index fd5baaddde..87ef302fb5 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -1226,7 +1226,7 @@ fn fileWritePositional(userdata: ?*anyopaque, file: File, buffer: [][]const u8, _ = offset; @panic("TODO"); } -fn fileReadStreaming(userdata: ?*anyopaque, file: File, data: [][]u8) File.ReadStreamingError!usize { +fn fileReadStreaming(userdata: ?*anyopaque, file: File, data: [][]u8) File.Reader.Error!usize { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; _ = file; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 36b53f1a4d..38a94f352d 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1219,14 +1219,17 @@ fn dirMakeOpenPathWasi( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, - mode: Io.Dir.OpenOptions, + options: Io.Dir.OpenOptions, ) Io.Dir.MakeOpenPathError!Io.Dir { const t: *Threaded = @ptrCast(@alignCast(userdata)); - _ = t; - _ = dir; - _ = sub_path; - _ = mode; - @panic("TODO implement dirMakeOpenPathWasi"); + const t_io = ioBasic(t); + return dirOpenDirWasi(t, dir, sub_path, options) catch |err| switch (err) { + error.FileNotFound => { + try dir.makePath(t_io, sub_path); + return dirOpenDirWasi(t, dir, sub_path, options); + }, + else => |e| return e, + }; } fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { @@ -2498,7 +2501,7 @@ const fileReadStreaming = switch (native_os) { else => fileReadStreamingPosix, }; -fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { +fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); var iovecs_buffer: [max_iovecs_len]posix.iovec = undefined; @@ -2523,7 +2526,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .BADF => return error.NotOpenForReading, // File operation on directory. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -2561,7 +2564,7 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io } } -fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.ReadStreamingError!usize { +fn fileReadStreamingWindows(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io.File.Reader.Error!usize { const t: *Threaded = @ptrCast(@alignCast(userdata)); const DWORD = windows.DWORD; @@ -2617,7 +2620,7 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, o .INVAL => |err| return errnoBug(err), .FAULT => |err| return errnoBug(err), .AGAIN => |err| return errnoBug(err), - .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .BADF => return error.NotOpenForReading, // File operation on directory. .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 56c2bad3c3..7952727e9a 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -814,7 +814,7 @@ pub fn exit(status: u8) noreturn { system.exit(status); } -pub const ReadError = std.Io.File.ReadStreamingError; +pub const ReadError = std.Io.File.Reader.Error; /// Returns the number of bytes that were read, which can be less than /// buf.len. If 0 bytes were read, that means EOF. From c4dc7d7c3d4eade5f1092eb5c4af9bf45e3e2fda Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 06:58:58 -0700 Subject: [PATCH 229/244] std.Io.Threaded: implement Unix sockets for Windows --- lib/std/Io/Threaded.zig | 101 +++++++++++++++++++++++++++++++++++++--- lib/std/Io/net.zig | 1 + 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 38a94f352d..63242810c3 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -3172,10 +3172,62 @@ fn netListenUnixWindows( ) net.UnixAddress.ListenError!net.Socket.Handle { if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); - _ = address; - _ = options; - @panic("TODO implement netListenUnixWindows"); + + const socket_handle = openSocketWsa(t, posix.AF.UNIX, .{ .mode = .stream }) catch |err| switch (err) { + error.ProtocolUnsupportedByAddressFamily => return error.AddressFamilyUnsupported, + else => |e| return e, + }; + errdefer closeSocketWindows(socket_handle); + + var storage: WsaAddress = undefined; + const addr_len = addressUnixToWsa(address, &storage); + + while (true) { + try t.checkCancel(); + const rc = ws2_32.bind(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .EADDRINUSE => return error.AddressInUse, + .EADDRNOTAVAIL => return error.AddressUnavailable, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .ENOBUFS => return error.SystemResources, + .ENETDOWN => return error.NetworkDown, + else => |err| return windows.unexpectedWSAError(err), + } + } + + while (true) { + try t.checkCancel(); + const rc = ws2_32.listen(socket_handle, options.kernel_backlog); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + .ENETDOWN => return error.NetworkDown, + .EADDRINUSE => return error.AddressInUse, + .EISCONN => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EMFILE, .ENOBUFS => return error.SystemResources, + .ENOTSOCK => |err| return wsaErrorBug(err), + .EOPNOTSUPP => |err| return wsaErrorBug(err), + .EINPROGRESS => |err| return wsaErrorBug(err), + else => |err| return windows.unexpectedWSAError(err), + } + } + + return socket_handle; } fn netListenUnixUnavailable( @@ -3493,9 +3545,37 @@ fn netConnectUnixWindows( ) net.UnixAddress.ConnectError!net.Socket.Handle { if (!net.has_unix_sockets) return error.AddressFamilyUnsupported; const t: *Threaded = @ptrCast(@alignCast(userdata)); - try t.checkCancel(); - _ = address; - @panic("TODO implement netConnectUnixWindows"); + + const socket_handle = try openSocketWsa(t, posix.AF.UNIX, .{ .mode = .stream }); + errdefer closeSocketWindows(socket_handle); + var storage: WsaAddress = undefined; + const addr_len = addressUnixToWsa(address, &storage); + + while (true) { + const rc = ws2_32.connect(socket_handle, &storage.any, addr_len); + if (rc != ws2_32.SOCKET_ERROR) break; + switch (ws2_32.WSAGetLastError()) { + .EINTR => continue, + .ECANCELLED, .E_CANCELLED, .OPERATION_ABORTED => return error.Canceled, + .NOTINITIALISED => { + try initializeWsa(t); + continue; + }, + + .ECONNREFUSED => return error.FileNotFound, + .EFAULT => |err| return wsaErrorBug(err), + .EINVAL => |err| return wsaErrorBug(err), + .EISCONN => |err| return wsaErrorBug(err), + .ENOTSOCK => |err| return wsaErrorBug(err), + .EWOULDBLOCK => return error.WouldBlock, + .EACCES => return error.AccessDenied, + .ENOBUFS => return error.SystemResources, + .EAFNOSUPPORT => return error.AddressFamilyUnsupported, + else => |err| return windows.unexpectedWSAError(err), + } + } + + return socket_handle; } fn netConnectUnixUnavailable( @@ -4935,6 +5015,13 @@ fn addressUnixToPosix(a: *const net.UnixAddress, storage: *UnixAddress) posix.so return @sizeOf(posix.sockaddr.un); } +fn addressUnixToWsa(a: *const net.UnixAddress, storage: *WsaAddress) i32 { + @memcpy(storage.un.path[0..a.path.len], a.path); + storage.un.family = posix.AF.UNIX; + storage.un.path[a.path.len] = 0; + return @sizeOf(posix.sockaddr.un); +} + fn address4FromPosix(in: *const posix.sockaddr.in) net.Ip4Address { return .{ .port = std.mem.bigToNative(u16, in.port), diff --git a/lib/std/Io/net.zig b/lib/std/Io/net.zig index 362ff424a8..b691b1501e 100644 --- a/lib/std/Io/net.zig +++ b/lib/std/Io/net.zig @@ -879,6 +879,7 @@ pub const UnixAddress = struct { NotDir, ReadOnlyFileSystem, WouldBlock, + NetworkDown, } || Io.Cancelable || Io.UnexpectedError; pub fn connect(ua: *const UnixAddress, io: Io) ConnectError!Stream { From 030b630829d3fe9c6c387a9bf68ae65ddb3b053c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 07:11:52 -0700 Subject: [PATCH 230/244] std.Io.Threaded: fix EBADF error code on wasm32-wasi -lc when reading --- lib/std/Io/Threaded.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 63242810c3..5c17a848ea 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2253,6 +2253,7 @@ const dirOpenDir = switch (native_os) { else => dirOpenDirPosix, }; +/// This function is also used for WASI when libc is linked. fn dirOpenDirPosix( userdata: ?*anyopaque, dir: Io.Dir, @@ -2551,7 +2552,10 @@ fn fileReadStreamingPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8) Io .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, - .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .BADF => |err| { + if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. + return errnoBug(err); // File descriptor used after closed. + }, .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, @@ -2648,7 +2652,10 @@ fn fileReadPositionalPosix(userdata: ?*anyopaque, file: Io.File, data: [][]u8, o .FAULT => |err| return errnoBug(err), .SRCH => return error.ProcessNotFound, .AGAIN => return error.WouldBlock, - .BADF => |err| return errnoBug(err), // File descriptor used after closed. + .BADF => |err| { + if (native_os == .wasi) return error.NotOpenForReading; // File operation on directory. + return errnoBug(err); // File descriptor used after closed. + }, .IO => return error.InputOutput, .ISDIR => return error.IsDir, .NOBUFS => return error.SystemResources, From 1553c8eae7d830cb793e78c3e52ad816fa8efd23 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 08:27:07 -0700 Subject: [PATCH 231/244] std.os.linux.x86: fix signal restore function After handling any signal on x86, it would previously segfault. --- lib/std/os/linux/x86.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/std/os/linux/x86.zig b/lib/std/os/linux/x86.zig index 3770607f55..a68a4af317 100644 --- a/lib/std/os/linux/x86.zig +++ b/lib/std/os/linux/x86.zig @@ -159,12 +159,14 @@ pub fn clone() callconv(.naked) u32 { pub fn restore() callconv(.naked) noreturn { switch (builtin.zig_backend) { .stage2_c => asm volatile ( + \\ addl $4, %%esp \\ movl %[number], %%eax \\ int $0x80 : : [number] "i" (@intFromEnum(SYS.sigreturn)), ), else => asm volatile ( + \\ addl $4, %%esp \\ int $0x80 : : [number] "{eax}" (@intFromEnum(SYS.sigreturn)), From 6f64c8b693446a6f8d170bb09324e5c5e506ce6f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 08:47:39 -0700 Subject: [PATCH 232/244] std.debug.SelfInfo.Windows: less invasive change restores code closer to master branch in hopes of avoiding a regression that was introduced when this was based on openSelfExe rather than GetModuleFileNameExW. --- lib/std/Io/Threaded.zig | 6 +++--- lib/std/debug/SelfInfo/Windows.zig | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 5c17a848ea..83eea97990 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2052,10 +2052,10 @@ fn dirOpenFileWindows( const sub_path_w_array = try windows.sliceToPrefixedFileW(dir.handle, sub_path); const sub_path_w = sub_path_w_array.span(); const dir_handle = if (std.fs.path.isAbsoluteWindowsWtf16(sub_path_w)) null else dir.handle; - return dirOpenFileWindowsInner(t, dir_handle, sub_path_w, flags); + return dirOpenFileWtf16(t, dir_handle, sub_path_w, flags); } -fn dirOpenFileWindowsInner( +pub fn dirOpenFileWtf16( t: *Threaded, dir_handle: ?windows.HANDLE, sub_path_w: [:0]const u16, @@ -2800,7 +2800,7 @@ fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelf const image_path_unicode_string = &windows.peb().ProcessParameters.ImagePathName; const image_path_name = image_path_unicode_string.Buffer.?[0 .. image_path_unicode_string.Length / 2 :0]; const prefixed_path_w = try windows.wToPrefixedFileW(null, image_path_name); - return dirOpenFileWindowsInner(t, null, prefixed_path_w.span(), flags); + return dirOpenFileWtf16(t, null, prefixed_path_w.span(), flags); }, else => @panic("TODO implement openSelfExe"), } diff --git a/lib/std/debug/SelfInfo/Windows.zig b/lib/std/debug/SelfInfo/Windows.zig index 51c41030dc..70009217db 100644 --- a/lib/std/debug/SelfInfo/Windows.zig +++ b/lib/std/debug/SelfInfo/Windows.zig @@ -297,7 +297,19 @@ const Module = struct { // a binary is produced with -gdwarf, since the section names are longer than 8 bytes. const mapped_file: ?DebugInfo.MappedFile = mapped: { if (!coff_obj.strtabRequired()) break :mapped null; - const coff_file = Io.File.openSelfExe(io, .{}) catch |err| switch (err) { + var name_buffer: [windows.PATH_MAX_WIDE + 4:0]u16 = undefined; + name_buffer[0..4].* = .{ '\\', '?', '?', '\\' }; // openFileAbsoluteW requires the prefix to be present + const process_handle = windows.GetCurrentProcess(); + const len = windows.kernel32.GetModuleFileNameExW( + process_handle, + module.handle, + name_buffer[4..], + windows.PATH_MAX_WIDE, + ); + if (len == 0) return error.MissingDebugInfo; + const name_w = name_buffer[0 .. len + 4 :0]; + var threaded: Io.Threaded = .init_single_threaded; + const coff_file = threaded.dirOpenFileWtf16(null, name_w, .{}) catch |err| switch (err) { error.Canceled => |e| return e, error.Unexpected => |e| return e, error.FileNotFound => return error.MissingDebugInfo, @@ -327,12 +339,6 @@ const Module = struct { error.SystemFdQuotaExceeded, error.FileLocksNotSupported, error.FileBusy, - error.InputOutput, - error.NotSupported, - error.FileSystem, - error.NotLink, - error.UnrecognizedVolume, - error.UnknownName, => return error.ReadFailed, }; errdefer coff_file.close(io); @@ -352,7 +358,6 @@ const Module = struct { errdefer windows.CloseHandle(section_handle); var coff_len: usize = 0; var section_view_ptr: ?[*]const u8 = null; - const process_handle = windows.GetCurrentProcess(); const map_section_rc = windows.ntdll.NtMapViewOfSection( section_handle, process_handle, From 9f986419bd0409c0b6b7630f0c7222b4137ead4f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 09:27:04 -0700 Subject: [PATCH 233/244] CI: link ws2_32.lib on Windows --- ci/x86_64-windows-debug.ps1 | 2 +- ci/x86_64-windows-release.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/x86_64-windows-debug.ps1 b/ci/x86_64-windows-debug.ps1 index 6cd28db466..9349a74547 100644 --- a/ci/x86_64-windows-debug.ps1 +++ b/ci/x86_64-windows-debug.ps1 @@ -95,7 +95,7 @@ Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\ CheckLastExitCode Write-Output "Build and run behavior tests with msvc..." -& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib +& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib ws2_32.lib CheckLastExitCode & .\test-x86_64-windows-msvc.exe diff --git a/ci/x86_64-windows-release.ps1 b/ci/x86_64-windows-release.ps1 index f3cdd66f48..a348c72fe9 100644 --- a/ci/x86_64-windows-release.ps1 +++ b/ci/x86_64-windows-release.ps1 @@ -113,7 +113,7 @@ Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\ CheckLastExitCode Write-Output "Build and run behavior tests with msvc..." -& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib +& cl.exe -I..\lib test-x86_64-windows-msvc.c compiler_rt-x86_64-windows-msvc.c /W3 /Z7 -link -nologo -debug -subsystem:console kernel32.lib ntdll.lib libcmt.lib ws2_32.lib CheckLastExitCode & .\test-x86_64-windows-msvc.exe From 6c794ce7bceeefceeee43a5514e633162ee946a9 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 11:04:59 -0700 Subject: [PATCH 234/244] std.Io.Threaded.dirOpenFileWtf16: SHARING_VIOLATION is the error code that needs the kernel bug workaround, not ACCESS_DENIED. --- lib/std/Io/File.zig | 6 +++--- lib/std/Io/Threaded.zig | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index ada57344e4..cc4ce64a22 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -221,13 +221,13 @@ pub fn readPositional(file: File, io: Io, buffer: []u8, offset: u64) ReadPositio pub const WriteStreamingError = error{} || Io.UnexpectedError || Io.Cancelable; -pub fn write(file: File, io: Io, buffer: []const u8) WriteStreamingError!usize { - return @errorCast(file.pwrite(io, buffer, -1)); +pub fn writeStreaming(file: File, io: Io, buffer: [][]const u8) WriteStreamingError!usize { + return file.fileWriteStreaming(io, buffer); } pub const WritePositionalError = WriteStreamingError || error{Unseekable}; -pub fn writePositional(file: File, io: Io, buffer: []const u8, offset: u64) WritePositionalError!usize { +pub fn writePositional(file: File, io: Io, buffer: [][]const u8, offset: u64) WritePositionalError!usize { return io.vtable.fileWritePositional(io.userdata, file, buffer, offset); } diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 83eea97990..345775b9eb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2120,18 +2120,18 @@ pub fn dirOpenFileWtf16( .BAD_NETWORK_NAME => return error.NetworkNotFound, // \\server was found but \\server\share wasn't .NO_MEDIA_IN_DEVICE => return error.NoDevice, .INVALID_PARAMETER => |err| return w.statusBug(err), - .SHARING_VIOLATION => return error.AccessDenied, - .ACCESS_DENIED => { + .SHARING_VIOLATION => { // This occurs if the file attempting to be opened is a running // executable. However, there's a kernel bug: the error may be // incorrectly returned for an indeterminate amount of time // after an executable file is closed. Here we work around the // kernel bug with retry attempts. - if (attempt - max_attempts == 0) return error.AccessDenied; + if (attempt - max_attempts == 0) return error.SharingViolation; _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE); attempt += 1; continue; }, + .ACCESS_DENIED => return error.AccessDenied, .PIPE_BUSY => return error.PipeBusy, .PIPE_NOT_AVAILABLE => return error.NoDevice, .OBJECT_PATH_SYNTAX_BAD => |err| return w.statusBug(err), @@ -2146,7 +2146,7 @@ pub fn dirOpenFileWtf16( // finished with the deletion operation, and so this CreateFile // call has failed. Here, we simulate the kernel bug being // fixed by sleeping and retrying until the error goes away. - if (attempt - max_attempts == 0) return error.AccessDenied; + if (attempt - max_attempts == 0) return error.SharingViolation; _ = w.kernel32.SleepEx((@as(u32, 1) << attempt) >> 1, w.TRUE); attempt += 1; continue; From 03fd132b1ce82f767ffdb6cc886d1934b40c6071 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 15:06:07 -0700 Subject: [PATCH 235/244] std.Io: fix Group.wait unsoundness Previously if a Group.wait was canceled, then a subsequent call to wait() or cancel() would trip an assertion in the synchronization code. --- lib/std/Io.zig | 26 ++++--------- lib/std/Io/Kqueue.zig | 11 +----- lib/std/Io/Threaded.zig | 73 ++++++++++++------------------------- lib/std/Io/net/HostName.zig | 5 +-- 4 files changed, 33 insertions(+), 82 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index cf07d8526f..68598203b5 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -653,8 +653,7 @@ pub const VTable = struct { context_alignment: std.mem.Alignment, start: *const fn (*Group, context: *const anyopaque) void, ) void, - groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) Cancelable!void, - groupWaitUncancelable: *const fn (?*anyopaque, *Group, token: *anyopaque) void, + groupWait: *const fn (?*anyopaque, *Group, token: *anyopaque) void, groupCancel: *const fn (?*anyopaque, *Group, token: *anyopaque) void, /// Blocks until one of the futures from the list has a result ready, such @@ -1038,29 +1037,18 @@ pub const Group = struct { io.vtable.groupAsync(io.userdata, g, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); } - /// Blocks until all tasks of the group finish. - /// - /// On success, further calls to `wait`, `waitUncancelable`, and `cancel` - /// do nothing. - /// - /// Not threadsafe. - pub fn wait(g: *Group, io: Io) Cancelable!void { - const token = g.token orelse return; - try io.vtable.groupWait(io.userdata, g, token); - g.token = null; - } - - /// Equivalent to `wait` except uninterruptible. + /// Blocks until all tasks of the group finish. During this time, + /// cancellation requests propagate to all members of the group. /// /// Idempotent. Not threadsafe. - pub fn waitUncancelable(g: *Group, io: Io) void { + pub fn wait(g: *Group, io: Io) void { const token = g.token orelse return; g.token = null; - io.vtable.groupWaitUncancelable(io.userdata, g, token); + io.vtable.groupWait(io.userdata, g, token); } - /// Equivalent to `wait` but requests cancellation on all tasks owned by - /// the group. + /// Equivalent to `wait` but immediately requests cancellation on all + /// members of the group. /// /// Idempotent. Not threadsafe. pub fn cancel(g: *Group, io: Io) void { diff --git a/lib/std/Io/Kqueue.zig b/lib/std/Io/Kqueue.zig index 87ef302fb5..5b4f71da08 100644 --- a/lib/std/Io/Kqueue.zig +++ b/lib/std/Io/Kqueue.zig @@ -859,7 +859,6 @@ pub fn io(k: *Kqueue) Io { .groupAsync = groupAsync, .groupWait = groupWait, - .groupWaitUncancelable = groupWaitUncancelable, .groupCancel = groupCancel, .mutexLock = mutexLock, @@ -1027,15 +1026,7 @@ fn groupAsync( @panic("TODO"); } -fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) Io.Cancelable!void { - const k: *Kqueue = @ptrCast(@alignCast(userdata)); - _ = k; - _ = group; - _ = token; - @panic("TODO"); -} - -fn groupWaitUncancelable(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { const k: *Kqueue = @ptrCast(@alignCast(userdata)); _ = k; _ = group; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 345775b9eb..995b9eb43c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -177,7 +177,6 @@ pub fn io(t: *Threaded) Io { .groupAsync = groupAsync, .groupWait = groupWait, - .groupWaitUncancelable = groupWaitUncancelable, .groupCancel = groupCancel, .mutexLock = mutexLock, @@ -274,7 +273,6 @@ pub fn ioBasic(t: *Threaded) Io { .groupAsync = groupAsync, .groupWait = groupWait, - .groupWaitUncancelable = groupWaitUncancelable, .groupCancel = groupCancel, .mutexLock = mutexLock, @@ -579,7 +577,9 @@ const GroupClosure = struct { assert(cancel_tid == .canceling); } - syncFinish(group_state, reset_event); + const prev_state = group_state.fetchSub(sync_one_pending, .acq_rel); + assert((prev_state / sync_one_pending) > 0); + if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set(); } fn free(gc: *GroupClosure, gpa: Allocator) void { @@ -602,29 +602,6 @@ const GroupClosure = struct { const sync_is_waiting: usize = 1 << 0; const sync_one_pending: usize = 1 << 1; - - fn syncStart(state: *std.atomic.Value(usize)) void { - const prev_state = state.fetchAdd(sync_one_pending, .monotonic); - assert((prev_state / sync_one_pending) < (std.math.maxInt(usize) / sync_one_pending)); - } - - fn syncFinish(state: *std.atomic.Value(usize), event: *ResetEvent) void { - const prev_state = state.fetchSub(sync_one_pending, .acq_rel); - assert((prev_state / sync_one_pending) > 0); - if (prev_state == (sync_one_pending | sync_is_waiting)) event.set(); - } - - fn syncWait(t: *Threaded, state: *std.atomic.Value(usize), event: *ResetEvent) Io.Cancelable!void { - const prev_state = state.fetchAdd(sync_is_waiting, .acquire); - assert(prev_state & sync_is_waiting == 0); - if ((prev_state / sync_one_pending) > 0) try event.wait(t); - } - - fn syncWaitUncancelable(state: *std.atomic.Value(usize), event: *ResetEvent) void { - const prev_state = state.fetchAdd(sync_is_waiting, .acquire); - assert(prev_state & sync_is_waiting == 0); - if ((prev_state / sync_one_pending) > 0) event.waitUncancelable(); - } }; fn groupAsync( @@ -686,13 +663,14 @@ fn groupAsync( // This needs to be done before unlocking the mutex to avoid a race with // the associated task finishing. const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); - GroupClosure.syncStart(group_state); + const prev_state = group_state.fetchAdd(GroupClosure.sync_one_pending, .monotonic); + assert((prev_state / GroupClosure.sync_one_pending) < (std.math.maxInt(usize) / GroupClosure.sync_one_pending)); t.mutex.unlock(); t.cond.signal(); } -fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) Io.Cancelable!void { +fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { const t: *Threaded = @ptrCast(@alignCast(userdata)); const gpa = t.allocator; @@ -700,26 +678,19 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) Io.Canc const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); - try GroupClosure.syncWait(t, group_state, reset_event); - - var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); - while (true) { - const gc: *GroupClosure = @fieldParentPtr("node", node); - const node_next = node.next; - gc.free(gpa); - node = node_next orelse break; - } -} - -fn groupWaitUncancelable(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { - const t: *Threaded = @ptrCast(@alignCast(userdata)); - const gpa = t.allocator; - - if (builtin.single_threaded) return; - - const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); - const reset_event: *ResetEvent = @ptrCast(&group.context); - GroupClosure.syncWaitUncancelable(group_state, reset_event); + const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire); + assert(prev_state & GroupClosure.sync_is_waiting == 0); + if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.wait(t) catch |err| switch (err) { + error.Canceled => { + var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); + while (true) { + const gc: *GroupClosure = @fieldParentPtr("node", node); + gc.closure.requestCancel(); + node = node.next orelse break; + } + reset_event.waitUncancelable(); + }, + }; var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); while (true) { @@ -747,7 +718,9 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void const group_state: *std.atomic.Value(usize) = @ptrCast(&group.state); const reset_event: *ResetEvent = @ptrCast(&group.context); - GroupClosure.syncWaitUncancelable(group_state, reset_event); + const prev_state = group_state.fetchAdd(GroupClosure.sync_is_waiting, .acquire); + assert(prev_state & GroupClosure.sync_is_waiting == 0); + if ((prev_state / GroupClosure.sync_one_pending) > 0) reset_event.waitUncancelable(); { var node: *std.SinglyLinkedList.Node = @ptrCast(@alignCast(token)); @@ -1549,7 +1522,7 @@ fn dirAccessPosix( .FAULT => |err| return errnoBug(err), .IO => return error.InputOutput, .NOMEM => return error.SystemResources, - .ILSEQ => return error.BadPathName, // TODO move to wasi + .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), } } diff --git a/lib/std/Io/net/HostName.zig b/lib/std/Io/net/HostName.zig index 9a10b36239..35aeff6942 100644 --- a/lib/std/Io/net/HostName.zig +++ b/lib/std/Io/net/HostName.zig @@ -280,9 +280,8 @@ pub fn connectMany( .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }), .canonical_name => continue, .end => |lookup_result| { - results.putOneUncancelable(io, .{ - .end = if (group.wait(io)) lookup_result else |err| err, - }); + group.wait(io); + results.putOneUncancelable(io, .{ .end = lookup_result }); return; }, } else |err| switch (err) { From c40204a3e521b82094fb44b2ce412f47d05964b2 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 18:04:55 -0700 Subject: [PATCH 236/244] std.Io: add unit tests for Group and concurrent --- lib/std/Io/Threaded.zig | 4 +++ lib/std/Io/Threaded/test.zig | 46 ++++++++++++++++++++++++++++++++++ lib/std/Io/test.zig | 48 +++++++++++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 lib/std/Io/Threaded/test.zig diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 995b9eb43c..109b467c41 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -6114,3 +6114,7 @@ fn initializeWsa(t: *Threaded) error{NetworkDown}!void { } return error.NetworkDown; } + +test { + _ = @import("Threaded/test.zig"); +} diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig new file mode 100644 index 0000000000..6b8b858075 --- /dev/null +++ b/lib/std/Io/Threaded/test.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const Io = std.Io; +const testing = std.testing; +const assert = std.debug.assert; + +test "concurrent vs main prevents deadlock via oversubscription" { + var threaded: Io.Threaded = .init(std.testing.allocator); + defer threaded.deinit(); + const io = threaded.io(); + + threaded.cpu_count = 1; + + var queue: Io.Queue(u8) = .init(&.{}); + + var putter = try io.concurrent(put, .{ io, &queue }); + defer putter.cancel(io); + + try testing.expectEqual(42, queue.getOneUncancelable(io)); +} + +fn put(io: Io, queue: *Io.Queue(u8)) void { + queue.putOneUncancelable(io, 42); +} + +fn get(io: Io, queue: *Io.Queue(u8)) void { + assert(queue.getOneUncancelable(io) == 42); +} + +test "concurrent vs concurrent prevents deadlock via oversubscription" { + var threaded: Io.Threaded = .init(std.testing.allocator); + defer threaded.deinit(); + const io = threaded.io(); + + threaded.cpu_count = 1; + + var queue: Io.Queue(u8) = .init(&.{}); + + var putter = try io.concurrent(put, .{ io, &queue }); + defer putter.cancel(io); + + var getter = try io.concurrent(get, .{ io, &queue }); + defer getter.cancel(io); + + getter.await(io); + putter.await(io); +} diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 22f211d6e4..f47da2c9e1 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -1,4 +1,8 @@ +const builtin = @import("builtin"); +const native_endian = builtin.cpu.arch.endian(); + const std = @import("std"); +const Io = std.Io; const DefaultPrng = std.Random.DefaultPrng; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; @@ -6,7 +10,6 @@ const expectError = std.testing.expectError; const mem = std.mem; const fs = std.fs; const File = std.fs.File; -const native_endian = @import("builtin").target.cpu.arch.endian(); const tmpDir = std.testing.tmpDir; @@ -123,3 +126,46 @@ test "updateTimes" { try expect(stat_new.atime.nanoseconds < stat_old.atime.nanoseconds); try expect(stat_new.mtime.nanoseconds < stat_old.mtime.nanoseconds); } + +test "Group" { + const io = std.testing.io; + + var group: Io.Group = .init; + var results: [2]usize = undefined; + + group.async(io, count, .{ 1, 10, &results[0] }); + group.async(io, count, .{ 20, 30, &results[1] }); + + group.wait(io); + + try std.testing.expectEqualSlices(usize, &.{ 45, 245 }, &results); +} + +fn count(a: usize, b: usize, result: *usize) void { + var sum: usize = 0; + for (a..b) |i| { + sum += i; + } + result.* = sum; +} + +test "Group cancellation" { + const io = std.testing.io; + + var group: Io.Group = .init; + var results: [2]usize = undefined; + + group.async(io, sleep, .{ io, &results[0] }); + group.async(io, sleep, .{ io, &results[1] }); + + group.cancel(io); + + try std.testing.expectEqualSlices(usize, &.{ 1, 1 }, &results); +} + +fn sleep(io: Io, result: *usize) void { + // TODO when cancellation race bug is fixed, make this timeout much longer so that + // it causes the unit test to be failed if not cancelled. + io.sleep(.fromMilliseconds(1), .awake) catch {}; + result.* = 1; +} From a45cafb7f0d182be9d2652229583297e55eac5c1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 18:13:49 -0700 Subject: [PATCH 237/244] std.Io: add unit test for select --- lib/std/Io.zig | 4 +-- lib/std/Io/test.zig | 61 ++++++++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 68598203b5..d6b6fb7979 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -1632,7 +1632,7 @@ pub fn SelectUnion(S: type) type { /// `s` is a struct with every field a `*Future(T)`, where `T` can be any type, /// and can be different for each field. -pub fn select(io: Io, s: anytype) SelectUnion(@TypeOf(s)) { +pub fn select(io: Io, s: anytype) Cancelable!SelectUnion(@TypeOf(s)) { const U = SelectUnion(@TypeOf(s)); const S = @TypeOf(s); const fields = @typeInfo(S).@"struct".fields; @@ -1641,7 +1641,7 @@ pub fn select(io: Io, s: anytype) SelectUnion(@TypeOf(s)) { const future = @field(s, field.name); any_future.* = future.any_future orelse return @unionInit(U, field.name, future.result); } - switch (io.vtable.select(io.userdata, &futures)) { + switch (try io.vtable.select(io.userdata, &futures)) { inline 0...(fields.len - 1) => |selected_index| { const field_name = fields[selected_index].name; return @unionInit(U, field_name, @field(s, field_name).await(io)); diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index f47da2c9e1..002a082ee8 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -3,24 +3,26 @@ const native_endian = builtin.cpu.arch.endian(); const std = @import("std"); const Io = std.Io; -const DefaultPrng = std.Random.DefaultPrng; +const testing = std.testing; const expect = std.testing.expect; const expectEqual = std.testing.expectEqual; const expectError = std.testing.expectError; +const DefaultPrng = std.Random.DefaultPrng; const mem = std.mem; const fs = std.fs; const File = std.fs.File; +const assert = std.debug.assert; const tmpDir = std.testing.tmpDir; test "write a file, read it, then delete it" { - const io = std.testing.io; + const io = testing.io; var tmp = tmpDir(.{}); defer tmp.cleanup(); var data: [1024]u8 = undefined; - var prng = DefaultPrng.init(std.testing.random_seed); + var prng = DefaultPrng.init(testing.random_seed); const random = prng.random(); random.bytes(data[0..]); const tmp_file_name = "temp_test_file.txt"; @@ -51,8 +53,8 @@ test "write a file, read it, then delete it" { var file_buffer: [1024]u8 = undefined; var file_reader = file.reader(io, &file_buffer); - const contents = try file_reader.interface.allocRemaining(std.testing.allocator, .limited(2 * 1024)); - defer std.testing.allocator.free(contents); + const contents = try file_reader.interface.allocRemaining(testing.allocator, .limited(2 * 1024)); + defer testing.allocator.free(contents); try expect(mem.eql(u8, contents[0.."begin".len], "begin")); try expect(mem.eql(u8, contents["begin".len .. contents.len - "end".len], &data)); @@ -94,18 +96,18 @@ test "setEndPos" { defer file.close(); // Verify that the file size changes and the file offset is not moved - try std.testing.expect((try file.getEndPos()) == 0); - try std.testing.expect((try file.getPos()) == 0); + try expect((try file.getEndPos()) == 0); + try expect((try file.getPos()) == 0); try file.setEndPos(8192); - try std.testing.expect((try file.getEndPos()) == 8192); - try std.testing.expect((try file.getPos()) == 0); + try expect((try file.getEndPos()) == 8192); + try expect((try file.getPos()) == 0); try file.seekTo(100); try file.setEndPos(4096); - try std.testing.expect((try file.getEndPos()) == 4096); - try std.testing.expect((try file.getPos()) == 100); + try expect((try file.getEndPos()) == 4096); + try expect((try file.getPos()) == 100); try file.setEndPos(0); - try std.testing.expect((try file.getEndPos()) == 0); - try std.testing.expect((try file.getPos()) == 100); + try expect((try file.getEndPos()) == 0); + try expect((try file.getPos()) == 100); } test "updateTimes" { @@ -128,7 +130,7 @@ test "updateTimes" { } test "Group" { - const io = std.testing.io; + const io = testing.io; var group: Io.Group = .init; var results: [2]usize = undefined; @@ -138,7 +140,7 @@ test "Group" { group.wait(io); - try std.testing.expectEqualSlices(usize, &.{ 45, 245 }, &results); + try testing.expectEqualSlices(usize, &.{ 45, 245 }, &results); } fn count(a: usize, b: usize, result: *usize) void { @@ -150,7 +152,7 @@ fn count(a: usize, b: usize, result: *usize) void { } test "Group cancellation" { - const io = std.testing.io; + const io = testing.io; var group: Io.Group = .init; var results: [2]usize = undefined; @@ -160,7 +162,7 @@ test "Group cancellation" { group.cancel(io); - try std.testing.expectEqualSlices(usize, &.{ 1, 1 }, &results); + try testing.expectEqualSlices(usize, &.{ 1, 1 }, &results); } fn sleep(io: Io, result: *usize) void { @@ -169,3 +171,28 @@ fn sleep(io: Io, result: *usize) void { io.sleep(.fromMilliseconds(1), .awake) catch {}; result.* = 1; } + +test "select" { + const io = testing.io; + + var queue: Io.Queue(u8) = .init(&.{}); + + var get_a = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }); + defer if (get_a.cancel(io)) |_| @panic("fail") else |err| assert(err == error.Canceled); + + var get_b = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }); + defer if (get_b.cancel(io)) |_| @panic("fail") else |err| assert(err == error.Canceled); + + var timeout = io.async(Io.sleep, .{ io, .fromMilliseconds(1), .awake }); + defer timeout.cancel(io) catch {}; + + switch (try io.select(.{ + .get_a = &get_a, + .get_b = &get_b, + .timeout = &timeout, + })) { + .get_a => return error.TestFailure, + .get_b => return error.TestFailure, + .timeout => {}, + } +} From 1c0a8b8fe4a979b0ba9e6cd790c33a6dc12f0f05 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 18:32:55 -0700 Subject: [PATCH 238/244] std.hash_map: tune slow unit tests These are the only two unit tests that take longer than 1s on my computer. --- lib/std/hash_map.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/std/hash_map.zig b/lib/std/hash_map.zig index 85424fe45e..83d4b5fae3 100644 --- a/lib/std/hash_map.zig +++ b/lib/std/hash_map.zig @@ -1827,9 +1827,9 @@ test "put and remove loop in random order" { } } -test "remove one million elements in random order" { +test "remove many elements in random order" { const Map = AutoHashMap(u32, u32); - const n = 1000 * 1000; + const n = 1000 * 100; var map = Map.init(std.heap.page_allocator); defer map.deinit(); @@ -2147,14 +2147,14 @@ test "getOrPut allocation failure" { try testing.expectError(error.OutOfMemory, map.getOrPut(std.testing.failing_allocator, "hello")); } -test "std.hash_map rehash" { +test "rehash" { var map = AutoHashMap(usize, usize).init(std.testing.allocator); defer map.deinit(); var prng = std.Random.DefaultPrng.init(0); const random = prng.random(); - const count = 6 * random.intRangeLessThan(u32, 100_000, 500_000); + const count = 4 * random.intRangeLessThan(u32, 100_000, 500_000); for (0..count) |i| { try map.put(i, i); From b863f2548b95cdc8b2e2818ac3a26e553be155dc Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 18:43:47 -0700 Subject: [PATCH 239/244] std.Io.Threaded: handle -fsingle-threaded in unit tests --- lib/std/Io/Threaded/test.zig | 16 ++++++++++++++-- lib/std/Io/test.zig | 7 ++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 6b8b858075..ef24b25f34 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -1,3 +1,5 @@ +const builtin = @import("builtin"); + const std = @import("std"); const Io = std.Io; const testing = std.testing; @@ -12,7 +14,12 @@ test "concurrent vs main prevents deadlock via oversubscription" { var queue: Io.Queue(u8) = .init(&.{}); - var putter = try io.concurrent(put, .{ io, &queue }); + var putter = io.concurrent(put, .{ io, &queue }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; defer putter.cancel(io); try testing.expectEqual(42, queue.getOneUncancelable(io)); @@ -35,7 +42,12 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" { var queue: Io.Queue(u8) = .init(&.{}); - var putter = try io.concurrent(put, .{ io, &queue }); + var putter = io.concurrent(put, .{ io, &queue }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; defer putter.cancel(io); var getter = try io.concurrent(get, .{ io, &queue }); diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 002a082ee8..fcced60677 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -177,7 +177,12 @@ test "select" { var queue: Io.Queue(u8) = .init(&.{}); - var get_a = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }); + var get_a = io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; defer if (get_a.cancel(io)) |_| @panic("fail") else |err| assert(err == error.Canceled); var get_b = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }); From 05b28409e7204f78e3edb32ea9c46b10e4e97cef Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 18:45:53 -0700 Subject: [PATCH 240/244] std.Io.Threaded: install and cleanup signal handlers rather than in start code. delete std.options.keep_sig_io and std.options.keep_sig_pipe --- lib/std/Io/Threaded.zig | 38 +++++++++++++++++++++++++++++++++++++- lib/std/posix.zig | 1 + lib/std/start.zig | 21 --------------------- lib/std/std.zig | 15 --------------- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 109b467c41..168395d335 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -26,8 +26,13 @@ threads: std.ArrayListUnmanaged(std.Thread), stack_size: usize, cpu_count: std.Thread.CpuCountError!usize, concurrent_count: usize, + wsa: if (is_windows) Wsa else struct {} = .{}, +have_signal_handler: bool, +old_sig_io: if (have_sig_io) posix.Sigaction else void, +old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void, + threadlocal var current_closure: ?*Closure = null; const max_iovecs_len = 8; @@ -104,23 +109,46 @@ pub fn init( .stack_size = std.Thread.SpawnConfig.default_stack_size, .cpu_count = std.Thread.getCpuCount(), .concurrent_count = 0, + .old_sig_io = undefined, + .old_sig_pipe = undefined, + .have_signal_handler = false, }; + if (t.cpu_count) |n| { t.threads.ensureTotalCapacityPrecise(gpa, n - 1) catch {}; } else |_| {} + + if (posix.Sigaction != void) { + // This causes sending `posix.SIG.IO` to thread to interrupt blocking + // syscalls, returning `posix.E.INTR`. + const act: posix.Sigaction = .{ + .handler = .{ .handler = doNothingSignalHandler }, + .mask = posix.sigemptyset(), + .flags = 0, + }; + if (have_sig_io) posix.sigaction(.IO, &act, &t.old_sig_io); + if (have_sig_pipe) posix.sigaction(.PIPE, &act, &t.old_sig_pipe); + t.have_signal_handler = true; + } + return t; } /// Statically initialize such that calls to `Io.VTable.concurrent` will fail /// with `error.ConcurrencyUnavailable`. /// -/// When initialized this way, `deinit` is safe, but unnecessary to call. +/// When initialized this way: +/// * cancel requests have no effect. +/// * `deinit` is safe, but unnecessary to call. pub const init_single_threaded: Threaded = .{ .allocator = .failing, .threads = .empty, .stack_size = std.Thread.SpawnConfig.default_stack_size, .cpu_count = 1, .concurrent_count = 0, + .old_sig_io = undefined, + .old_sig_pipe = undefined, + .have_signal_handler = false, }; pub fn deinit(t: *Threaded) void { @@ -130,6 +158,10 @@ pub fn deinit(t: *Threaded) void { if (is_windows and t.wsa.status == .initialized) { if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected(); } + if (posix.Sigaction != void and t.have_signal_handler) { + if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null); + if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null); + } t.* = undefined; } @@ -338,6 +370,8 @@ const have_preadv = switch (native_os) { .windows, .haiku, .serenity => false, // 💩💩💩 else => true, }; +const have_sig_io = posix.SIG != void and @hasField(posix.SIG, "IO"); +const have_sig_pipe = posix.SIG != void and @hasField(posix.SIG, "PIPE"); const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; @@ -6115,6 +6149,8 @@ fn initializeWsa(t: *Threaded) error{NetworkDown}!void { return error.NetworkDown; } +fn doNothingSignalHandler(_: posix.SIG) callconv(.c) void {} + test { _ = @import("Threaded/test.zig"); } diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 7952727e9a..6faf6bbe72 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -55,6 +55,7 @@ else switch (native_os) { pub const mode_t = u0; pub const ino_t = void; pub const IFNAMESIZE = {}; + pub const SIG = void; }, }; diff --git a/lib/std/start.zig b/lib/std/start.zig index 79e2d93134..a563912bbc 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -651,7 +651,6 @@ inline fn callMainWithArgs(argc: usize, argv: [*][*:0]u8, envp: [][*:0]u8) u8 { std.os.argv = argv[0..argc]; std.os.environ = envp; - maybeIgnoreSignals(); std.debug.maybeEnableSegfaultHandler(); return callMain(); @@ -756,23 +755,3 @@ pub fn call_wWinMain() std.os.windows.INT { // second parameter hPrevInstance, MSDN: "This parameter is always NULL" return root.wWinMain(hInstance, null, lpCmdLine, nCmdShow); } - -fn maybeIgnoreSignals() void { - const posix = std.posix; - if (posix.Sigaction == void) return; - const act: posix.Sigaction = .{ - // Set handler to a noop function instead of `IGN` to prevent - // leaking signal disposition to a child process. - .handler = .{ .handler = noopSigHandler }, - .mask = posix.sigemptyset(), - .flags = 0, - }; - - if (@hasField(posix.SIG, "IO") and !std.options.keep_sig_io) - posix.sigaction(.IO, &act, null); - - if (@hasField(posix.SIG, "PIPE") and !std.options.keep_sig_pipe) - posix.sigaction(.PIPE, &act, null); -} - -fn noopSigHandler(_: std.posix.SIG) callconv(.c) void {} diff --git a/lib/std/std.zig b/lib/std/std.zig index ff1976ae04..1b8142ce4c 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -144,21 +144,6 @@ pub const Options = struct { crypto_fork_safety: bool = true, - keep_sig_io: bool = false, - - /// By default Zig disables SIGPIPE by setting a "no-op" handler for it. Set this option - /// to `true` to prevent that. - /// - /// Note that we use a "no-op" handler instead of SIG_IGN because it will not be inherited by - /// any child process. - /// - /// SIGPIPE is triggered when a process attempts to write to a broken pipe. By default, SIGPIPE - /// will terminate the process instead of exiting. It doesn't trigger the panic handler so in many - /// cases it's unclear why the process was terminated. By capturing SIGPIPE instead, functions that - /// write to broken pipes will return the EPIPE error (error.BrokenPipe) and the program can handle - /// it like any other error. - keep_sig_pipe: bool = false, - /// By default, std.http.Client will support HTTPS connections. Set this option to `true` to /// disable TLS support. /// From b1d270d38ed1fc36a5625f9b0f0f110b6dc12ec7 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 19:21:53 -0700 Subject: [PATCH 241/244] std.os.linux.s390x: fix restore function --- lib/std/os/linux/s390x.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/std/os/linux/s390x.zig b/lib/std/os/linux/s390x.zig index 13b6bfd512..0a09982f2a 100644 --- a/lib/std/os/linux/s390x.zig +++ b/lib/std/os/linux/s390x.zig @@ -136,7 +136,13 @@ pub fn clone() callconv(.naked) u64 { ); } -pub const restore = restore_rt; +pub fn restore() callconv(.naked) noreturn { + asm volatile ( + \\svc 0 + : + : [number] "{r1}" (@intFromEnum(SYS.sigreturn)), + ); +} pub fn restore_rt() callconv(.naked) noreturn { asm volatile ( From e8e1e2793a2021d37743b33f28c0e9846525068d Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 28 Oct 2025 22:00:55 -0700 Subject: [PATCH 242/244] std.Io: make select unit test not depend on cancellation --- lib/std/Io/test.zig | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index fcced60677..52aca5ddcf 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -183,10 +183,10 @@ test "select" { return; }, }; - defer if (get_a.cancel(io)) |_| @panic("fail") else |err| assert(err == error.Canceled); + defer if (get_a.cancel(io)) |_| {} else |_| @panic("fail"); var get_b = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io }); - defer if (get_b.cancel(io)) |_| @panic("fail") else |err| assert(err == error.Canceled); + defer if (get_b.cancel(io)) |_| {} else |_| @panic("fail"); var timeout = io.async(Io.sleep, .{ io, .fromMilliseconds(1), .awake }); defer timeout.cancel(io) catch {}; @@ -198,6 +198,13 @@ test "select" { })) { .get_a => return error.TestFailure, .get_b => return error.TestFailure, - .timeout => {}, + .timeout => { + // Unblock the queues to avoid making this unit test depend on + // cancellation. + queue.putOneUncancelable(io, 1); + queue.putOneUncancelable(io, 1); + try testing.expectEqual(1, try get_a.await(io)); + try testing.expectEqual(1, try get_b.await(io)); + }, } } From 0205ce4736467a55129bdaecc0fd4296dfc3b7ea Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 29 Oct 2025 06:20:58 -0700 Subject: [PATCH 243/244] std.os.linux.IoUring: disable failing test tracked by https://github.com/ziglang/zig/issues/25734 --- lib/std/os/linux/IoUring.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/std/os/linux/IoUring.zig b/lib/std/os/linux/IoUring.zig index 3b2f753a37..60c24be227 100644 --- a/lib/std/os/linux/IoUring.zig +++ b/lib/std/os/linux/IoUring.zig @@ -3835,6 +3835,11 @@ test "accept_direct" { test "accept_multishot_direct" { try skipKernelLessThan(.{ .major = 5, .minor = 19, .patch = 0 }); + if (builtin.cpu.arch == .riscv64) { + // https://github.com/ziglang/zig/issues/25734 + return error.SkipZigTest; + } + var ring = IoUring.init(1, 0) catch |err| switch (err) { error.SystemOutdated => return error.SkipZigTest, error.PermissionDenied => return error.SkipZigTest, From 16185f66f1e500d61d43550e7c847a36ad1032df Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 29 Oct 2025 13:50:04 -0700 Subject: [PATCH 244/244] std.http: disable failing test on 32-bit arm tracked by https://github.com/ziglang/zig/issues/25762 --- lib/std/http/test.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index cb973993ec..08cafc4d5a 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -12,6 +12,11 @@ const expectEqualStrings = std.testing.expectEqualStrings; const expectError = std.testing.expectError; test "trailers" { + if (builtin.cpu.arch == .arm) { + // https://github.com/ziglang/zig/issues/25762 + return error.SkipZigTest; + } + const io = std.testing.io; const test_server = try createTestServer(io, struct { fn run(test_server: *TestServer) anyerror!void {