commit 59073484baf89072aa02f23fe9a09662e5def100 (tree)
parent e5454ff780ae4571cfa71a2edb6f4287eb8cf4de
Author: Andrew Kelley <andrew@ziglang.org>
Date: Sun, 1 Feb 2026 01:08:01 -0800
std.Io: add ioctl / DeviceIoControlFile API
Diffstat:
5 files changed, 211 insertions(+), 58 deletions(-)
diff --git 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
@@ -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 } };
},
@@ -3027,6 +3040,59 @@ fn batchAwaitWindows(b: *Io.Batch, concurrency: bool) error{ Canceled, Concurren
};
}
},
+ .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;
+ },
+ };
+ }
+ },
}
index = submission.node.next;
}
@@ -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
@@ -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
@@ -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
@@ -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.