From 59ebd82ebf4746e47b3e84367b1ea87bbec7d222 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 20 Jan 2026 17:05:14 -0800 Subject: [PATCH] std.Io: give File a nonblocking bit on Windows This tracks whether it is a file opened in synchronous mode, or something that supports APC. This will be needed in order to know whether concurrent batch operations on the file should return error.ConcurrencyUnavailable, or use APC to complete the batch. This patch also switches to using NtCreateFile directly in std.Io.Threaded for dirCreateFile, as well as NtReadFile for fileReadStreaming, making it handle files opened in synchronous mode as well as files opened in asynchronous mode. --- lib/std/Io/File.zig | 15 +++++ lib/std/Io/Threaded.zig | 123 +++++++++++++++++++++++------------ lib/std/Progress.zig | 1 + lib/std/os/windows/ntdll.zig | 2 +- 4 files changed, 100 insertions(+), 41 deletions(-) diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index e537755a3365de8bab78d79fb55a40da0c33fe03..d0f487b911a4f37474d0dfc18925f6021ceda6c3 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -10,8 +10,20 @@ const assert = std.debug.assert; const Dir = std.Io.Dir; handle: Handle, +flags: Flags = .{}, pub const Handle = std.posix.fd_t; +pub const Flags = switch (native_os) { + .windows => packed struct(u1) { + /// * true: opened with MODE.IO.ASYNCHRONOUS + /// * false: opened with SYNCHRONOUS_ALERT or SYNCHRONOUS_NONALERT, or + /// not a file. + /// This is default-initialized to false as a workaround for + /// https://codeberg.org/ziglang/zig/issues/30842 + nonblocking: bool = false, + }, + else => packed struct(u0) {}, +}; pub const Reader = @import("File/Reader.zig"); pub const Writer = @import("File/Writer.zig"); @@ -77,6 +89,7 @@ pub fn stdout() File { return switch (native_os) { .windows => .{ .handle = std.os.windows.peb().ProcessParameters.hStdOutput, + .flags = .{ .nonblocking = false }, }, else => .{ .handle = std.posix.STDOUT_FILENO, @@ -88,6 +101,7 @@ pub fn stderr() File { return switch (native_os) { .windows => .{ .handle = std.os.windows.peb().ProcessParameters.hStdError, + .flags = .{ .nonblocking = false }, }, else => .{ .handle = std.posix.STDERR_FILENO, @@ -99,6 +113,7 @@ pub fn stdin() File { return switch (native_os) { .windows => .{ .handle = std.os.windows.peb().ProcessParameters.hStdInput, + .flags = .{ .nonblocking = false }, }, else => .{ .handle = std.posix.STDIN_FILENO, diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f0562504fd5e4b833ed19ddea38638b0824bd5f1..cdbb0fd18291dd7ea75fbd229969573cd161eed0 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1173,6 +1173,11 @@ const Syscall = struct { .blocked_canceling => return error.Canceled, // new status is `.canceled` } } + fn toApc(s: Syscall) Io.Cancelable!void { + // TODO set state to indicate instead of NtCancelSynchronousIoFile we + // need to use NtCancelIoFileEx + return s.checkCancel(); + } /// Marks this syscall as finished. fn finish(s: Syscall) void { const thread = s.thread orelse return; @@ -2759,7 +2764,12 @@ fn dirCreateDirPathOpenWasi( fn dirStat(userdata: ?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat { const t: *Threaded = @ptrCast(@alignCast(userdata)); - const file: File = .{ .handle = dir.handle }; + const file: File = if (is_windows) .{ + .handle = dir.handle, + .flags = .{ .nonblocking = false }, + } else .{ + .handle = dir.handle, + }; return fileStat(t, file); } @@ -3682,7 +3692,10 @@ fn dirCreateFileWindows( errdefer windows.CloseHandle(handle); const exclusive = switch (flags.lock) { - .none => return .{ .handle = handle }, + .none => return .{ + .handle = handle, + .flags = .{ .nonblocking = false }, + }, .shared => false, .exclusive => true, }; @@ -3702,7 +3715,10 @@ fn dirCreateFileWindows( )) { .SUCCESS => { syscall.finish(); - return .{ .handle = handle }; + return .{ + .handle = handle, + .flags = .{ .nonblocking = false }, + }; }, .INSUFFICIENT_RESOURCES => return syscall.fail(error.SystemResources), .LOCK_NOT_GRANTED => return syscall.fail(error.WouldBlock), @@ -4273,7 +4289,10 @@ pub fn dirOpenFileWtf16( errdefer w.CloseHandle(handle); const exclusive = switch (flags.lock) { - .none => return .{ .handle = handle }, + .none => return .{ + .handle = handle, + .flags = .{ .nonblocking = false }, + }, .shared => false, .exclusive => true, }; @@ -4296,7 +4315,10 @@ pub fn dirOpenFileWtf16( .ACCESS_VIOLATION => |err| return syscall.ntstatusBug(err), // bad io_status_block pointer else => |status| return syscall.unexpectedNtstatus(status), }; - return .{ .handle = handle }; + return .{ + .handle = handle, + .flags = .{ .nonblocking = false }, + }; } fn dirOpenFileWasi( @@ -8365,46 +8387,66 @@ fn fileReadStreamingPosix(file: File, data: []const []u8) File.Reader.Error!usiz } fn fileReadStreamingWindows(file: File, data: []const []u8) File.Reader.Error!usize { - const DWORD = windows.DWORD; var index: usize = 0; while (index < data.len and data[index].len == 0) index += 1; if (index == data.len) return 0; const buffer = data[index]; - const want_read_count: DWORD = @min(std.math.maxInt(DWORD), buffer.len); - const syscall: Syscall = try .start(); - while (true) { - var n: DWORD = undefined; - if (windows.kernel32.ReadFile(file.handle, buffer.ptr, want_read_count, &n, null) != 0) { - syscall.finish(); - return n; + var io_status_block: windows.IO_STATUS_BLOCK = undefined; + + read: { + const syscall: Syscall = try .start(); + while (true) { + switch (windows.ntdll.NtReadFile( + file.handle, + null, // event + noopApc, // apc callback + null, // apc context + &io_status_block, + buffer.ptr, + @min(std.math.maxInt(u32), buffer.len), + null, // byte offset + null, // key + )) { + .SUCCESS => break :read syscall.finish(), + .PENDING => break, + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + .INVALID_PARAMETER => |err| return syscall.ntstatusBug(err), // wrong value for flags.nonblocking + else => |status| return syscall.unexpectedNtstatus(status), + } } - switch (windows.GetLastError()) { - .IO_PENDING => |err| { - syscall.finish(); - return windows.errorBug(err); - }, - .OPERATION_ABORTED => { - try syscall.checkCancel(); - continue; - }, - .BROKEN_PIPE, .HANDLE_EOF => { - syscall.finish(); - return 0; - }, - .NETNAME_DELETED => if (is_debug) unreachable else return error.Unexpected, - .LOCK_VIOLATION => return syscall.fail(error.LockViolation), - .ACCESS_DENIED => return syscall.fail(error.AccessDenied), - .INVALID_HANDLE => if (is_debug) unreachable else return error.Unexpected, - // TODO: Determine if INVALID_FUNCTION is possible in more scenarios than just passing - // a handle to a directory. - .INVALID_FUNCTION => return syscall.fail(error.IsDir), - else => |err| { - syscall.finish(); - return windows.unexpectedError(err); - }, + try syscall.toApc(); + while (true) { + switch (windows.ntdll.NtDelayExecution(1, null)) { + .USER_APC => break syscall.finish(), + .SUCCESS, .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + else => |status| return syscall.unexpectedNtstatus(status), + } } } + + switch (io_status_block.u.Status) { + .SUCCESS, .END_OF_FILE, .PIPE_BROKEN => {}, + .ACCESS_DENIED => return error.AccessDenied, + else => |status| return windows.unexpectedStatus(status), + } + return io_status_block.Information; +} + +fn noopApc( + apc_context: ?*anyopaque, + io_status_block: *windows.IO_STATUS_BLOCK, + unused: windows.ULONG, +) callconv(.winapi) void { + _ = apc_context; + _ = io_status_block; + _ = unused; } fn fileReadPositionalPosix(file: File, data: []const []u8, offset: u64) File.ReadPositionalError!usize { @@ -14560,9 +14602,9 @@ fn processSpawnWindows(userdata: ?*anyopaque, options: process.SpawnOptions) pro return .{ .id = piProcInfo.hProcess, .thread_handle = piProcInfo.hThread, - .stdin = if (g_hChildStd_IN_Wr) |h| .{ .handle = h } else null, - .stdout = if (g_hChildStd_OUT_Rd) |h| .{ .handle = h } else null, - .stderr = if (g_hChildStd_ERR_Rd) |h| .{ .handle = h } else null, + .stdin = if (g_hChildStd_IN_Wr) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null, + .stdout = if (g_hChildStd_OUT_Rd) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null, + .stderr = if (g_hChildStd_ERR_Rd) |h| .{ .handle = h, .flags = .{ .nonblocking = true } } else null, .request_resource_usage_statistics = options.request_resource_usage_statistics, }; } @@ -15696,6 +15738,7 @@ fn progressParentFile(userdata: ?*anyopaque) std.Progress.ParentFileError!File { .pointer => @ptrFromInt(int), else => return error.UnsupportedOperation, }, + .flags = if (is_windows) .{ .nonblocking = true } else .{}, }; } diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 5ccc46778b43d0260ee1a6323e13eae20e31a2c9..5da61110796cd72016782b78ae7b83561ec0083d 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -979,6 +979,7 @@ fn serializeIpc(start_serialized_len: usize, serialized_buffer: *Serialized.Buff if (main_parent == .unused) continue; const file: Io.File = .{ .handle = main_storage.getIpcFd() orelse continue, + .flags = if (is_windows) .{ .nonblocking = true } else .{}, }; const opt_saved_metadata = findOld(file.handle, old_ipc_metadata_fds, old_ipc_metadata); var bytes_read: usize = 0; diff --git a/lib/std/os/windows/ntdll.zig b/lib/std/os/windows/ntdll.zig index f61cbbf5b8fe8bc468ad2f93638ca60503c54ed2..774ca28f19064bc66ad96840aa3114e9fb039cee 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -596,7 +596,7 @@ pub extern "ntdll" fn NtCancelSynchronousIoFile( pub extern "ntdll" fn NtDelayExecution( Alertable: BOOLEAN, - DelayInterval: *const LARGE_INTEGER, + DelayInterval: ?*const LARGE_INTEGER, ) callconv(.winapi) NTSTATUS; pub extern "ntdll" fn NtCancelIoFileEx( -- 2.54.0