From 59073484baf89072aa02f23fe9a09662e5def100 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 1 Feb 2026 01:08:01 -0800 Subject: [PATCH] std.Io: add ioctl / DeviceIoControlFile API --- lib/std/Io.zig | 29 ++++++- lib/std/Io/Threaded.zig | 188 ++++++++++++++++++++++++++++++++++------ lib/std/Progress.zig | 23 +++-- lib/std/c.zig | 7 +- lib/std/posix.zig | 22 ----- 5 files changed, 211 insertions(+), 58 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index a892c0123c..d2b9648245 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -257,6 +257,9 @@ pub const VTable = struct { pub const Operation = union(enum) { file_read_streaming: FileReadStreaming, file_write_streaming: FileWriteStreaming, + /// On Windows this is NtDeviceIoControlFile. On POSIX this is ioctl. On + /// other systems this tag is unreachable. + device_io_control: DeviceIoControl, pub const Tag = @typeInfo(Operation).@"union".tag_type.?; @@ -324,13 +327,37 @@ pub const Operation = union(enum) { pub const Result = Error!usize; }; + pub const DeviceIoControl = switch (builtin.os.tag) { + .wasi => noreturn, + .windows => struct { + file: File, + IoControlCode: std.os.windows.CTL_CODE, + InputBuffer: ?*const anyopaque, + InputBufferLength: u32, + OutputBuffer: ?*anyopaque, + OutputBufferLength: u32, + + pub const Result = std.os.windows.IO_STATUS_BLOCK; + }, + else => struct { + file: File, + /// Device-dependent operation code. + code: u32, + arg: ?*anyopaque, + + /// Device and operation dependent result. Negative values are + /// negative errno. + pub const Result = i32; + }, + }; + pub const Result = Result: { const operation_fields = @typeInfo(Operation).@"union".fields; var field_names: [operation_fields.len][]const u8 = undefined; var field_types: [operation_fields.len]type = undefined; for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| { field_name.* = field.name; - field_type.* = field.type.Result; + field_type.* = if (field.type == noreturn) noreturn else field.type.Result; } break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{})); }; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 8de5e2692a..0feec928fc 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2500,6 +2500,9 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper else => |e| e, }, }, + .device_io_control => |*o| return .{ + .device_io_control = try deviceIoControl(t, o), + }, } } @@ -2531,6 +2534,14 @@ fn batchAwaitAsync(userdata: ?*anyopaque, b: *Io.Batch) Io.Cancelable!void { poll_buffer[poll_len] = .{ .fd = o.file.handle, .events = posix.POLL.OUT, .revents = 0 }; poll_len += 1; }, + .device_io_control => |o| { + poll_buffer[poll_len] = .{ + .fd = o.file.handle, + .events = posix.POLL.OUT | posix.POLL.IN | posix.POLL.ERR, + .revents = 0, + }; + poll_len += 1; + }, } index = submission.node.next; } @@ -2696,6 +2707,7 @@ fn batchAwaitConcurrent(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout switch (submission.operation) { .file_read_streaming => |o| try poll_storage.add(o.file, posix.POLL.IN), .file_write_streaming => |o| try poll_storage.add(o.file, posix.POLL.OUT), + .device_io_control => |o| try poll_storage.add(o.file, posix.POLL.IN | posix.POLL.OUT | posix.POLL.ERR), } index = submission.node.next; } @@ -2874,6 +2886,7 @@ fn batchApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows const result: Io.Operation.Result = switch (pending.tag) { .file_read_streaming => .{ .file_read_streaming = ntReadFileResult(iosb) }, .file_write_streaming => .{ .file_write_streaming = ntWriteFileResult(iosb) }, + .device_io_control => .{ .device_io_control = iosb.* }, }; storage.* = .{ .completion = .{ .node = .{ .next = .none }, .result = result } }; }, @@ -3020,6 +3033,59 @@ fn batchAwaitWindows(b: *Io.Batch, concurrency: bool) error{ Canceled, Concurren else => |status| { syscall.finish(); + context.iosb.u.Status = status; + batchApc(b, &context.iosb, 0); + break; + }, + }; + } + }, + .device_io_control => |o| { + if (o.file.flags.nonblocking) { + context.file = o.file.handle; + switch (windows.ntdll.NtDeviceIoControlFile( + o.file.handle, + null, // event + &batchApc, + b, + &context.iosb, + o.IoControlCode, + o.InputBuffer, + o.InputBufferLength, + o.OutputBuffer, + o.OutputBufferLength, + )) { + .PENDING, .SUCCESS => {}, + .CANCELLED => unreachable, + else => |status| { + context.iosb.u.Status = status; + batchApc(b, &context.iosb, 0); + }, + } + } else { + if (concurrency) return error.ConcurrencyUnavailable; + + const syscall: Syscall = try .start(); + while (true) switch (windows.ntdll.NtDeviceIoControlFile( + o.file.handle, + null, // event + null, // APC routine + null, // APC context + &context.iosb, + o.IoControlCode, + o.InputBuffer, + o.InputBufferLength, + o.OutputBuffer, + o.OutputBufferLength, + )) { + .PENDING => unreachable, // unrecoverable: wrong File nonblocking flag + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + else => |status| { + syscall.finish(); + context.iosb.u.Status = status; batchApc(b, &context.iosb, 0); break; @@ -12986,31 +13052,18 @@ fn netInterfaceNameResolve( }; const syscall: Syscall = try .start(); - while (true) { - switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { - .SUCCESS => { - syscall.finish(); - return .{ .index = @bitCast(ifr.ifru.ivalue) }; - }, - .INTR => { - try syscall.checkCancel(); - continue; - }, - else => |e| { - syscall.finish(); - switch (e) { - .INVAL => |err| return errnoBug(err), // Bad parameters. - .NOTTY => |err| return errnoBug(err), - .NXIO => |err| return errnoBug(err), - .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, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } + while (true) switch (posix.errno(posix.system.ioctl(sock_fd, posix.SIOCGIFINDEX, @intFromPtr(&ifr)))) { + .SUCCESS => { + syscall.finish(); + return .{ .index = @bitCast(ifr.ifru.ivalue) }; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + .NODEV => return syscall.fail(error.InterfaceNotFound), + else => |err| return syscall.unexpectedErrno(err), + }; } if (is_windows) { @@ -17849,3 +17902,88 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError! } } } + +fn deviceIoControl(t: *Threaded, o: *const Io.Operation.DeviceIoControl) Io.Cancelable!Io.Operation.DeviceIoControl.Result { + _ = t; + if (is_windows) { + var iosb: windows.IO_STATUS_BLOCK = undefined; + if (o.file.flags.nonblocking) { + var done: bool = false; + switch (windows.ntdll.NtDeviceIoControlFile( + o.file.handle, + null, // event + flagApc, + &done, // APC context + &iosb, + o.IoControlCode, + o.InputBuffer, + o.InputBufferLength, + o.OutputBuffer, + o.OutputBufferLength, + )) { + // We must wait for the APC routine. + .PENDING, .SUCCESS => while (!done) { + // Once we get here we must not return from the function until the + // operation completes, thereby releasing reference to io_status_block. + const alertable_syscall = AlertableSyscall.start() catch |err| switch (err) { + error.Canceled => |e| { + var cancel_iosb: windows.IO_STATUS_BLOCK = undefined; + _ = windows.ntdll.NtCancelIoFileEx(o.file.handle, &iosb, &cancel_iosb); + while (!done) waitForApcOrAlert(); + return e; + }, + }; + waitForApcOrAlert(); + alertable_syscall.finish(); + }, + else => |status| iosb.u.Status = status, + } + } else { + const syscall: Syscall = try .start(); + while (true) switch (windows.ntdll.NtDeviceIoControlFile( + o.file.handle, + null, // event + null, // APC routine + null, // APC context + &iosb, + o.IoControlCode, + o.InputBuffer, + o.InputBufferLength, + o.OutputBuffer, + o.OutputBufferLength, + )) { + .PENDING => unreachable, // unrecoverable: wrong asynchronous flag + .CANCELLED => { + try syscall.checkCancel(); + continue; + }, + else => |status| { + syscall.finish(); + iosb.u.Status = status; + break; + }, + }; + } + return iosb; + } else { + const syscall: Syscall = try .start(); + while (true) { + const rc = posix.system.ioctl(o.file.handle, @bitCast(o.code), @intFromPtr(o.arg)); + switch (posix.errno(rc)) { + .SUCCESS => { + syscall.finish(); + if (@TypeOf(rc) == usize) return @bitCast(@as(u32, @truncate(rc))); + return rc; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + else => |err| { + syscall.finish(); + return -@as(i32, @intFromEnum(err)); + }, + } + } + } +} diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index ee2a993a4d..e2f3ed5232 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -573,7 +573,7 @@ fn updateTask(io: Io) void { { const resize_flag = wait(io, global_progress.initial_delay_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; - maybeUpdateSize(resize_flag); + maybeUpdateSize(io, resize_flag) catch return; const buffer, _ = computeRedraw(&serialized_buffer); if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { @@ -592,7 +592,7 @@ fn updateTask(io: Io) void { return clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; } - maybeUpdateSize(resize_flag); + maybeUpdateSize(io, resize_flag) catch return; const buffer, _ = computeRedraw(&serialized_buffer); if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { @@ -622,7 +622,7 @@ fn windowsApiUpdateTask(io: Io) void { { const resize_flag = wait(io, global_progress.initial_delay_ns); if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; - maybeUpdateSize(resize_flag); + maybeUpdateSize(io, resize_flag) catch return; const buffer, const nl_n = computeRedraw(&serialized_buffer); if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { @@ -643,7 +643,7 @@ fn windowsApiUpdateTask(io: Io) void { return clearWrittenWindowsApi() catch {}; } - maybeUpdateSize(resize_flag); + maybeUpdateSize(io, resize_flag) catch return; const buffer, const nl_n = computeRedraw(&serialized_buffer); if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { @@ -1484,15 +1484,15 @@ fn writevNonblock(io: Io, file: Io.File, iov: [][]const u8) Io.File.Writer.Error } } -fn maybeUpdateSize(resize_flag: bool) void { +fn maybeUpdateSize(io: Io, resize_flag: bool) !void { if (!resize_flag) return; - const fd = global_progress.terminal.handle; + const file = global_progress.terminal; if (is_windows) { var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(fd, &info) != windows.FALSE) { + if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.FALSE) { // In the old Windows console, dwSize.Y is the line count of the // entire scrollback buffer, so we use this instead so that we // always get the size of the screen. @@ -1512,8 +1512,13 @@ fn maybeUpdateSize(resize_flag: bool) void { .ypixel = 0, }; - const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); - if (posix.errno(err) == .SUCCESS) { + const err = (try io.operate(.{ .device_io_control = .{ + .file = file, + .code = posix.T.IOCGWINSZ, + .arg = &winsize, + } })).device_io_control; + + if (err >= 0) { global_progress.rows = winsize.row; global_progress.cols = winsize.col; } else { diff --git a/lib/std/c.zig b/lib/std/c.zig index e68d5a71c5..809ce29a47 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -10731,7 +10731,6 @@ pub extern "c" fn sysctlnametomib(name: [*:0]const u8, mibp: ?*c_int, sizep: ?*u pub extern "c" fn tcgetattr(fd: fd_t, termios_p: *termios) c_int; pub extern "c" fn tcsetattr(fd: fd_t, optional_action: TCSA, termios_p: *const termios) c_int; pub extern "c" fn fcntl(fd: fd_t, cmd: c_int, ...) c_int; -pub extern "c" fn ioctl(fd: fd_t, request: c_int, ...) c_int; pub extern "c" fn uname(buf: *utsname) c_int; pub extern "c" fn gethostname(name: [*]u8, len: usize) c_int; @@ -11108,6 +11107,11 @@ pub const clock_nanosleep = switch (native_os) { else => {}, }; +pub const ioctl = switch (native_os) { + .windows, .wasi => {}, + else => private.ioctl, +}; + // OS-specific bits. These are protected from being used on the wrong OS by // comptime assertions inside each OS-specific file. @@ -11495,6 +11499,7 @@ const private = struct { }; extern "c" fn getrusage(who: c_int, usage: *rusage) c_int; extern "c" fn gettimeofday(noalias tv: ?*timeval, noalias tz: ?*timezone) c_int; + extern "c" fn ioctl(fd: fd_t, request: c_int, ...) c_int; extern "c" fn msync(addr: *align(page_size) const anyopaque, len: usize, flags: c_int) c_int; extern "c" fn nanosleep(rqtp: *const timespec, rmtp: ?*timespec) c_int; extern "c" fn clock_nanosleep(clockid: clockid_t, flags: TIMER, t: *const timespec, remain: ?*timespec) c_int; diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 5e2cde9aa5..7c03e7953b 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1800,28 +1800,6 @@ pub fn name_to_handle_atZ( } } -pub const IoCtl_SIOCGIFINDEX_Error = error{ - FileSystem, - InterfaceNotFound, -} || UnexpectedError; - -pub fn ioctl_SIOCGIFINDEX(fd: fd_t, ifr: *ifreq) IoCtl_SIOCGIFINDEX_Error!void { - while (true) { - switch (errno(system.ioctl(fd, SIOCGIFINDEX, @intFromPtr(ifr)))) { - .SUCCESS => return, - .INVAL => unreachable, // Bad parameters. - .NOTTY => unreachable, - .NXIO => unreachable, - .BADF => unreachable, // Always a race condition. - .FAULT => unreachable, // Bad pointer parameter. - .INTR => continue, - .IO => return error.FileSystem, - .NODEV => return error.InterfaceNotFound, - else => |err| return unexpectedErrno(err), - } - } -} - 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.