From de3a2c0ebb60901f7603af0fbfa0a57b27a4b5a5 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 6323a39454..b49f40157b 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 38f6199f46..9e4b5e6991 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; @@ -2754,7 +2759,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); } @@ -3676,7 +3686,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, }; @@ -3696,7 +3709,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), @@ -4271,7 +4287,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, }; @@ -4294,7 +4313,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( @@ -8222,46 +8244,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 { @@ -14321,9 +14363,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, }; } @@ -15457,6 +15499,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 5ccc46778b..5da6111079 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 ee3cdeb069..d36417d9d4 100644 --- a/lib/std/os/windows/ntdll.zig +++ b/lib/std/os/windows/ntdll.zig @@ -591,7 +591,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(