From ed7f2588e409b6d154f4a08bd0029ef1e7beb06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20=C3=85stholm?= Date: Wed, 5 Nov 2025 00:50:19 +0100 Subject: [PATCH 1/4] io: Translate Windows `Clock.real` timestamps to the POSIX/Unix epoch This fixes `std.http.Client` TLS certificate validation on Windows. --- 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 11f9b149caf5..58febae64a76 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2864,7 +2864,8 @@ fn nowWindows(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestam .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 }; + const epoch_ns = std.time.epoch.windows * std.time.ns_per_s; + return .{ .nanoseconds = @as(i96, windows.ntdll.RtlGetSystemTimePrecise()) * 100 + epoch_ns }; }, .awake, .boot => { // QPC on windows doesn't fail on >= XP/2000 and includes time suspended. From 5f1392247d8bb0c6355f2986ff6fbf09a0885b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20=C3=85stholm?= Date: Wed, 5 Nov 2025 00:51:09 +0100 Subject: [PATCH 2/4] io: Redefine `Clock.real` to return timestamps relative to the POSIX/Unix epoch `Clock.real` being defined to return timestamps relative to an implementation-specific epoch means that there's currently no way for the user to translate returned timestamps to actual calendar dates without digging into implementation details of any particular `Io` implementation. Redefining it to return timestamps relative to 1970-01-01T00:00:00Z fixes this problem. There are other ways to solve this, such as adding a new vtable function for returning the implementation-specific epoch, but in terms of complexity this redefinition is by far the simplest solution and only amounts to a simple 96-bit integer addition's worth of overhead on OSes like Windows that use non-POSIX/Unix epochs. --- lib/std/Io.zig | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 810a7a8102b2..653326296eba 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -734,7 +734,7 @@ 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 + /// clock), and by frequency adjustments performed by NTP and similar /// applications. /// /// This clock normally counts the number of seconds since 1970-01-01 @@ -742,8 +742,11 @@ pub const Clock = enum { /// 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. + /// Timestamps returned by implementations of this clock represent time + /// elapsed since 1970-01-01T00:00:00Z, the POSIX/Unix epoch, ignoring + /// leap seconds. This is colloquially known as "Unix time". If the + /// underlying OS uses a different epoch for native timestamps (e.g., + /// Windows, which uses 1601-01-01) they are translated accordingly. real, /// A nonsettable system-wide clock that represents time since some /// unspecified point in the past. From cca2d0995099fc37ac792e6322cad88cc67ac056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20=C3=85stholm?= Date: Wed, 5 Nov 2025 01:00:44 +0100 Subject: [PATCH 3/4] io: Correctly align async closure contexts This fixes package fetching on Windows. Previously, `Async/GroupClosure` allocations were only aligned for the closure struct type, which resulted in panics when `context_alignment` (or `result_alignment` for that matter) had a greater alignment. --- lib/std/Io/Threaded.zig | 189 ++++++++++++++++++++--------------- lib/std/Io/Threaded/test.zig | 59 +++++++++++ 2 files changed, 165 insertions(+), 83 deletions(-) diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 58febae64a76..c0dac8a7963f 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -389,6 +389,7 @@ const AsyncClosure = struct { select_condition: ?*ResetEvent, context_alignment: std.mem.Alignment, result_offset: usize, + alloc_len: usize, const done_reset_event: *ResetEvent = @ptrFromInt(@alignOf(ResetEvent)); @@ -425,18 +426,59 @@ const AsyncClosure = struct { fn contextPointer(ac: *AsyncClosure) [*]u8 { const base: [*]u8 = @ptrCast(ac); - return base + ac.context_alignment.forward(@sizeOf(AsyncClosure)); + const context_offset = ac.context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure)) - @intFromPtr(ac); + return base + context_offset; + } + + fn init( + gpa: Allocator, + mode: enum { async, concurrent }, + result_len: usize, + result_alignment: std.mem.Alignment, + context: []const u8, + context_alignment: std.mem.Alignment, + func: *const fn (context: *const anyopaque, result: *anyopaque) void, + ) Allocator.Error!*AsyncClosure { + const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(AsyncClosure); + const worst_case_context_offset = context_alignment.forward(@sizeOf(AsyncClosure) + max_context_misalignment); + const worst_case_result_offset = result_alignment.forward(worst_case_context_offset + context.len); + const alloc_len = worst_case_result_offset + result_len; + + const ac: *AsyncClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(AsyncClosure), alloc_len))); + errdefer comptime unreachable; + + const actual_context_addr = context_alignment.forward(@intFromPtr(ac) + @sizeOf(AsyncClosure)); + const actual_result_addr = result_alignment.forward(actual_context_addr + context.len); + const actual_result_offset = actual_result_addr - @intFromPtr(ac); + ac.* = .{ + .closure = .{ + .cancel_tid = .none, + .start = start, + .is_concurrent = switch (mode) { + .async => false, + .concurrent => true, + }, + }, + .func = func, + .context_alignment = context_alignment, + .result_offset = actual_result_offset, + .alloc_len = alloc_len, + .reset_event = .unset, + .select_condition = null, + }; + @memcpy(ac.contextPointer()[0..context.len], context); + return ac; } - fn waitAndFree(ac: *AsyncClosure, gpa: Allocator, result: []u8) void { + fn waitAndDeinit(ac: *AsyncClosure, gpa: Allocator, result: []u8) void { ac.reset_event.waitUncancelable(); @memcpy(result, ac.resultPointer()[0..result.len]); - free(ac, gpa, result.len); + ac.deinit(gpa); } - fn free(ac: *AsyncClosure, gpa: Allocator, result_len: usize) void { + fn deinit(ac: *AsyncClosure, gpa: Allocator) void { const base: [*]align(@alignOf(AsyncClosure)) u8 = @ptrCast(ac); - gpa.free(base[0 .. ac.result_offset + result_len]); + gpa.free(base[0..ac.alloc_len]); } }; @@ -452,6 +494,7 @@ fn async( start(context.ptr, result.ptr); return null; } + 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 { @@ -459,37 +502,20 @@ fn async( return null; }; }; + 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; - const ac: *AsyncClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch { + const ac = AsyncClosure.init(gpa, .async, result.len, result_alignment, context, context_alignment, start) catch { start(context.ptr, result.ptr); return null; - })); - - ac.* = .{ - .closure = .{ - .cancel_tid = .none, - .start = AsyncClosure.start, - .is_concurrent = false, - }, - .func = start, - .context_alignment = context_alignment, - .result_offset = result_offset, - .reset_event = .unset, - .select_condition = null, }; - @memcpy(ac.contextPointer()[0..context.len], context); - t.mutex.lock(); const thread_capacity = cpu_count - 1 + t.concurrent_count; t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { t.mutex.unlock(); - ac.free(gpa, result.len); + ac.deinit(gpa); start(context.ptr, result.ptr); return null; }; @@ -501,7 +527,7 @@ fn async( if (t.threads.items.len == 0) { assert(t.run_queue.popFirst() == &ac.closure.node); t.mutex.unlock(); - ac.free(gpa, result.len); + ac.deinit(gpa); start(context.ptr, result.ptr); return null; } @@ -530,27 +556,11 @@ fn concurrent( 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; - const ac_bytes = gpa.alignedAlloc(u8, .of(AsyncClosure), n) catch + const ac = AsyncClosure.init(gpa, .concurrent, result_len, result_alignment, context, context_alignment, start) catch { return error.ConcurrencyUnavailable; - const ac: *AsyncClosure = @ptrCast(@alignCast(ac_bytes)); - - ac.* = .{ - .closure = .{ - .cancel_tid = .none, - .start = AsyncClosure.start, - .is_concurrent = true, - }, - .func = start, - .context_alignment = context_alignment, - .result_offset = result_offset, - .reset_event = .unset, - .select_condition = null, }; - @memcpy(ac.contextPointer()[0..context.len], context); t.mutex.lock(); @@ -559,7 +569,7 @@ fn concurrent( t.threads.ensureTotalCapacity(gpa, thread_capacity) catch { t.mutex.unlock(); - ac.free(gpa, result_len); + ac.deinit(gpa); return error.ConcurrencyUnavailable; }; @@ -569,7 +579,7 @@ fn concurrent( 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); + ac.deinit(gpa); return error.ConcurrencyUnavailable; }; t.threads.appendAssumeCapacity(thread); @@ -588,7 +598,7 @@ const GroupClosure = struct { node: std.SinglyLinkedList.Node, func: *const fn (*Io.Group, context: *anyopaque) void, context_alignment: std.mem.Alignment, - context_len: usize, + alloc_len: usize, fn start(closure: *Closure) void { const gc: *GroupClosure = @alignCast(@fieldParentPtr("closure", closure)); @@ -616,22 +626,48 @@ const GroupClosure = struct { if (prev_state == (sync_one_pending | sync_is_waiting)) reset_event.set(); } - 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 { - 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(gc: *GroupClosure) [*]u8 { const base: [*]u8 = @ptrCast(gc); - return base + contextOffset(gc.context_alignment); + const context_offset = gc.context_alignment.forward(@intFromPtr(gc) + @sizeOf(GroupClosure)) - @intFromPtr(gc); + return base + context_offset; + } + + /// Does not initialize the `node` field. + fn init( + gpa: Allocator, + t: *Threaded, + group: *Io.Group, + context: []const u8, + context_alignment: std.mem.Alignment, + func: *const fn (*Io.Group, context: *const anyopaque) void, + ) Allocator.Error!*GroupClosure { + const max_context_misalignment = context_alignment.toByteUnits() -| @alignOf(GroupClosure); + const worst_case_context_offset = context_alignment.forward(@sizeOf(GroupClosure) + max_context_misalignment); + const alloc_len = worst_case_context_offset + context.len; + + const gc: *GroupClosure = @ptrCast(@alignCast(try gpa.alignedAlloc(u8, .of(GroupClosure), alloc_len))); + errdefer comptime unreachable; + + gc.* = .{ + .closure = .{ + .cancel_tid = .none, + .start = start, + .is_concurrent = false, + }, + .t = t, + .group = group, + .node = undefined, + .func = func, + .context_alignment = context_alignment, + .alloc_len = alloc_len, + }; + @memcpy(gc.contextPointer()[0..context.len], context); + return gc; + } + + fn deinit(gc: *GroupClosure, gpa: Allocator) void { + const base: [*]align(@alignOf(GroupClosure)) u8 = @ptrCast(gc); + gpa.free(base[0..gc.alloc_len]); } const sync_is_waiting: usize = 1 << 0; @@ -646,27 +682,14 @@ fn groupAsync( start: *const fn (*Io.Group, context: *const anyopaque) void, ) void { 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; - const n = GroupClosure.contextEnd(context_alignment, context.len); - const gc: *GroupClosure = @ptrCast(@alignCast(gpa.alignedAlloc(u8, .of(GroupClosure), n) catch { + const gc = GroupClosure.init(gpa, t, group, context, context_alignment, start) catch { return start(group, context.ptr); - })); - gc.* = .{ - .closure = .{ - .cancel_tid = .none, - .start = GroupClosure.start, - .is_concurrent = false, - }, - .t = t, - .group = group, - .node = undefined, - .func = start, - .context_alignment = context_alignment, - .context_len = context.len, }; - @memcpy(gc.contextPointer()[0..context.len], context); t.mutex.lock(); @@ -678,7 +701,7 @@ fn groupAsync( t.threads.ensureTotalCapacityPrecise(gpa, thread_capacity) catch { t.mutex.unlock(); - gc.free(gpa); + gc.deinit(gpa); return start(group, context.ptr); }; @@ -688,7 +711,7 @@ fn groupAsync( 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); + gc.deinit(gpa); return start(group, context.ptr); }; t.threads.appendAssumeCapacity(thread); @@ -730,7 +753,7 @@ fn groupWait(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void { while (true) { const gc: *GroupClosure = @fieldParentPtr("node", node); const node_next = node.next; - gc.free(gpa); + gc.deinit(gpa); node = node_next orelse break; } } @@ -761,7 +784,7 @@ fn groupCancel(userdata: ?*anyopaque, group: *Io.Group, token: *anyopaque) void while (true) { const gc: *GroupClosure = @fieldParentPtr("node", node); const node_next = node.next; - gc.free(gpa); + gc.deinit(gpa); node = node_next orelse break; } } @@ -776,7 +799,7 @@ fn await( _ = result_alignment; const t: *Threaded = @ptrCast(@alignCast(userdata)); const closure: *AsyncClosure = @ptrCast(@alignCast(any_future)); - closure.waitAndFree(t.allocator, result); + closure.waitAndDeinit(t.allocator, result); } fn cancel( @@ -789,7 +812,7 @@ fn cancel( const t: *Threaded = @ptrCast(@alignCast(userdata)); const ac: *AsyncClosure = @ptrCast(@alignCast(any_future)); ac.closure.requestCancel(); - ac.waitAndFree(t.allocator, result); + ac.waitAndDeinit(t.allocator, result); } fn cancelRequested(userdata: ?*anyopaque) bool { diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index ef24b25f3496..350fa281eef7 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -56,3 +56,62 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" { getter.await(io); putter.await(io); } + +const ByteArray256 = struct { x: [32]u8 align(32) }; +const ByteArray512 = struct { x: [64]u8 align(64) }; + +fn concatByteArrays(a: ByteArray256, b: ByteArray256) ByteArray512 { + return .{ .x = a.x ++ b.x }; +} + +test "async/concurrent context and result alignment" { + var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buffer); + + var threaded: std.Io.Threaded = .init(fba.allocator()); + defer threaded.deinit(); + const io = threaded.io(); + + const a: ByteArray256 = .{ .x = @splat(2) }; + const b: ByteArray256 = .{ .x = @splat(3) }; + const expected: ByteArray512 = .{ .x = @as([32]u8, @splat(2)) ++ @as([32]u8, @splat(3)) }; + + { + var future = io.async(concatByteArrays, .{ a, b }); + const result = future.await(io); + try std.testing.expectEqualSlices(u8, &expected.x, &result.x); + } + { + var future = io.concurrent(concatByteArrays, .{ a, b }) catch |err| switch (err) { + error.ConcurrencyUnavailable => { + try testing.expect(builtin.single_threaded); + return; + }, + }; + const result = future.await(io); + try std.testing.expectEqualSlices(u8, &expected.x, &result.x); + } +} + +fn concatByteArraysResultPtr(a: ByteArray256, b: ByteArray256, result: *ByteArray512) void { + result.* = .{ .x = a.x ++ b.x }; +} + +test "Group.async context alignment" { + var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buffer); + + var threaded: std.Io.Threaded = .init(fba.allocator()); + defer threaded.deinit(); + const io = threaded.io(); + + const a: ByteArray256 = .{ .x = @splat(2) }; + const b: ByteArray256 = .{ .x = @splat(3) }; + const expected: ByteArray512 = .{ .x = @as([32]u8, @splat(2)) ++ @as([32]u8, @splat(3)) }; + + var group: std.Io.Group = .init; + var result: ByteArray512 = undefined; + group.async(io, concatByteArraysResultPtr, .{ a, b, &result }); + group.wait(io); + try std.testing.expectEqualSlices(u8, &expected.x, &result.x); +} From 8887346b530e1a3499407a580324f81b1b6caac9 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Sun, 9 Nov 2025 21:43:20 +0100 Subject: [PATCH 4/4] std.Io: fix calls on functions that return an array type --- lib/std/Io.zig | 14 +++++++------- lib/std/Io/Threaded/test.zig | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 653326296eba..ba82b8fb5981 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -993,7 +993,7 @@ pub fn Future(Result: type) type { /// 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)); + io.vtable.cancel(io.userdata, any_future, @ptrCast(&f.result), .of(Result)); f.any_future = null; return f.result; } @@ -1001,7 +1001,7 @@ pub fn Future(Result: type) type { /// 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)); + io.vtable.await(io.userdata, any_future, @ptrCast(&f.result), .of(Result)); f.any_future = null; return f.result; } @@ -1037,7 +1037,7 @@ pub const Group = struct { @call(.auto, function, args_casted.*); } }; - io.vtable.groupAsync(io.userdata, g, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + io.vtable.groupAsync(io.userdata, g, @ptrCast(&args), .of(Args), TypeErased.start); } /// Blocks until all tasks of the group finish. During this time, @@ -1114,7 +1114,7 @@ pub fn Select(comptime U: type) type { } }; _ = @atomicRmw(usize, &s.outstanding, .Add, 1, .monotonic); - s.io.vtable.groupAsync(s.io.userdata, &s.group, @ptrCast((&args)[0..1]), .of(Args), TypeErased.start); + s.io.vtable.groupAsync(s.io.userdata, &s.group, @ptrCast(&args), .of(Args), TypeErased.start); } /// Blocks until another task of the select finishes. @@ -1542,9 +1542,9 @@ pub fn async( var future: Future(Result) = undefined; future.any_future = io.vtable.async( io.userdata, - @ptrCast((&future.result)[0..1]), + @ptrCast(&future.result), .of(Result), - @ptrCast((&args)[0..1]), + @ptrCast(&args), .of(Args), TypeErased.start, ); @@ -1583,7 +1583,7 @@ pub fn concurrent( io.userdata, @sizeOf(Result), .of(Result), - @ptrCast((&args)[0..1]), + @ptrCast(&args), .of(Args), TypeErased.start, ); diff --git a/lib/std/Io/Threaded/test.zig b/lib/std/Io/Threaded/test.zig index 350fa281eef7..7e6e687cf219 100644 --- a/lib/std/Io/Threaded/test.zig +++ b/lib/std/Io/Threaded/test.zig @@ -115,3 +115,17 @@ test "Group.async context alignment" { group.wait(io); try std.testing.expectEqualSlices(u8, &expected.x, &result.x); } + +fn returnArray() [32]u8 { + return @splat(5); +} + +test "async with array return type" { + var threaded: std.Io.Threaded = .init(std.testing.allocator); + defer threaded.deinit(); + const io = threaded.io(); + + var future = io.async(returnArray, .{}); + const result = future.await(io); + try std.testing.expectEqualSlices(u8, &@as([32]u8, @splat(5)), &result); +}