├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── README.md ├── build.zig ├── src │ ├── benchmark.zig │ ├── client.zig │ └── server.zig └── zig.mod ├── src └── async_io_uring.zig └── zig.mod /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text eol=lf 3 | zig.mod text eol=lf 4 | zigmod.* text eol=lf 5 | zig.mod linguist-language=YAML 6 | zig.mod gitlab-language=yaml 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-* 2 | .zigmod 3 | deps.zig 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Matthew Saltz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | 4 | `AsyncIOUring` is an event loop that wraps the `IO_Uring` library with coroutines 5 | support. It supports all `IO_Uring` operations (with the intentional exception 6 | of `poll_update`\*). 7 | 8 | In addition, it allows: 9 | * Adding timeouts to operations 10 | * Manual cancellation of operations 11 | * Writing custom operations for advanced use cases 12 | 13 | It is currently functionally complete, though there are a few `TODO`s marked in 14 | the source related to polishing the API. It's not used in production anywhere currently. 15 | 16 | See `src/async_io_uring.zig` for full API documentation. 17 | 18 | See the `examples` directory for an echo client and server that use the event loop. 19 | 20 | \* If you need this for some reason, please create an issue. 21 | 22 | > :warning: **The `main` branch of `async_io_uring` will follow changes to zig's `master` branch to stay up-to-date with changes to the `IO_Uring` API (among others).** See the tagged releases of `async_io_uring` that are marked to work with specific stable versions of zig. (E.g., release v0.1.0 works with zig 0.9.1) 23 | 24 | ## Table of contents 25 | * [Background](#background) 26 | * [Goals](#goals) 27 | * [Installation](#installation) 28 | * [Example usage](#example-usage) 29 | * [Echo client](#echo-client) 30 | * [Operation timeouts](#operation-cancellation) 31 | * [Operation cancellation](#operation-cancellation) 32 | 33 | --- 34 | # Background 35 | 36 | As an overview for the unfamiliar, `io_uring` is a new-ish Linux kernel feature 37 | that allows users to enqueue requests to perform syscalls into a submission 38 | queue (e.g. a request to read from a socket) and then submit the submission 39 | queue to the kernel for processing. 40 | 41 | When requests from the submission queue have been satisfied, the result is 42 | placed onto completion queue by the kernel. The user is able to either poll 43 | the kernel for completion queue results or block until results are 44 | available. 45 | 46 | Zig's `IO_Uring` library provides a convenient interface to the kernel's 47 | `io_uring` functionality. The user of `IO_Uring`, however, still has to manually 48 | deal with submitting requests to the kernel and retrieving events from the 49 | completion queue, which can be tedious. 50 | 51 | This library wraps the `IO_Uring` library by adding an event loop that handles 52 | request submission and completion, and provides an interface for each syscall 53 | that uses zig's `async` functionality to suspend execution of the calling code 54 | until the syscall has been completed. This lets the user write code that looks 55 | like blocking code, while still allowing for concurrency even within a single 56 | thread. 57 | 58 | # Goals 59 | 60 | * **Minimal**: Wraps the `IO_Uring` library in the most lightweight way 61 | possible. This means it still uses the `IO_Uring` data structures in many 62 | places, like for completion queue entries. There are no additional internal 63 | data structures other than the submission queue and completion queue used by 64 | the `IO_Uring` library. This means there's no heap allocation. It also relies 65 | entirely on kernel functionality for timeouts and cancellation. 66 | * **Complete**: You should be able to do anything with this that you could do 67 | with `IO_Uring`. 68 | * **Easy to use**: Because of the use of coroutines, code written with this 69 | library looks almost identical to blocking code. In addition, operation 70 | timeouts and cancellation support is integrated into the API for all operations. 71 | * **Performant**: The library does no heap allocation and there's minimal 72 | additional logic on top of `suspend`/`resume`. 73 | 74 | # Installation 75 | 76 | This library integrates with the [zigmod](https://github.com/nektro/zigmod) 77 | package manager. If you've installed `zigmod`, you can add a line like the 78 | following to your `root_dependencies` in the `zig.mod` file of your project 79 | and run `zigmod fetch`: 80 | ```yml 81 | root_dependencies: 82 | - ... 83 | - src: git https://github.com/saltzm/async_io_uring.git 84 | ``` 85 | 86 | You'll then be able to include `async_io_uring.zig` by doing something like: 87 | ```zig 88 | const io = @import("async_io_uring"); 89 | ``` 90 | 91 | The examples directory is structured roughly as you might structure a project 92 | that uses `async_io_uring`, with a working `zig.mod` file and `build.zig` that 93 | can serve as examples. 94 | 95 | You'll also need a Linux kernel version that supports all of the `io_uring` 96 | features you'd like to use. (All testing was done on version 5.13.0.) 97 | 98 | # Example usage 99 | 100 | ## Echo client 101 | 102 | Jumping right into a realistic example, the following is a snippet of code from 103 | the echo client in the `examples` directory: 104 | 105 | ```zig 106 | const io = @import("async_io_uring"); 107 | 108 | pub fn run_client(ring: *AsyncIOUring) !void { 109 | // Make a data structure that lets us do async file I/O with the same 110 | // syntax as `std.debug.print`. 111 | var writer = try AsyncWriter.init(ring, std.io.getStdErr().handle); 112 | 113 | // Address of the echo server. 114 | const address = try net.Address.parseIp4("127.0.0.1", 3131); 115 | 116 | // Open a socket for connecting to the server. 117 | const server = try os.socket(address.any.family, os.SOCK.STREAM | os.SOCK.CLOEXEC, 0); 118 | defer { 119 | _ = ring.close(server, null, null) catch { 120 | std.os.exit(1); 121 | }; 122 | } 123 | 124 | // Connect to the server. 125 | _ = try ring.connect(server, &address.any, address.getOsSockLen(), null, null); 126 | 127 | const stdin_file = std.io.getStdIn(); 128 | const stdin_fd = stdin_file.handle; 129 | var input_buffer: [256]u8 = undefined; 130 | 131 | while (true) { 132 | // Prompt the user for input. 133 | try writer.print("Input: ", .{}); 134 | 135 | const read_timeout = os.linux.kernel_timespec{ .tv_sec = 10, .tv_nsec = 0 }; 136 | // Read a line from stdin with a 10 second timeout. 137 | // This is the more verbose API - you can also do `ring.read`. 138 | const read_cqe = ring.do( 139 | io.Read{ .fd = stdin_fd, .buffer = input_buffer[0..], .offset = input_buffer.len }, 140 | io.Timeout{ .ts = &read_timeout, .flags = 0 }, 141 | null, 142 | ) catch |err| { 143 | if (err == error.Cancelled) { 144 | try writer.print("\nTimed out waiting for input, exiting...\n", .{}); 145 | return; 146 | } else return err; 147 | }; 148 | 149 | const num_bytes_read = @intCast(usize, read_cqe.res); 150 | 151 | // Send it to the server. 152 | _ = try ring.send(server, input_buffer[0..num_bytes_read], 0, null, null); 153 | 154 | // Receive response. 155 | const recv_cqe = try ring.recv(server, input_buffer[0..], 0, null, null); 156 | 157 | const num_bytes_received = @intCast(usize, recv_cqe.res); 158 | try writer.print("Received: {s}\n", .{input_buffer[0..num_bytes_received]}); 159 | } 160 | } 161 | ``` 162 | 163 | ## Operation timeouts 164 | 165 | `AsyncIOUring` supports adding timeouts to all operations. Adding a timeout to 166 | an operation causes it to be cancelled after the specified timeout, returning 167 | an error code `error.Cancelled` if cancellation was successful. 168 | 169 | An example from the unit tests: 170 | 171 | ```zig 172 | fn testReadThatTimesOut(ring: *AsyncIOUring) !void { 173 | var read_buffer = [_]u8{0} ** 20; 174 | 175 | const ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 10000 }; 176 | // Try to read from stdin - there won't be any input so this should 177 | // reliably time out. 178 | const read_cqe = ring.do( 179 | Read{ .fd = std.io.getStdIn().handle, .buffer = read_buffer[0..], .offset = 0 }, 180 | Timeout{ .ts = &ts, .flags = 0 }, 181 | null, 182 | ); 183 | try std.testing.expectEqual(read_cqe, error.Cancelled); 184 | } 185 | ``` 186 | 187 | ## Operation cancellation 188 | 189 | `AsyncIOUring` supports cancellation for all operations. Each operation is 190 | identified by an `id` that is set via a `maybe_id` "output parameter" in all 191 | operation submission functions (e.g. `read`, `send`, etc.). This `id` can then 192 | be passed to `AsyncIOUring.cancel` to cancel that operation. 193 | 194 | An example from the unit tests: 195 | 196 | ```zig 197 | fn testReadThatIsCancelled(ring: *AsyncIOUring) !void { 198 | var read_buffer = [_]u8{0} ** 20; 199 | 200 | var op_id: u64 = undefined; 201 | 202 | // Try to read from stdin - there won't be any input so this operation should 203 | // reliably hang until cancellation. 204 | var read_frame = async ring.do( 205 | Read{ .fd = std.io.getStdIn().handle, .buffer = read_buffer[0..], .offset = 0 }, 206 | null, 207 | &op_id, 208 | ); 209 | 210 | const cancel_cqe = try ring.cancel(op_id, 0, null, null); 211 | // Expect that cancellation succeeded. 212 | try std.testing.expectEqual(cancel_cqe.res, 0); 213 | 214 | const read_cqe = await read_frame; 215 | try std.testing.expectEqual(read_cqe, error.Cancelled); 216 | } 217 | ``` 218 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # About the examples 3 | 4 | Files: 5 | * `src/client.zig`: Echo client. All I/O (including reading from stdin) is 6 | non-blocking except for the debug logging. 7 | * `src/server.zig`: Echo server. Handles multiple concurrent connections. All I/O 8 | is non-blocking except for the debug logging. 9 | * `src/benchmark.zig`: A silly benchmarking loop to check throughput from the 10 | perspective of a client sending "hello" over and over again. 11 | 12 | # Running the echo client and server 13 | ```sh 14 | $ cd examples 15 | # Fetch async_io_uring as a dependency for the examples. 16 | $ zigmod fetch 17 | 18 | # Run the server 19 | $ zig build run_server 20 | 21 | # Example output when server.zig has max_connections set to 2 22 | Accepting 23 | Spawning new connection with index: 0 24 | Accepting 25 | Spawning new connection with index: 1 26 | Accepting 27 | Reached connection limit, refusing connection. 28 | Accepting 29 | Closing connection with index 1 30 | Closing connection with index 0 31 | 32 | # In a separate shell 33 | $ zig build run_client 34 | #Example output 35 | Input: hello 36 | Received: hello 37 | 38 | Input: world 39 | Received: world 40 | ``` 41 | 42 | # Running the benchmark 43 | ```sh 44 | # Run the server 45 | $ zig build run_server -Drelease-fast 46 | 47 | # In a separate shell 48 | $ zig build run_benchmark -Drelease-fast 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const deps = @import("./deps.zig"); 3 | 4 | pub fn build(b: *std.build.Builder) void { 5 | // Standard target options allows the person running `zig build` to choose 6 | // what target to build for. Here we do not override the defaults, which 7 | // means any target is allowed, and the default is native. Other options 8 | // for restricting supported target set are available. 9 | const target = b.standardTargetOptions(.{}); 10 | 11 | // Standard release options allow the person running `zig build` to select 12 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 13 | const mode = b.standardReleaseOptions(); 14 | 15 | { 16 | const server_exe = b.addExecutable("async_io_uring_server", "src/server.zig"); 17 | server_exe.setTarget(target); 18 | server_exe.setBuildMode(mode); 19 | deps.addAllTo(server_exe); 20 | server_exe.install(); 21 | 22 | const run_cmd = server_exe.run(); 23 | run_cmd.step.dependOn(b.getInstallStep()); 24 | if (b.args) |args| { 25 | run_cmd.addArgs(args); 26 | } 27 | 28 | const run_step = b.step("run_server", "Run an echo server"); 29 | run_step.dependOn(&run_cmd.step); 30 | } 31 | 32 | { 33 | const client_exe = b.addExecutable("async_io_uring_client", "src/client.zig"); 34 | client_exe.setTarget(target); 35 | client_exe.setBuildMode(mode); 36 | deps.addAllTo(client_exe); 37 | client_exe.install(); 38 | 39 | const run_cmd = client_exe.run(); 40 | run_cmd.step.dependOn(b.getInstallStep()); 41 | if (b.args) |args| { 42 | run_cmd.addArgs(args); 43 | } 44 | 45 | const run_step = b.step("run_client", "Run an echo client"); 46 | run_step.dependOn(&run_cmd.step); 47 | } 48 | 49 | { 50 | const exe = b.addExecutable("async_io_uring_benchmark", "src/benchmark.zig"); 51 | exe.setTarget(target); 52 | exe.setBuildMode(mode); 53 | deps.addAllTo(exe); 54 | exe.install(); 55 | 56 | const run_cmd = exe.run(); 57 | run_cmd.step.dependOn(b.getInstallStep()); 58 | if (b.args) |args| { 59 | run_cmd.addArgs(args); 60 | } 61 | 62 | const run_step = b.step("run_benchmark", "Run a benchmark for a running echo server"); 63 | run_step.dependOn(&run_cmd.step); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/src/benchmark.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const IO_Uring = std.os.linux.IO_Uring; 3 | const assert = std.debug.assert; 4 | const builtin = std.builtin; 5 | const mem = std.mem; 6 | const net = std.net; 7 | const os = std.os; 8 | const linux = os.linux; 9 | const testing = std.testing; 10 | const Timer = std.time.Timer; 11 | 12 | const aiou = @import("async_io_uring"); 13 | const AsyncIOUring = aiou.AsyncIOUring; 14 | 15 | const BenchmarkResult = struct { num_ops: u64 }; 16 | 17 | const max_ops = 5000; 18 | const buffer_to_send: [512]u8 = [_]u8{0} ** 512; 19 | const num_bytes_to_send = buffer_to_send.len; 20 | 21 | pub fn run_client(ring: *AsyncIOUring) !void { 22 | const address = try net.Address.parseIp4("127.0.0.1", 3131); 23 | const client = try os.socket(address.any.family, os.SOCK.STREAM | os.SOCK.CLOEXEC, 0); 24 | defer { 25 | _ = ring.close(client, null, null) catch { 26 | std.debug.print("Error closing\n", .{}); 27 | std.os.exit(1); 28 | }; 29 | } 30 | 31 | // Connect to the server. 32 | const cqe_connect = try ring.connect(client, &address.any, address.getOsSockLen(), null, null); 33 | assert(cqe_connect.res == 0); 34 | 35 | var input_buffer: [512]u8 = undefined; 36 | var num_ops: u64 = 0; 37 | 38 | while (num_ops < max_ops) : (num_ops += 1) { 39 | // Send it to the server. 40 | const send_result = try ring.send( 41 | client, 42 | buffer_to_send[0..num_bytes_to_send], 43 | @intCast(u32, num_bytes_to_send), 44 | null, 45 | null, 46 | ); 47 | 48 | assert(send_result.res == num_bytes_to_send); 49 | 50 | // Receive the response. 51 | const cqe_recv = try ring.recv(client, input_buffer[0..], 0, null, null); 52 | const num_bytes_received = @intCast(usize, cqe_recv.res); 53 | 54 | assert(num_bytes_received == num_bytes_to_send); 55 | } 56 | } 57 | 58 | // Silly echo server benchmark that sends "hello" 100k times in a loop and then 59 | // outputs throughput. 60 | pub fn benchmark(ring: *AsyncIOUring, result: *BenchmarkResult) !void { 61 | const max_clients = 30; 62 | var client_frames: [max_clients]@Frame(run_client) = undefined; 63 | 64 | var i: u64 = 0; 65 | while (i < max_clients) : (i += 1) { 66 | client_frames[i] = async run_client(ring); 67 | } 68 | 69 | i = 0; 70 | while (i < max_clients) : (i += 1) { 71 | await client_frames[i] catch |err| { 72 | std.debug.print("client had err {}\n", .{err}); 73 | }; 74 | } 75 | result.num_ops = max_clients * max_ops; 76 | } 77 | 78 | pub fn run_benchmark_event_loop(_: u64, result: *BenchmarkResult) !void { 79 | var ring = try IO_Uring.init(4096, 0); 80 | defer ring.deinit(); 81 | var async_ring = AsyncIOUring{ .ring = &ring }; 82 | _ = async benchmark(&async_ring, result); 83 | try async_ring.run_event_loop(); 84 | } 85 | 86 | pub fn main() !void { 87 | const num_threads = 4; 88 | var threads: [num_threads]std.Thread = undefined; 89 | var results: [num_threads]BenchmarkResult = undefined; 90 | 91 | var timer = try Timer.start(); 92 | const start = timer.lap(); 93 | 94 | var i: u64 = 0; 95 | while (i < num_threads) : (i += 1) { 96 | std.debug.print("Spawning\n", .{}); 97 | threads[i] = try std.Thread.spawn(.{}, run_benchmark_event_loop, .{ i, &results[i] }); 98 | } 99 | 100 | i = 0; 101 | 102 | var total_ops: u64 = 0; 103 | while (i < num_threads) : (i += 1) { 104 | std.debug.print("Joining {}\n", .{i}); 105 | std.Thread.join(threads[i]); 106 | total_ops += results[i].num_ops; 107 | } 108 | 109 | const end = timer.read(); 110 | 111 | const elapsed_s = @intToFloat(f64, end - start) / std.time.ns_per_s; 112 | 113 | const ops_per_sec = @intToFloat(f64, total_ops) / elapsed_s; 114 | 115 | std.debug.print("Total throughput: {d} ops per sec\n", .{ops_per_sec}); 116 | } 117 | -------------------------------------------------------------------------------- /examples/src/client.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const IO_Uring = std.os.linux.IO_Uring; 3 | const assert = std.debug.assert; 4 | const net = std.net; 5 | const os = std.os; 6 | const linux = os.linux; 7 | 8 | const io = @import("async_io_uring"); 9 | const AsyncIOUring = io.AsyncIOUring; 10 | const AsyncWriter = io.AsyncWriter; 11 | 12 | // Echo client. Reads a string from stdin, sends it to the server, and prints 13 | // the response. 14 | // 15 | // Note to the reader: CQE stands for completion queue entry in variable names. 16 | // This is the data structure returned by the kernel when an io_uring event is 17 | // complete. 18 | pub fn run_client(ring: *AsyncIOUring) !void { 19 | var writer = try AsyncWriter.init(ring, std.io.getStdErr().handle); 20 | 21 | // Address of the echo server. 22 | const address = try net.Address.parseIp4("127.0.0.1", 3131); 23 | 24 | // Open a socket for connecting to the server. 25 | const server = try os.socket(address.any.family, os.SOCK.STREAM | os.SOCK.CLOEXEC, 0); 26 | defer { 27 | writer.print("Closing connection\n", .{}) catch { 28 | std.debug.print("Error logging connection closure\n", .{}); 29 | std.os.exit(1); 30 | }; 31 | 32 | _ = ring.close(server, null, null) catch { 33 | std.debug.print("Error closing\n", .{}); 34 | std.os.exit(1); 35 | }; 36 | } 37 | 38 | // Connect to the server. 39 | _ = try ring.connect(server, &address.any, address.getOsSockLen(), null, null); 40 | 41 | const stdin_file = std.io.getStdIn(); 42 | const stdin_fd = stdin_file.handle; 43 | var input_buffer: [256]u8 = undefined; 44 | 45 | while (true) { 46 | // Prompt the user for input. 47 | try writer.print("Input: ", .{}); 48 | 49 | const read_timeout = os.linux.kernel_timespec{ .tv_sec = 10, .tv_nsec = 0 }; 50 | // Read a line from stdin with a 10 second timeout. 51 | const read_cqe = ring.do( 52 | io.Read{ .fd = stdin_fd, .buffer = input_buffer[0..], .offset = input_buffer.len }, 53 | io.Timeout{ .ts = &read_timeout, .flags = 0 }, 54 | null, 55 | ) catch |err| { 56 | if (err == error.Cancelled) { 57 | try writer.print("\nTimed out waiting for input, exiting...\n", .{}); 58 | return; 59 | } else return err; 60 | }; 61 | 62 | const num_bytes_read = @intCast(usize, read_cqe.res); 63 | 64 | // Send it to the server. 65 | _ = try ring.send(server, input_buffer[0..num_bytes_read], 0, null, null); 66 | 67 | // Receive response. 68 | const recv_cqe = try ring.recv(server, input_buffer[0..], 0, null, null); 69 | 70 | const num_bytes_received = @intCast(usize, recv_cqe.res); 71 | try writer.print("Received: {s}\n", .{input_buffer[0..num_bytes_received]}); 72 | } 73 | } 74 | 75 | pub fn main() !void { 76 | var ring = try IO_Uring.init(16, 0); 77 | defer ring.deinit(); 78 | 79 | var async_ring = AsyncIOUring{ .ring = &ring }; 80 | 81 | var client_frame = async run_client(&async_ring); 82 | 83 | try async_ring.run_event_loop(); 84 | // Important for propagating any errors received in run_client. 85 | try nosuspend await client_frame; 86 | } 87 | -------------------------------------------------------------------------------- /examples/src/server.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const io = @import("async_io_uring"); 3 | const IO_Uring = std.os.linux.IO_Uring; 4 | const assert = std.debug.assert; 5 | const mem = std.mem; 6 | const net = std.net; 7 | const os = std.os; 8 | const linux = os.linux; 9 | 10 | const AsyncIOUring = io.AsyncIOUring; 11 | const AsyncWriter = io.AsyncWriter; 12 | 13 | // Currently the number of max connections is hardcoded. This allows you to 14 | // avoid heap allocation in growing and shrinking the list of active connections. 15 | const max_connections = 10000; 16 | 17 | pub fn main() !void { 18 | const num_threads = 1; 19 | var threads: [num_threads]std.Thread = undefined; 20 | 21 | // Starts at 1 to reserve id 0 for the main thread. 22 | var i: u64 = 1; 23 | while (i < num_threads) : (i += 1) { 24 | std.debug.print("Spawning thread {}\n", .{i}); 25 | threads[i] = try std.Thread.spawn(.{}, run_server_event_loop, .{i}); 26 | } 27 | 28 | std.debug.print("Starting event loop in main thread (thread 0)\n", .{}); 29 | 30 | // Use the main thread as an event loop as well. 31 | try run_server_event_loop(0); 32 | 33 | std.debug.print("Joining all threads\n", .{}); 34 | for (threads) |t| { 35 | std.Thread.join(t); 36 | } 37 | } 38 | 39 | pub fn run_server_event_loop(id: u64) !void { 40 | var ring = try IO_Uring.init(4096, 0); 41 | defer ring.deinit(); 42 | 43 | var async_ring = AsyncIOUring{ .ring = &ring }; 44 | 45 | // The frame size is very large when the max number of connections is high. 46 | // We could increase stack size but for now we're just allocating it on the 47 | // heap - doesn't seem to have much of an affect on performance (and that 48 | // makes sense because we're only doing it once). 49 | const frame = try std.heap.page_allocator.create(@Frame(run_server)); 50 | defer std.heap.page_allocator.destroy(frame); 51 | frame.* = async run_server(&async_ring, id); 52 | 53 | try async_ring.run_event_loop(); 54 | try nosuspend await frame; 55 | } 56 | 57 | // Open a socket and run the echo server listening on that socket. The server 58 | // can handle up to max_connections concurrent connections, all in a single 59 | // thread.. 60 | pub fn run_server(ring: *AsyncIOUring, id: u64) !void { 61 | const address = try net.Address.parseIp4("127.0.0.1", 3131); 62 | const kernel_backlog = 128; 63 | const server = try os.socket(address.any.family, os.SOCK.STREAM | os.SOCK.CLOEXEC, 0); 64 | defer os.close(server); 65 | try os.setsockopt(server, os.SOL.SOCKET, os.SO.REUSEPORT, &mem.toBytes(@as(c_int, 1))); 66 | try os.bind(server, &address.any, address.getOsSockLen()); 67 | try os.listen(server, kernel_backlog); 68 | 69 | try run_acceptor_loop(ring, server, id); 70 | } 71 | 72 | // Loops accepting new connections and spawning new coroutines to handle those 73 | // connections. 74 | pub fn run_acceptor_loop(ring: *AsyncIOUring, server: os.fd_t, _: u64) !void { 75 | // TODO: Put this in a struct and abstract away some of the connection 76 | // tracking. 77 | var open_conns: [max_connections]@Frame(handle_connection) = undefined; 78 | var closed_conns: [max_connections]u64 = undefined; 79 | var num_open_conns: usize = 0; 80 | var num_closed_conns: usize = 0; 81 | 82 | var writer = try AsyncWriter.init(ring, std.io.getStdErr().handle); 83 | 84 | while (true) { 85 | var accept_addr: os.sockaddr = undefined; 86 | var accept_addr_len: os.socklen_t = @sizeOf(@TypeOf(accept_addr)); 87 | 88 | // Wait for a new connection request. 89 | var accept_cqe = ring.accept(server, &accept_addr, &accept_addr_len, 0, null, null) catch |err| { 90 | try writer.print("Error in run_acceptor_loop: accept {} \n", .{err}); 91 | continue; 92 | }; 93 | 94 | var new_conn_fd = accept_cqe.res; 95 | 96 | // Get an index in the array of open connections for this new 97 | // connection. If we already have max_connections open connections, 98 | // this_conn_idx will be null. 99 | const this_conn_idx = blk: { 100 | if (num_closed_conns > 0) { 101 | // Reuse the last closed connection's index and decrement the 102 | // number of closed connections. 103 | num_closed_conns -= 1; 104 | break :blk closed_conns[num_closed_conns]; 105 | } else { 106 | if (num_open_conns == max_connections) break :blk null; 107 | 108 | const next_idx = num_open_conns; 109 | // We need to expand the number of open connections. 110 | num_open_conns += 1; 111 | break :blk next_idx; 112 | } 113 | }; 114 | 115 | if (this_conn_idx) |idx| { 116 | // std.debug.print("Spawning new connection with index: {} in thread: {} \n", .{idx, id}); 117 | // Spawns a new connection handler in a different coroutine. 118 | open_conns[idx] = async handle_connection(ring, new_conn_fd, idx, &closed_conns, &num_closed_conns); 119 | } else { 120 | try writer.print("Reached connection limit, refusing connection. \n", .{}); 121 | _ = try ring.close(new_conn_fd, null, null); 122 | } 123 | } 124 | 125 | // This isn't really needed since this only happens at process shutdown, 126 | // but why not. 127 | for (open_conns[0..num_open_conns]) |conn| { 128 | await conn; 129 | } 130 | } 131 | 132 | // Does the main echo server loop for a single connection, recieving and 133 | // echoing input over the file descriptor for the client. 134 | pub fn handle_connection(ring: *AsyncIOUring, client: os.fd_t, conn_idx: u64, closed_conns: *[max_connections]u64, num_closed_conns: *usize) !void { 135 | defer { 136 | // std.debug.print("Closing connection with index {}\n", .{conn_idx}); 137 | _ = ring.close(client, null, null) catch |err| { 138 | std.debug.print("Error closing {}\n", .{err}); 139 | std.os.exit(1); 140 | }; 141 | // Return this connection index to the list of free connection indices. 142 | closed_conns[num_closed_conns.*] = conn_idx; 143 | num_closed_conns.* += 1; 144 | } 145 | 146 | // Used to send and receive. 147 | var buffer: [512]u8 = undefined; 148 | 149 | // Loop until the connection is closed, receiving input and sending back 150 | // that input as output. 151 | while (true) { 152 | const recv_cqe = try ring.recv(client, buffer[0..], 0, null, null); 153 | const num_bytes_received = @intCast(usize, recv_cqe.res); 154 | if (num_bytes_received == 0) { 155 | // 0 bytes received indicates orderly connection closure. 156 | break; 157 | } 158 | 159 | _ = try ring.send(client, buffer[0..num_bytes_received], 0, null, null); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /examples/zig.mod: -------------------------------------------------------------------------------- 1 | id: 69mt8h48pb2a2dxf3x11opf52fm0zlpa2dtqwwsa5gwvkc7w 2 | name: examples 3 | license: MIT 4 | description: Example usage of async_io_uring 5 | root_dependencies: 6 | - src: git https://github.com/saltzm/async_io_uring.git 7 | -------------------------------------------------------------------------------- /src/async_io_uring.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const os = std.os; 5 | const linux = os.linux; 6 | const IO_Uring = linux.IO_Uring; 7 | 8 | /// Wrapper for IO_Uring that turns its functions into async functions that suspend after enqueuing 9 | /// entries to the submission queue, and resume and return once a result is available in the 10 | /// completion queue. 11 | /// 12 | /// Usage requires calling AsyncIOUring.run_event_loop to submit and process completion queue 13 | /// entries. 14 | /// 15 | /// AsyncIOUring is NOT thread-safe. If you wish to have a multi-threaded event-loop, you should 16 | /// create one AsyncIOUring object per thread and only use it within the thread where it was 17 | /// created. 18 | /// 19 | /// As an overview for the unfamiliar, io_uring works by allowing users to enqueue requests into a 20 | /// submission queue (e.g. a request to read from a socket) and then submit the submission queue to 21 | /// the kernel for processing. When requests from the submission queue have been satisfied, the 22 | /// result is placed onto completion queue by the kernel. The user is able to either poll the kernel 23 | /// for completion queue results or block until results are available. 24 | /// 25 | /// Note on abbreviations: 26 | /// SQE == submission queue entry 27 | /// CQE == completion queue entry 28 | /// 29 | /// Parts of the function-level comments were copied from the IO_Uring library. More details on each 30 | /// function can be found in the comments of the IO_Uring library functions that this wraps, since 31 | /// this is just a thin wrapper for those. If any of those functions require modification of the SQE 32 | /// before enqueueing an operation into the submission queue, users of AsyncIOUring must make their 33 | /// own operation struct with a custom enqueueSubmissionQueueEntries function. See 34 | /// testReadWithManualAPIAndOverridenEnqueueSqes and testTimeoutRemoveCanUpdateTimeout for examples. 35 | /// 36 | /// TODO: 37 | /// * Constrain the error set of `do` so that individual operations can 38 | /// constrain their own error sets. 39 | pub const AsyncIOUring = struct { 40 | /// Users may access this field directly to call functions on the IO_Uring which do not require 41 | /// use of the submission queue, such as register_files and the other register_* functions. 42 | ring: *IO_Uring = undefined, 43 | 44 | /// Number of events submitted minus number of events completed. We can exit when this is 0. 45 | /// 46 | /// This should not be modified outside of AsyncIOUring. 47 | num_outstanding_events: u64 = 0, 48 | 49 | /// Runs a loop to submit tasks on the underlying IO_Uring and block waiting for completion 50 | /// events. When a completion queue event (cqe) is available, it will resume the coroutine that 51 | /// submitted the request corresponding to that cqe. 52 | pub fn run_event_loop(self: *AsyncIOUring) !void { 53 | // TODO: Make the size of this a comptime parameter? 54 | var cqes: [4096]linux.io_uring_cqe = undefined; 55 | // Loop until no new events were processed. This happens only when no new events were 56 | // submitted or completed, which means there's no more work left to do. 57 | while (true) { 58 | const num_events_processed = try self.process_outstanding_events(cqes[0..]); 59 | if (num_events_processed == 0) { 60 | break; 61 | } 62 | } 63 | } 64 | 65 | /// Submits any outstanding requests, and processes events in the completion queue. When a 66 | /// completion queue event (cqe) is available, the coroutine that submitted the request 67 | /// corresponding to that cqe will be resumed. 68 | /// 69 | /// This may be used for more custom use cases that want to control how iterations of the event 70 | /// loop are scheduled. You should not be using this if you're also using run_event_loop. 71 | /// 72 | /// Returns the number of events that were processed in the completion queue. If this number is 73 | /// 0, that means no new work was submitted since the last time this function was called. 74 | pub fn process_outstanding_events(self: *AsyncIOUring, cqes: []linux.io_uring_cqe) !u32 { 75 | const num_submitted = try self.ring.submit(); 76 | self.num_outstanding_events += num_submitted; 77 | 78 | // If we have no outstanding events even after submitting, that means there's no more work 79 | // to be done and we can exit. 80 | if (self.num_outstanding_events == 0) { 81 | return 0; 82 | } 83 | 84 | // The second parameter of copy_cqes indicates how many events we should wait for in the 85 | // kernel before being resumed. We want our program to resume as soon as any event we've 86 | // submitted is ready, so we set the second parameter to 1. 87 | const num_ready_cqes = try self.ring.copy_cqes(cqes[0..], 1); 88 | 89 | self.num_outstanding_events -= num_ready_cqes; 90 | 91 | for (cqes[0..num_ready_cqes]) |cqe| { 92 | if (cqe.user_data != 0) { 93 | var resume_node = @intToPtr(*ResumeNode, cqe.user_data); 94 | resume_node.result = cqe; 95 | // Resume the frame that enqueued the original request. 96 | resume resume_node.frame; 97 | } 98 | } 99 | return num_ready_cqes; 100 | } 101 | 102 | /// Submits a user-supplied IO_Uring operation to the submission queue and suspends until the 103 | /// result of that operation is available in the completion queue. 104 | /// 105 | /// If a timeout is supplied, that timeout will be set on the provided operation and if the 106 | /// timeout expires before the operation completes, the operation will return error.Cancelled. 107 | /// 108 | /// If a pointer to an id is supplied, that id will be set to a value that can be used to cancel 109 | /// the operation using the function AsyncIOUring.cancel. This id is only valid prior to 110 | /// awaiting the result of the call to 'do'. 111 | /// 112 | /// Note that operations may non-deterministically return the error code error.Cancelled if 113 | /// cancelled by the kernel. (This corresponds to EINTR.) If you wish to retry on such errors, 114 | /// you must do so manually. 115 | /// TODO: Consider doing this automatically or allowing a parameter that lets users decide to 116 | /// retry on Cancelled. The problem is that if they set a timeout then Cancelled is actually 117 | /// expected. We could also possibly always retry unless timeout or id are set, since if neither 118 | /// are provided then we know the user did not expect cancellation to occur. 119 | pub fn do( 120 | self: *AsyncIOUring, 121 | op: anytype, 122 | maybe_timeout: ?Timeout, 123 | maybe_id: ?*u64, 124 | ) !linux.io_uring_cqe { 125 | var node = ResumeNode{ .frame = @frame(), .result = undefined }; 126 | 127 | // Check if the submission queue has enough space for this operation and its timeout, and if 128 | // not, submit the current entries in the queue and wait for enough space to be available in 129 | // the queue to submit this operation. 130 | { 131 | const num_required_sqes_for_op = op.getNumRequiredSubmissionQueueEntries(); 132 | const num_required_sqes = if (maybe_timeout) |_| num_required_sqes_for_op + 1 else num_required_sqes_for_op; 133 | 134 | const num_free_entries_in_sq = @intCast(u32, self.ring.sq.sqes.len - self.ring.sq_ready()); 135 | if (num_free_entries_in_sq < num_required_sqes) { 136 | const num_submitted = try self.ring.submit_and_wait(num_required_sqes - 137 | num_free_entries_in_sq); 138 | self.num_outstanding_events += num_submitted; 139 | } 140 | } 141 | 142 | // Enqueue the operation's SQEs into the submission queue. 143 | const sqe = try op.enqueueSubmissionQueueEntries(self.ring, @ptrToInt(&node)); 144 | // Attach a linked timeout if one is supplied. 145 | if (maybe_timeout) |t| { 146 | sqe.flags |= linux.IOSQE_IO_LINK; 147 | // No user data - we don't care about the result, since it will show up in the result of 148 | // sqe as -INTR if the timeout expires before the operation completes. 149 | _ = try self.ring.link_timeout(0, t.ts, t.flags); 150 | } 151 | 152 | // Set the id for cancellation if one is supplied. Note: This must go prior to suspend. 153 | if (maybe_id) |id| { 154 | id.* = @ptrToInt(&node); 155 | } 156 | 157 | // Suspend here until resumed by the event loop when the result of this operation is 158 | // processed in the completion queue. 159 | suspend {} 160 | 161 | // If the return code indicates success, return the result - otherwise return an op-defined 162 | // zig error corresponding to the Linux error code. 163 | return switch (node.result.err()) { 164 | .SUCCESS => node.result, 165 | else => |linux_err| if (@TypeOf(op).convertError(linux_err)) |err| { 166 | return err; 167 | } else { 168 | return node.result; 169 | }, 170 | }; 171 | } 172 | 173 | /// Queues an SQE to remove an existing operation and suspends until the operation has been 174 | /// cancelled (or been found not to exist). 175 | /// 176 | /// Returns a pointer to the CQE. 177 | /// 178 | /// The operation is identified by the operation id passed to AsyncIOUring.do. 179 | /// 180 | /// The completion event result will be `0` if the operation was found and cancelled 181 | /// successfully. 182 | /// 183 | /// If the operation was found but was already in progress, it will return 184 | /// error.OperationAlreadyInProgress. 185 | /// 186 | /// If the operation was not found, it will return error.OperationNotFound. 187 | pub fn cancel( 188 | self: *AsyncIOUring, 189 | operation_id: u64, 190 | flags: u32, 191 | maybe_timeout: ?Timeout, 192 | maybe_id: ?*u64, 193 | ) !linux.io_uring_cqe { 194 | return self.do( 195 | Cancel{ .cancel_user_data = operation_id, .flags = flags }, 196 | maybe_timeout, 197 | maybe_id, 198 | ); 199 | } 200 | 201 | /// Queues an SQE to register a timeout operation and suspends until the operation has been 202 | /// completed. 203 | /// 204 | /// Returns the CQE for the operation. 205 | pub fn timeout( 206 | self: *AsyncIOUring, 207 | ts: *const os.linux.kernel_timespec, 208 | count: u32, 209 | flags: u32, 210 | // Note that there's no ability to add a timeout to a timeout because that wouldn't make 211 | // sense. 212 | maybe_id: ?*u64, 213 | ) !linux.io_uring_cqe { 214 | return self.do(TimeOut{ .ts = ts, .count = count, .flags = flags }, null, maybe_id); 215 | } 216 | 217 | /// Queues an SQE to remove an existing timeout operation and suspends until the operation has 218 | /// been completed. 219 | /// 220 | /// The timeout is identified by its `id`. 221 | /// 222 | /// Returns the CQE for the operation if removing the timeout was successful. Otherwise returns 223 | /// an error (see TimeoutRemove.convertError for possible errors). 224 | pub fn timeout_remove( 225 | self: *AsyncIOUring, 226 | timeout_id: u64, 227 | flags: u32, 228 | maybe_timeout: ?Timeout, 229 | maybe_id: ?*u64, 230 | ) !linux.io_uring_cqe { 231 | return self.do( 232 | TimeoutRemove{ .timeout_user_data = timeout_id, .flags = flags }, 233 | maybe_timeout, 234 | maybe_id, 235 | ); 236 | } 237 | 238 | /// Queues an SQE to perform a `poll(2)` and suspends until the operation has been completed. 239 | /// 240 | /// Returns the CQE for the operation. 241 | pub fn poll_add( 242 | self: *AsyncIOUring, 243 | fd: os.fd_t, 244 | poll_mask: u32, 245 | maybe_timeout: ?Timeout, 246 | maybe_id: ?*u64, 247 | ) !linux.io_uring_cqe { 248 | return self.do(PollAdd{ .fd = fd, .poll_mask = poll_mask }, maybe_timeout, maybe_id); 249 | } 250 | 251 | /// Queues an SQE to remove an existing poll operation and suspends until the operation has been 252 | /// completed. 253 | /// 254 | /// The poll operation to be removed is identified by its `id`. 255 | /// 256 | /// Returns the CQE for the operation. 257 | pub fn poll_remove( 258 | self: *AsyncIOUring, 259 | poll_id: u64, 260 | maybe_timeout: ?Timeout, 261 | maybe_id: ?*u64, 262 | ) !linux.io_uring_cqe { 263 | return self.do(PollRemove{ .poll_id = poll_id }, maybe_timeout, maybe_id); 264 | } 265 | 266 | /// Queues an SQE to perform an `fsync(2)` and suspends until the operation has been completed. 267 | /// 268 | /// Returns the CQE for the operation. 269 | pub fn fsync( 270 | self: *AsyncIOUring, 271 | fd: os.fd_t, 272 | flags: u32, 273 | maybe_timeout: ?Timeout, 274 | maybe_id: ?*u64, 275 | ) !linux.io_uring_cqe { 276 | return self.do(Fsync{ .fd = fd, .flags = flags }, maybe_timeout, maybe_id); 277 | } 278 | 279 | /// Queues an SQE to perform a no-op and suspends until the operation has been completed. 280 | /// 281 | /// Returns the CQE for the operation. 282 | pub fn nop( 283 | self: *AsyncIOUring, 284 | maybe_timeout: ?Timeout, 285 | maybe_id: ?*u64, 286 | ) !linux.io_uring_cqe { 287 | return self.do(Nop{}, maybe_timeout, maybe_id); 288 | } 289 | 290 | /// Queues an SQE to perform a `read(2)` and suspends until the operation has been completed. 291 | /// 292 | /// Returns the CQE for the operation. 293 | pub fn read( 294 | self: *AsyncIOUring, 295 | fd: os.fd_t, 296 | buffer: []u8, 297 | offset: u64, 298 | maybe_timeout: ?Timeout, 299 | maybe_id: ?*u64, 300 | ) !linux.io_uring_cqe { 301 | return self.do( 302 | Read{ .fd = fd, .buffer = buffer, .offset = offset }, 303 | maybe_timeout, 304 | maybe_id, 305 | ); 306 | } 307 | 308 | /// Queues an SQE to perform a `write(2)` and suspends until the operation has been completed. 309 | /// 310 | /// Returns the CQE for the operation. 311 | pub fn write( 312 | self: *AsyncIOUring, 313 | fd: os.fd_t, 314 | buffer: []const u8, 315 | offset: u64, 316 | maybe_timeout: ?Timeout, 317 | maybe_id: ?*u64, 318 | ) !linux.io_uring_cqe { 319 | return self.do( 320 | Write{ .fd = fd, .buffer = buffer, .offset = offset }, 321 | maybe_timeout, 322 | maybe_id, 323 | ); 324 | } 325 | 326 | /// Queues an SQE to perform a `preadv()` and suspends until the operation has been completed. 327 | /// 328 | /// Returns the CQE for the operation. 329 | pub fn readv( 330 | self: *AsyncIOUring, 331 | fd: os.fd_t, 332 | iovecs: []const os.iovec, 333 | offset: u64, 334 | maybe_timeout: ?Timeout, 335 | maybe_id: ?*u64, 336 | ) !linux.io_uring_cqe { 337 | return self.do( 338 | ReadV{ .fd = fd, .iovecs = iovecs, .offset = offset }, 339 | maybe_timeout, 340 | maybe_id, 341 | ); 342 | } 343 | 344 | /// Queues an SQE to perform a IORING_OP_READ_FIXED and suspends until the operation has been 345 | /// completed. 346 | /// 347 | /// Returns the CQE for the operation. 348 | pub fn read_fixed( 349 | self: *AsyncIOUring, 350 | fd: os.fd_t, 351 | buffer: *os.iovec, 352 | offset: u64, 353 | buffer_index: u16, 354 | maybe_timeout: ?Timeout, 355 | maybe_id: ?*u64, 356 | ) !linux.io_uring_cqe { 357 | return self.do( 358 | ReadFixed{ .fd = fd, .buffer = buffer, .offset = offset, .buffer_index = buffer_index }, 359 | maybe_timeout, 360 | maybe_id, 361 | ); 362 | } 363 | 364 | /// Queues an SQE to perform a `pwritev()` and suspends until the operation has been completed. 365 | /// 366 | /// Returns the CQE for the operation. 367 | pub fn writev( 368 | self: *AsyncIOUring, 369 | fd: os.fd_t, 370 | iovecs: []const os.iovec_const, 371 | offset: u64, 372 | maybe_timeout: ?Timeout, 373 | maybe_id: ?*u64, 374 | ) !linux.io_uring_cqe { 375 | return self.do( 376 | WriteV{ .fd = fd, .iovecs = iovecs, .offset = offset }, 377 | maybe_timeout, 378 | maybe_id, 379 | ); 380 | } 381 | 382 | /// Queues an SQE to perform a IORING_OP_WRITE_FIXED and suspends until the operation has been 383 | /// completed. 384 | /// 385 | /// Returns the CQE for the operation. 386 | pub fn write_fixed( 387 | self: *AsyncIOUring, 388 | fd: os.fd_t, 389 | buffer: *os.iovec, 390 | offset: u64, 391 | buffer_index: u16, 392 | maybe_timeout: ?Timeout, 393 | maybe_id: ?*u64, 394 | ) !linux.io_uring_cqe { 395 | return self.do( 396 | WriteFixed{ .fd = fd, .buffer = buffer, .offset = offset, .buffer_index = buffer_index }, 397 | maybe_timeout, 398 | maybe_id, 399 | ); 400 | } 401 | 402 | /// Queues an SQE to perform an `accept4(2)` on a socket and suspends until the operation has 403 | /// been completed. 404 | /// 405 | /// Returns the CQE for the operation. 406 | pub fn accept( 407 | self: *AsyncIOUring, 408 | fd: os.fd_t, 409 | addr: *os.sockaddr, 410 | addrlen: *os.socklen_t, 411 | flags: u32, 412 | maybe_timeout: ?Timeout, 413 | maybe_id: ?*u64, 414 | ) !linux.io_uring_cqe { 415 | return self.do( 416 | Accept{ .fd = fd, .addr = addr, .addrlen = addrlen, .flags = flags }, 417 | maybe_timeout, 418 | maybe_id, 419 | ); 420 | } 421 | 422 | /// Queue an SQE to perform a `connect(2)` on a socket and suspends until the operation has been 423 | /// completed. 424 | /// 425 | /// Returns the CQE for the operation. 426 | pub fn connect( 427 | self: *AsyncIOUring, 428 | fd: os.fd_t, 429 | addr: *const os.sockaddr, 430 | addrlen: os.socklen_t, 431 | maybe_timeout: ?Timeout, 432 | maybe_id: ?*u64, 433 | ) !linux.io_uring_cqe { 434 | return self.do( 435 | Connect{ .fd = fd, .addr = addr, .addrlen = addrlen }, 436 | maybe_timeout, 437 | maybe_id, 438 | ); 439 | } 440 | 441 | /// Queues an SQE to perform a `epoll_ctl(2)` and suspends until the operation has been 442 | /// completed. 443 | /// 444 | /// Returns the CQE for the operation. 445 | pub fn epoll_ctl( 446 | self: *AsyncIOUring, 447 | epfd: os.fd_t, 448 | fd: os.fd_t, 449 | op: u32, 450 | ev: ?*linux.epoll_event, 451 | maybe_timeout: ?Timeout, 452 | maybe_id: ?*u64, 453 | ) !linux.io_uring_cqe { 454 | return self.do( 455 | EpollCtl{ .epfd = epfd, .fd = fd, .op = op, .ev = ev }, 456 | maybe_timeout, 457 | maybe_id, 458 | ); 459 | } 460 | 461 | /// Queues an SQE to perform a `recv(2)` and suspends 462 | /// until the operation has been completed. 463 | /// 464 | /// Returns the CQE for the operation. 465 | pub fn recv( 466 | self: *AsyncIOUring, 467 | fd: os.fd_t, 468 | buffer: []u8, 469 | flags: u32, 470 | maybe_timeout: ?Timeout, 471 | maybe_id: ?*u64, 472 | ) !linux.io_uring_cqe { 473 | return self.do(Recv{ .fd = fd, .buffer = buffer, .flags = flags }, maybe_timeout, maybe_id); 474 | } 475 | 476 | /// Queues an SQE to perform a `send(2)` and suspends until the operation has been completed. 477 | /// 478 | /// Returns the CQE for the operation. 479 | pub fn send( 480 | self: *AsyncIOUring, 481 | fd: os.fd_t, 482 | buffer: []const u8, 483 | flags: u32, 484 | maybe_timeout: ?Timeout, 485 | maybe_id: ?*u64, 486 | ) !linux.io_uring_cqe { 487 | return self.do(Send{ .fd = fd, .buffer = buffer, .flags = flags }, maybe_timeout, maybe_id); 488 | } 489 | 490 | /// Queues an SQE to perform an `openat(2)` and suspends until the operation has been completed. 491 | /// 492 | /// Returns the CQE for the operation. 493 | pub fn openat( 494 | self: *AsyncIOUring, 495 | fd: os.fd_t, 496 | path: [*:0]const u8, 497 | flags: u32, 498 | mode: os.mode_t, 499 | maybe_timeout: ?Timeout, 500 | maybe_id: ?*u64, 501 | ) !linux.io_uring_cqe { 502 | return self.do( 503 | OpenAt{ .fd = fd, .path = path, .flags = flags, .mode = mode }, 504 | maybe_timeout, 505 | maybe_id, 506 | ); 507 | } 508 | 509 | /// Queues an SQE to perform a `close(2)` and suspends until the operation has been completed. 510 | /// 511 | /// Returns the CQE for the operation. 512 | pub fn close( 513 | self: *AsyncIOUring, 514 | fd: os.fd_t, 515 | maybe_timeout: ?Timeout, 516 | maybe_id: ?*u64, 517 | ) !linux.io_uring_cqe { 518 | return self.do(Close{ .fd = fd }, maybe_timeout, maybe_id); 519 | } 520 | 521 | /// Queues an SQE to perform an `fallocate(2)` and suspends until the operation has been 522 | /// completed. 523 | /// 524 | /// Returns the CQE for the operation. 525 | pub fn fallocate( 526 | self: *AsyncIOUring, 527 | fd: os.fd_t, 528 | mode: i32, 529 | offset: u64, 530 | len: u64, 531 | maybe_timeout: ?Timeout, 532 | maybe_id: ?*u64, 533 | ) !linux.io_uring_cqe { 534 | return self.do( 535 | Fallocate{ .fd = fd, .mode = mode, .offset = offset, .len = len }, 536 | maybe_timeout, 537 | maybe_id, 538 | ); 539 | } 540 | 541 | /// Queues an SQE to perform an `statx(2)` and suspends until the operation has been completed. 542 | /// 543 | /// Returns the CQE for the operation. 544 | pub fn statx( 545 | self: *AsyncIOUring, 546 | fd: os.fd_t, 547 | path: [:0]const u8, 548 | flags: u32, 549 | mask: u32, 550 | buf: *linux.Statx, 551 | maybe_timeout: ?Timeout, 552 | maybe_id: ?*u64, 553 | ) !linux.io_uring_cqe { 554 | return self.do( 555 | Statx{ .fd = fd, .path = path, .flags = flags, .mask = mask, .buf = buf }, 556 | maybe_timeout, 557 | maybe_id, 558 | ); 559 | } 560 | 561 | /// Queues an SQE to perform a `shutdown(2)` and suspends until the operation has been 562 | /// completed. 563 | /// 564 | /// Returns the CQE for the operation. 565 | pub fn shutdown( 566 | self: *AsyncIOUring, 567 | sockfd: os.socket_t, 568 | how: u32, 569 | maybe_timeout: ?Timeout, 570 | maybe_id: ?*u64, 571 | ) !linux.io_uring_cqe { 572 | return self.do(Shutdown{ .sockfd = sockfd, .how = how }, maybe_timeout, maybe_id); 573 | } 574 | 575 | /// Queues an SQE to perform a `renameat2(2)` and suspends until the operation has been 576 | /// completed. 577 | /// 578 | /// Returns the CQE for the operation. 579 | pub fn renameat( 580 | self: *AsyncIOUring, 581 | old_dir_fd: os.fd_t, 582 | old_path: [*:0]const u8, 583 | new_dir_fd: os.fd_t, 584 | new_path: [*:0]const u8, 585 | flags: u32, 586 | maybe_timeout: ?Timeout, 587 | maybe_id: ?*u64, 588 | ) !linux.io_uring_cqe { 589 | return self.do(RenameAt{ 590 | .old_dir_fd = old_dir_fd, 591 | .old_path = old_path, 592 | .new_dir_fd = new_dir_fd, 593 | .new_path = new_path, 594 | .flags = flags, 595 | }, maybe_timeout, maybe_id); 596 | } 597 | 598 | /// Queues an SQE to perform a `unlinkat(2)` and suspends until the operation has been 599 | /// completed. 600 | /// 601 | /// Returns the CQE for the operation. 602 | pub fn unlinkat( 603 | self: *AsyncIOUring, 604 | dir_fd: os.fd_t, 605 | path: [*:0]const u8, 606 | flags: u32, 607 | maybe_timeout: ?Timeout, 608 | maybe_id: ?*u64, 609 | ) !linux.io_uring_cqe { 610 | return self.do( 611 | UnlinkAt{ .dir_fd = dir_fd, .path = path, .flags = flags }, 612 | maybe_timeout, 613 | maybe_id, 614 | ); 615 | } 616 | 617 | /// Queues an SQE to perform a `mkdirat(2)` and suspends until the operation has been completed. 618 | /// 619 | /// Returns the CQE for the operation. 620 | pub fn mkdirat( 621 | self: *AsyncIOUring, 622 | dir_fd: os.fd_t, 623 | path: [*:0]const u8, 624 | mode: os.mode_t, 625 | maybe_timeout: ?Timeout, 626 | maybe_id: ?*u64, 627 | ) !linux.io_uring_cqe { 628 | return self.do( 629 | MkdirAt{ .dir_fd = dir_fd, .path = path, .mode = mode }, 630 | maybe_timeout, 631 | maybe_id, 632 | ); 633 | } 634 | 635 | /// Queues an SQE to perform a `symlinkat(2)` and suspends until the operation has been 636 | /// completed. 637 | /// 638 | /// Returns the CQE for the operation. 639 | pub fn symlinkat( 640 | self: *AsyncIOUring, 641 | target: [*:0]const u8, 642 | new_dir_fd: os.fd_t, 643 | link_path: [*:0]const u8, 644 | maybe_timeout: ?Timeout, 645 | maybe_id: ?*u64, 646 | ) !linux.io_uring_cqe { 647 | return self.do( 648 | SymlinkAt{ .target = target, .new_dir_fd = new_dir_fd, .link_path = link_path }, 649 | maybe_timeout, 650 | maybe_id, 651 | ); 652 | } 653 | 654 | /// Queues an SQE to perform a `linkat(2)` and suspends until the operation has been completed. 655 | /// 656 | /// Returns the CQE for the operation. 657 | pub fn linkat( 658 | self: *AsyncIOUring, 659 | old_dir_fd: os.fd_t, 660 | old_path: [*:0]const u8, 661 | new_dir_fd: os.fd_t, 662 | new_path: [*:0]const u8, 663 | flags: u32, 664 | maybe_timeout: ?Timeout, 665 | maybe_id: ?*u64, 666 | ) !linux.io_uring_cqe { 667 | return self.do(LinkAt{ 668 | .old_dir_fd = old_dir_fd, 669 | .old_path = old_path, 670 | .new_dir_fd = new_dir_fd, 671 | .new_path = new_path, 672 | .flags = flags, 673 | }, maybe_timeout, maybe_id); 674 | } 675 | }; 676 | 677 | /// Used as user data for submission queue entries, so that the event loop can have resume the 678 | /// callers frame. 679 | const ResumeNode = struct { 680 | frame: anyframe = undefined, 681 | result: linux.io_uring_cqe = undefined, 682 | }; 683 | 684 | /// Represents an operation timeout. 685 | pub const Timeout = struct { 686 | ts: *const os.linux.kernel_timespec, 687 | flags: u32, 688 | }; 689 | 690 | /// An object that can be used to do async file I/O with the same syntax as `std.debug.print`. 691 | pub const AsyncWriter = struct { 692 | const Self = @This(); 693 | 694 | ring: *AsyncIOUring, 695 | writer: std.io.Writer(AsyncWriterContext, ErrorSetOf(asyncWrite), asyncWrite), 696 | 697 | /// Expects fd to be already open for appending. 698 | pub fn init(ring: *AsyncIOUring, fd: os.fd_t) !AsyncWriter { 699 | return AsyncWriter{ .ring = ring, .writer = asyncWriter(ring, fd) }; 700 | } 701 | 702 | pub fn print(self: @This(), comptime format: []const u8, args: anytype) !void { 703 | try self.writer.print(format, args); 704 | } 705 | }; 706 | 707 | const AsyncWriterContext = struct { ring: *AsyncIOUring, fd: os.fd_t }; 708 | 709 | fn asyncWrite(context: AsyncWriterContext, buffer: []const u8) !usize { 710 | const cqe = try context.ring.write(context.fd, buffer, 0, null, null); 711 | return @intCast(usize, cqe.res); 712 | } 713 | 714 | /// Copied from x/net/tcp.zig 715 | fn ErrorSetOf(comptime Function: anytype) type { 716 | return @typeInfo(@typeInfo(@TypeOf(Function)).Fn.return_type.?).ErrorUnion.error_set; 717 | } 718 | 719 | /// Wrap `AsyncIOUring` into `std.io.Writer`. 720 | fn asyncWriter(ring: *AsyncIOUring, fd: os.fd_t) std.io.Writer(AsyncWriterContext, ErrorSetOf(asyncWrite), asyncWrite) { 721 | return .{ .context = .{ .ring = ring, .fd = fd } }; 722 | } 723 | 724 | //////////////////////////////////////////////////////////////////////////////// 725 | // The following are structs defined for individual operations that may be // 726 | // passed directly to the `AsyncIOUring.do` function. Users may define their // 727 | // own structs with the same interface as these to implement custom use cases // 728 | // that require e.g. modification of the SQE prior to submission. See test // 729 | // cases for examples. // 730 | //////////////////////////////////////////////////////////////////////////////// 731 | 732 | const DefaultError = error{Cancelled} || std.os.UnexpectedError; 733 | 734 | /// Fallback error-handling for interruption/cancellation errors. 735 | fn defaultConvertError(linux_err: os.E) DefaultError { 736 | return switch (linux_err) { 737 | .INTR, .CANCELED => error.Cancelled, 738 | else => |err| os.unexpectedErrno(err), 739 | }; 740 | } 741 | 742 | pub const Read = struct { 743 | fd: os.fd_t, 744 | buffer: []u8, 745 | offset: u64, 746 | 747 | const Error = std.os.ReadError || DefaultError; 748 | 749 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 750 | return 1; 751 | } 752 | 753 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 754 | return try ring.read(user_data, op.fd, .{ .buffer = op.buffer }, op.offset); 755 | } 756 | 757 | /// See read man pages for specific meaning of possible errors: 758 | /// http://manpages.ubuntu.com/manpages/impish/man2/read.2.html#errors 759 | pub fn convertError(linux_err: os.E) ?Error { 760 | return switch (linux_err) { 761 | // Copied from std.os.read. 762 | .INVAL => unreachable, 763 | .FAULT => unreachable, 764 | .AGAIN => return error.WouldBlock, 765 | .BADF => return error.NotOpenForReading, // Can be a race condition. 766 | .IO => return error.InputOutput, 767 | .ISDIR => return error.IsDir, 768 | .NOBUFS => return error.SystemResources, 769 | .NOMEM => return error.SystemResources, 770 | .CONNRESET => return error.ConnectionResetByPeer, 771 | .TIMEDOUT => return error.ConnectionTimedOut, 772 | else => |err| defaultConvertError(err), 773 | }; 774 | } 775 | }; 776 | 777 | pub const Write = struct { 778 | fd: os.fd_t, 779 | buffer: []const u8, 780 | offset: u64, 781 | 782 | const Error = std.os.WriteError || DefaultError; 783 | 784 | pub fn convertError(linux_err: os.E) ?Error { 785 | return switch (linux_err) { 786 | // Copied from std.os.write. 787 | .INVAL => unreachable, 788 | .FAULT => unreachable, 789 | .AGAIN => unreachable, 790 | .BADF => error.NotOpenForWriting, // can be a race condition. 791 | .DESTADDRREQ => unreachable, // `connect` was never called. 792 | .DQUOT => error.DiskQuota, 793 | .FBIG => error.FileTooBig, 794 | .IO => error.InputOutput, 795 | .NOSPC => error.NoSpaceLeft, 796 | .PERM => error.AccessDenied, 797 | .PIPE => error.BrokenPipe, 798 | else => |err| defaultConvertError(err), 799 | }; 800 | } 801 | 802 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 803 | return 1; 804 | } 805 | 806 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 807 | return ring.write(user_data, op.fd, op.buffer, op.offset); 808 | } 809 | }; 810 | 811 | pub const ReadV = struct { 812 | fd: os.fd_t, 813 | iovecs: []const os.iovec, 814 | offset: u64, 815 | 816 | const Error = std.os.PReadError || DefaultError; 817 | 818 | pub fn convertError(linux_err: os.E) ?Error { 819 | // Copied from std.os.preadv. 820 | return switch (linux_err) { 821 | .INVAL => unreachable, 822 | .FAULT => unreachable, 823 | .AGAIN => error.WouldBlock, 824 | .BADF => error.NotOpenForReading, // can be a race condition 825 | .IO => error.InputOutput, 826 | .ISDIR => error.IsDir, 827 | .NOBUFS => error.SystemResources, 828 | .NOMEM => error.SystemResources, 829 | .NXIO => error.Unseekable, 830 | .SPIPE => error.Unseekable, 831 | .OVERFLOW => error.Unseekable, 832 | else => |err| defaultConvertError(err), 833 | }; 834 | } 835 | 836 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 837 | return 1; 838 | } 839 | 840 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 841 | return ring.readv(user_data, op.fd, op.iovecs, op.offset); 842 | } 843 | }; 844 | 845 | pub const ReadFixed = struct { 846 | fd: os.fd_t, 847 | buffer: *os.iovec, 848 | offset: u64, 849 | buffer_index: u16, 850 | 851 | // TODO: Double-check this. 852 | const convertError = Read.convertError; 853 | 854 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 855 | return 1; 856 | } 857 | 858 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 859 | return ring.read_fixed(user_data, op.fd, op.buffer, op.offset, op.buffer_index); 860 | } 861 | }; 862 | 863 | pub const WriteV = struct { 864 | fd: os.fd_t, 865 | iovecs: []const os.iovec_const, 866 | offset: u64, 867 | 868 | const Error = std.os.PWriteError || DefaultError; 869 | 870 | pub fn convertError(linux_err: os.E) ?Error { 871 | // Copied from std.os.pwritev. 872 | return switch (linux_err) { 873 | .INVAL => unreachable, 874 | .FAULT => unreachable, 875 | .AGAIN => error.WouldBlock, 876 | .BADF => error.NotOpenForWriting, // Can be a race condition. 877 | .DESTADDRREQ => unreachable, // `connect` was never called. 878 | .DQUOT => error.DiskQuota, 879 | .FBIG => error.FileTooBig, 880 | .IO => error.InputOutput, 881 | .NOSPC => error.NoSpaceLeft, 882 | .PERM => error.AccessDenied, 883 | .PIPE => error.BrokenPipe, 884 | .CONNRESET => error.ConnectionResetByPeer, 885 | else => |err| defaultConvertError(err), 886 | }; 887 | } 888 | 889 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 890 | return 1; 891 | } 892 | 893 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 894 | return ring.writev(user_data, op.fd, op.iovecs, op.offset); 895 | } 896 | }; 897 | 898 | pub const WriteFixed = struct { 899 | fd: os.fd_t, 900 | buffer: *os.iovec, 901 | offset: u64, 902 | buffer_index: u16, 903 | 904 | // TODO: Double-check this. 905 | const convertError = Write.convertError; 906 | 907 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 908 | return 1; 909 | } 910 | 911 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 912 | return ring.write_fixed(user_data, op.fd, op.buffer, op.offset, op.buffer_index); 913 | } 914 | }; 915 | 916 | pub const Accept = struct { 917 | fd: os.fd_t, 918 | addr: *os.sockaddr, 919 | addrlen: *os.socklen_t, 920 | flags: u32, 921 | 922 | const Error = std.os.AcceptError || DefaultError; 923 | 924 | pub fn convertError(linux_err: os.E) ?Error { 925 | // Copied from std.os.accept. 926 | return switch (linux_err) { 927 | .AGAIN => error.WouldBlock, 928 | .BADF => unreachable, // always a race condition 929 | .CONNABORTED => error.ConnectionAborted, 930 | .FAULT => unreachable, 931 | .INVAL => error.SocketNotListening, 932 | .NOTSOCK => unreachable, 933 | .MFILE => error.ProcessFdQuotaExceeded, 934 | .NFILE => error.SystemFdQuotaExceeded, 935 | .NOBUFS => error.SystemResources, 936 | .NOMEM => error.SystemResources, 937 | .OPNOTSUPP => unreachable, 938 | .PROTO => error.ProtocolFailure, 939 | .PERM => error.BlockedByFirewall, 940 | else => |err| defaultConvertError(err), 941 | }; 942 | } 943 | 944 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 945 | return 1; 946 | } 947 | 948 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 949 | return ring.accept(user_data, op.fd, op.addr, op.addrlen, op.flags); 950 | } 951 | }; 952 | 953 | pub const Connect = struct { 954 | fd: os.fd_t, 955 | addr: *const os.sockaddr, 956 | addrlen: os.socklen_t, 957 | 958 | const Error = std.os.ConnectError || DefaultError; 959 | 960 | pub fn convertError(linux_err: os.E) ?Error { 961 | // Copied from std.os.connect. 962 | return switch (linux_err) { 963 | .ACCES => error.PermissionDenied, 964 | .PERM => error.PermissionDenied, 965 | .ADDRINUSE => error.AddressInUse, 966 | .ADDRNOTAVAIL => error.AddressNotAvailable, 967 | .AFNOSUPPORT => error.AddressFamilyNotSupported, 968 | .AGAIN, .INPROGRESS => error.WouldBlock, 969 | .ALREADY => error.ConnectionPending, 970 | .BADF => unreachable, // sockfd is not a valid open file descriptor. 971 | .CONNREFUSED => error.ConnectionRefused, 972 | .CONNRESET => error.ConnectionResetByPeer, 973 | .FAULT => unreachable, // The socket structure address is outside the user's address space. 974 | .ISCONN => unreachable, // The socket is already connected. 975 | .NETUNREACH => error.NetworkUnreachable, 976 | .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. 977 | .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol. 978 | .TIMEDOUT => error.ConnectionTimedOut, 979 | .NOENT => error.FileNotFound, // Returned when socket is AF.UNIX and the given path does not exist. 980 | else => |err| defaultConvertError(err), 981 | }; 982 | } 983 | 984 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 985 | return 1; 986 | } 987 | 988 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 989 | return ring.connect(user_data, op.fd, op.addr, op.addrlen); 990 | } 991 | }; 992 | 993 | pub const Recv = struct { 994 | fd: os.fd_t, 995 | buffer: []u8, 996 | flags: u32, 997 | 998 | const Error = std.os.RecvFromError || DefaultError; 999 | 1000 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1001 | return 1; 1002 | } 1003 | 1004 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1005 | return ring.recv(user_data, op.fd, .{ .buffer = op.buffer }, op.flags); 1006 | } 1007 | 1008 | pub fn convertError(linux_err: os.E) ?Error { 1009 | // Copied from std.os.recvfrom. 1010 | return switch (linux_err) { 1011 | .BADF => unreachable, // always a race condition 1012 | .FAULT => unreachable, 1013 | .INVAL => unreachable, 1014 | .NOTCONN => unreachable, 1015 | .NOTSOCK => unreachable, 1016 | .AGAIN => error.WouldBlock, 1017 | .NOMEM => error.SystemResources, 1018 | .CONNREFUSED => error.ConnectionRefused, 1019 | .CONNRESET => error.ConnectionResetByPeer, 1020 | else => |err| defaultConvertError(err), 1021 | }; 1022 | } 1023 | }; 1024 | 1025 | pub const Fsync = struct { 1026 | fd: os.fd_t, 1027 | flags: u32, 1028 | 1029 | const Error = std.os.SyncError || DefaultError; 1030 | 1031 | pub fn convertError(linux_err: os.E) ?Error { 1032 | // Copied from std.os.fsync. 1033 | return switch (linux_err) { 1034 | .BADF, .INVAL, .ROFS => unreachable, 1035 | .IO => error.InputOutput, 1036 | .NOSPC => error.NoSpaceLeft, 1037 | .DQUOT => error.DiskQuota, 1038 | else => |err| defaultConvertError(err), 1039 | }; 1040 | } 1041 | 1042 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1043 | return 1; 1044 | } 1045 | 1046 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1047 | return ring.fsync(user_data, self.fd, self.flags); 1048 | } 1049 | }; 1050 | 1051 | pub const Fallocate = struct { 1052 | fd: os.fd_t, 1053 | mode: i32, 1054 | offset: u64, 1055 | len: u64, 1056 | 1057 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1058 | return 1; 1059 | } 1060 | 1061 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1062 | return ring.fallocate(user_data, self.fd, self.mode, self.offset, self.len); 1063 | } 1064 | 1065 | const Error = DefaultError; 1066 | 1067 | // TODO: fallocate can only return '1' as an error code according to the 1068 | // manpages. Right now this will lead to "UnexpectedError" which is not 1069 | // really correct. 1070 | pub fn convertError(linux_err: os.E) ?Error { 1071 | return defaultConvertError(linux_err); 1072 | } 1073 | }; 1074 | 1075 | pub const Statx = struct { 1076 | fd: os.fd_t, 1077 | path: [:0]const u8, 1078 | flags: u32, 1079 | mask: u32, 1080 | buf: *linux.Statx, 1081 | 1082 | // Comments for these errors were copied from Ubuntu manpages on Ubuntu 20.04, Linux 1083 | // kernel version 5.13.0-25-generic. 1084 | const Error = error{ 1085 | /// Search permission is denied for one of the directories in the path 1086 | /// prefix of path. 1087 | AccessDenied, 1088 | /// Too many symbolic links encountered while traversing the path. 1089 | SymLinkLoop, 1090 | /// path is too long. 1091 | NameTooLong, 1092 | /// A component of path does not exist, or path is an empty string and 1093 | /// AT_EMPTY_PATH was not specified in flags. 1094 | FileNotFound, 1095 | /// Out of memory (i.e., kernel memory). 1096 | SystemResources, 1097 | /// A component of the path prefix of path is not a directory or path 1098 | /// is relative and fd is a file descriptor referring to a file other 1099 | /// than a directory. 1100 | NotDir, 1101 | } || DefaultError; 1102 | 1103 | pub fn convertError(linux_err: os.E) ?Error { 1104 | // Copied from std.os.preadv. 1105 | return switch (linux_err) { 1106 | .ACCES => error.AccessDenied, 1107 | // fd is not a valid open file descriptor. 1108 | .BADF => unreachable, 1109 | // path or buf is NULL or points to a location outside the 1110 | // process's accessible address space. 1111 | .FAULT => unreachable, 1112 | // Invalid flag specified in flags or reserved flag specified 1113 | // in mask. 1114 | .INVAL => unreachable, 1115 | .LOOP => error.SymLinkLoop, 1116 | .NAMETOOLONG => error.NameTooLong, 1117 | .NOENT => error.FileNotFound, 1118 | .NOMEM => error.SystemResources, 1119 | .NOTDIR => error.NotDir, 1120 | else => |err| defaultConvertError(err), 1121 | }; 1122 | } 1123 | 1124 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1125 | return 1; 1126 | } 1127 | 1128 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1129 | return ring.statx(user_data, self.fd, self.path, self.flags, self.mask, self.buf); 1130 | } 1131 | }; 1132 | 1133 | pub const Shutdown = struct { 1134 | sockfd: os.socket_t, 1135 | how: u32, 1136 | 1137 | const Error = std.os.ShutdownError || DefaultError; 1138 | 1139 | pub fn convertError(linux_err: os.E) ?Error { 1140 | // Copied from std.os.shutdown. 1141 | return switch (linux_err) { 1142 | .BADF => unreachable, 1143 | .INVAL => unreachable, 1144 | .NOTCONN => error.SocketNotConnected, 1145 | .NOTSOCK => unreachable, 1146 | .NOBUFS => error.SystemResources, 1147 | else => |err| defaultConvertError(err), 1148 | }; 1149 | } 1150 | 1151 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1152 | return 1; 1153 | } 1154 | 1155 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1156 | return ring.shutdown(user_data, self.sockfd, self.how); 1157 | } 1158 | }; 1159 | 1160 | pub const RenameAt = struct { 1161 | old_dir_fd: os.fd_t, 1162 | old_path: [*:0]const u8, 1163 | new_dir_fd: os.fd_t, 1164 | new_path: [*:0]const u8, 1165 | flags: u32, 1166 | 1167 | const Error = std.os.RenameError || DefaultError; 1168 | 1169 | pub fn convertError(linux_err: os.E) ?Error { 1170 | // Copied from std.os.renameatZ. 1171 | return switch (linux_err) { 1172 | .ACCES => error.AccessDenied, 1173 | .PERM => error.AccessDenied, 1174 | .BUSY => error.FileBusy, 1175 | .DQUOT => error.DiskQuota, 1176 | .FAULT => unreachable, 1177 | .INVAL => unreachable, 1178 | .ISDIR => error.IsDir, 1179 | .LOOP => error.SymLinkLoop, 1180 | .MLINK => error.LinkQuotaExceeded, 1181 | .NAMETOOLONG => error.NameTooLong, 1182 | .NOENT => error.FileNotFound, 1183 | .NOTDIR => error.NotDir, 1184 | .NOMEM => error.SystemResources, 1185 | .NOSPC => error.NoSpaceLeft, 1186 | .EXIST => error.PathAlreadyExists, 1187 | .NOTEMPTY => error.PathAlreadyExists, 1188 | .ROFS => error.ReadOnlyFileSystem, 1189 | .XDEV => error.RenameAcrossMountPoints, 1190 | else => |err| defaultConvertError(err), 1191 | }; 1192 | } 1193 | 1194 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1195 | return 1; 1196 | } 1197 | 1198 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1199 | return ring.renameat( 1200 | user_data, 1201 | self.old_dir_fd, 1202 | self.old_path, 1203 | self.new_dir_fd, 1204 | self.new_path, 1205 | self.flags, 1206 | ); 1207 | } 1208 | }; 1209 | 1210 | pub const UnlinkAt = struct { 1211 | dir_fd: os.fd_t, 1212 | path: [*:0]const u8, 1213 | flags: u32, 1214 | 1215 | const Error = std.os.UnlinkError || DefaultError; 1216 | 1217 | pub fn convertError(linux_err: os.E) ?Error { 1218 | // Copied from std.os.unlinkZ. 1219 | return switch (linux_err) { 1220 | .ACCES => error.AccessDenied, 1221 | .PERM => error.AccessDenied, 1222 | .BUSY => error.FileBusy, 1223 | .FAULT => unreachable, 1224 | .INVAL => unreachable, 1225 | .IO => error.FileSystem, 1226 | .ISDIR => error.IsDir, 1227 | .LOOP => error.SymLinkLoop, 1228 | .NAMETOOLONG => error.NameTooLong, 1229 | .NOENT => error.FileNotFound, 1230 | .NOTDIR => error.NotDir, 1231 | .NOMEM => error.SystemResources, 1232 | .ROFS => error.ReadOnlyFileSystem, 1233 | else => |err| defaultConvertError(err), 1234 | }; 1235 | } 1236 | 1237 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1238 | return 1; 1239 | } 1240 | 1241 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1242 | return ring.unlinkat(user_data, self.dir_fd, self.path, self.flags); 1243 | } 1244 | }; 1245 | 1246 | pub const MkdirAt = struct { 1247 | dir_fd: os.fd_t, 1248 | path: [*:0]const u8, 1249 | mode: os.mode_t, 1250 | 1251 | const Error = std.os.MakeDirError || DefaultError; 1252 | 1253 | pub fn convertError(linux_err: os.E) ?Error { 1254 | // Copied from std.os.mkdiratZ. 1255 | return switch (linux_err) { 1256 | .ACCES => error.AccessDenied, 1257 | .BADF => unreachable, 1258 | .PERM => error.AccessDenied, 1259 | .DQUOT => error.DiskQuota, 1260 | .EXIST => error.PathAlreadyExists, 1261 | .FAULT => unreachable, 1262 | .LOOP => error.SymLinkLoop, 1263 | .MLINK => error.LinkQuotaExceeded, 1264 | .NAMETOOLONG => error.NameTooLong, 1265 | .NOENT => error.FileNotFound, 1266 | .NOMEM => error.SystemResources, 1267 | .NOSPC => error.NoSpaceLeft, 1268 | .NOTDIR => error.NotDir, 1269 | .ROFS => error.ReadOnlyFileSystem, 1270 | else => |err| defaultConvertError(err), 1271 | }; 1272 | } 1273 | 1274 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1275 | return 1; 1276 | } 1277 | 1278 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1279 | return ring.mkdirat(user_data, self.dir_fd, self.path, self.mode); 1280 | } 1281 | }; 1282 | 1283 | pub const SymlinkAt = struct { 1284 | target: [*:0]const u8, 1285 | new_dir_fd: os.fd_t, 1286 | link_path: [*:0]const u8, 1287 | 1288 | const Error = std.os.SymLinkError || DefaultError; 1289 | 1290 | pub fn convertError(linux_err: os.E) ?Error { 1291 | // Copied from std.os.symlinkatZ. 1292 | return switch (linux_err) { 1293 | .FAULT => unreachable, 1294 | .INVAL => unreachable, 1295 | .ACCES => return error.AccessDenied, 1296 | .PERM => return error.AccessDenied, 1297 | .DQUOT => return error.DiskQuota, 1298 | .EXIST => return error.PathAlreadyExists, 1299 | .IO => return error.FileSystem, 1300 | .LOOP => return error.SymLinkLoop, 1301 | .NAMETOOLONG => return error.NameTooLong, 1302 | .NOENT => return error.FileNotFound, 1303 | .NOTDIR => return error.NotDir, 1304 | .NOMEM => return error.SystemResources, 1305 | .NOSPC => return error.NoSpaceLeft, 1306 | .ROFS => return error.ReadOnlyFileSystem, 1307 | else => |err| defaultConvertError(err), 1308 | }; 1309 | } 1310 | 1311 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1312 | return 1; 1313 | } 1314 | 1315 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1316 | return ring.symlinkat(user_data, self.target, self.new_dir_fd, self.link_path); 1317 | } 1318 | }; 1319 | 1320 | pub const LinkAt = struct { 1321 | old_dir_fd: os.fd_t, 1322 | old_path: [*:0]const u8, 1323 | new_dir_fd: os.fd_t, 1324 | new_path: [*:0]const u8, 1325 | flags: u32, 1326 | 1327 | const Error = std.os.LinkatError || DefaultError; 1328 | 1329 | pub fn convertError(linux_err: os.E) ?Error { 1330 | // Copied from std.os.linkatZ. 1331 | return switch (linux_err) { 1332 | .ACCES => error.AccessDenied, 1333 | .DQUOT => error.DiskQuota, 1334 | .EXIST => error.PathAlreadyExists, 1335 | .FAULT => unreachable, 1336 | .IO => error.FileSystem, 1337 | .LOOP => error.SymLinkLoop, 1338 | .MLINK => error.LinkQuotaExceeded, 1339 | .NAMETOOLONG => error.NameTooLong, 1340 | .NOENT => error.FileNotFound, 1341 | .NOMEM => error.SystemResources, 1342 | .NOSPC => error.NoSpaceLeft, 1343 | .NOTDIR => error.NotDir, 1344 | .PERM => error.AccessDenied, 1345 | .ROFS => error.ReadOnlyFileSystem, 1346 | .XDEV => error.NotSameFileSystem, 1347 | .INVAL => unreachable, 1348 | else => |err| defaultConvertError(err), 1349 | }; 1350 | } 1351 | 1352 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1353 | return 1; 1354 | } 1355 | 1356 | pub fn enqueueSubmissionQueueEntries(self: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1357 | return ring.linkat( 1358 | user_data, 1359 | self.old_dir_fd, 1360 | self.old_path, 1361 | self.new_dir_fd, 1362 | self.new_path, 1363 | self.flags, 1364 | ); 1365 | } 1366 | }; 1367 | 1368 | pub const Send = struct { 1369 | fd: os.fd_t, 1370 | buffer: []const u8, 1371 | flags: u32, 1372 | 1373 | const Error = std.os.SendError || DefaultError; 1374 | 1375 | pub fn convertError(linux_err: os.E) ?Error { 1376 | // Copied from std.os.sendto + std.os.send. 1377 | // TODO: Double-check some of these unreachables with send man pages. 1378 | return switch (linux_err) { 1379 | .ACCES => error.AccessDenied, 1380 | .AGAIN => error.WouldBlock, 1381 | .ALREADY => error.FastOpenAlreadyInProgress, 1382 | .BADF => unreachable, // always a race condition 1383 | .CONNRESET => error.ConnectionResetByPeer, 1384 | .DESTADDRREQ => unreachable, // The socket is not connection-mode, and no peer address is set. 1385 | .FAULT => unreachable, // An invalid user space address was specified for an argument. 1386 | .INVAL => unreachable, // Invalid argument passed. 1387 | .ISCONN => unreachable, // connection-mode socket was connected already but a recipient was specified 1388 | .MSGSIZE => error.MessageTooBig, 1389 | .NOBUFS => error.SystemResources, 1390 | .NOMEM => error.SystemResources, 1391 | .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket. 1392 | .OPNOTSUPP => unreachable, // Some bit in the flags argument is inappropriate for the socket type. 1393 | .PIPE => error.BrokenPipe, 1394 | .AFNOSUPPORT => unreachable, 1395 | .LOOP => unreachable, 1396 | .NAMETOOLONG => unreachable, 1397 | .NOENT => unreachable, 1398 | .NOTDIR => unreachable, 1399 | .HOSTUNREACH => unreachable, 1400 | .NETUNREACH => unreachable, 1401 | .NOTCONN => unreachable, 1402 | .NETDOWN => error.NetworkSubsystemFailed, 1403 | else => |err| defaultConvertError(err), 1404 | }; 1405 | } 1406 | 1407 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1408 | return 1; 1409 | } 1410 | 1411 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1412 | return ring.send(user_data, op.fd, op.buffer, op.flags); 1413 | } 1414 | }; 1415 | 1416 | pub const OpenAt = struct { 1417 | fd: os.fd_t, 1418 | path: [*:0]const u8, 1419 | flags: u32, 1420 | mode: os.mode_t, 1421 | 1422 | const Error = std.os.OpenError || DefaultError; 1423 | 1424 | pub fn convertError(linux_err: os.E) ?Error { 1425 | // Copied from std.os.openatZ. 1426 | return switch (linux_err) { 1427 | .FAULT => unreachable, 1428 | .INVAL => unreachable, 1429 | .BADF => unreachable, 1430 | .ACCES => error.AccessDenied, 1431 | .FBIG => error.FileTooBig, 1432 | .OVERFLOW => error.FileTooBig, 1433 | .ISDIR => error.IsDir, 1434 | .LOOP => error.SymLinkLoop, 1435 | .MFILE => error.ProcessFdQuotaExceeded, 1436 | .NAMETOOLONG => error.NameTooLong, 1437 | .NFILE => error.SystemFdQuotaExceeded, 1438 | .NODEV => error.NoDevice, 1439 | .NOENT => error.FileNotFound, 1440 | .NOMEM => error.SystemResources, 1441 | .NOSPC => error.NoSpaceLeft, 1442 | .NOTDIR => error.NotDir, 1443 | .PERM => error.AccessDenied, 1444 | .EXIST => error.PathAlreadyExists, 1445 | .BUSY => error.DeviceBusy, 1446 | .OPNOTSUPP => error.FileLocksNotSupported, 1447 | .AGAIN => error.WouldBlock, 1448 | .TXTBSY => error.FileBusy, 1449 | else => |err| defaultConvertError(err), 1450 | }; 1451 | } 1452 | 1453 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1454 | return 1; 1455 | } 1456 | 1457 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1458 | return ring.openat(user_data, op.fd, op.path, op.flags, op.mode); 1459 | } 1460 | }; 1461 | 1462 | pub const Close = struct { 1463 | fd: os.fd_t, 1464 | 1465 | const Error = DefaultError; 1466 | 1467 | // TODO: The stdlib says that INTR on close is actually an indicator of 1468 | // success - so we may need a way to convert that to success here. For now, 1469 | // the caller can ignore error.Cancelled. 1470 | pub fn convertError(linux_err: os.E) ?Error { 1471 | // Copied from std.os.close. 1472 | return switch (linux_err) { 1473 | .BADF => unreachable, // Always a race condition. 1474 | else => |err| defaultConvertError(err), 1475 | }; 1476 | } 1477 | 1478 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1479 | return 1; 1480 | } 1481 | 1482 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1483 | return ring.close(user_data, op.fd); 1484 | } 1485 | }; 1486 | 1487 | pub const Cancel = struct { 1488 | cancel_user_data: u64, 1489 | flags: u32, 1490 | 1491 | pub fn convertError(linux_err: os.E) ?anyerror { 1492 | return switch (linux_err) { 1493 | .ALREADY => error.OperationAlreadyInProgress, 1494 | .NOENT => error.OperationNotFound, 1495 | else => |err| return defaultConvertError(err), 1496 | }; 1497 | } 1498 | 1499 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1500 | return 1; 1501 | } 1502 | 1503 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1504 | return ring.cancel(user_data, op.cancel_user_data, op.flags); 1505 | } 1506 | }; 1507 | 1508 | // TODO: Rename after adding scope for operations. 1509 | pub const TimeOut = struct { 1510 | ts: *const os.linux.kernel_timespec, 1511 | count: u32, 1512 | flags: u32, 1513 | 1514 | pub fn convertError(linux_err: os.E) ?anyerror { 1515 | return switch (linux_err) { 1516 | .TIME => @as(?anyerror, null), 1517 | else => |err| return defaultConvertError(err), 1518 | }; 1519 | } 1520 | 1521 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1522 | return 1; 1523 | } 1524 | 1525 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1526 | return ring.timeout(user_data, op.ts, op.count, op.flags); 1527 | } 1528 | }; 1529 | 1530 | pub const TimeoutRemove = struct { 1531 | timeout_user_data: u64, 1532 | flags: u32, 1533 | 1534 | pub fn convertError(linux_err: os.E) ?anyerror { 1535 | return switch (linux_err) { 1536 | .BUSY => error.OperationAlreadyInProgress, 1537 | .NOENT => error.OperationNotFound, 1538 | else => |err| return defaultConvertError(err), 1539 | }; 1540 | } 1541 | 1542 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1543 | return 1; 1544 | } 1545 | 1546 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1547 | return ring.timeout_remove(user_data, op.timeout_user_data, op.flags); 1548 | } 1549 | }; 1550 | 1551 | pub const PollAdd = struct { 1552 | fd: os.fd_t, 1553 | poll_mask: u32, 1554 | 1555 | const Error = std.os.PollError || DefaultError; 1556 | 1557 | pub fn convertError(linux_err: os.E) ?Error { 1558 | return switch (linux_err) { 1559 | // Copied from std.os.poll. 1560 | .FAULT => unreachable, 1561 | .INVAL => unreachable, 1562 | .NOMEM => error.SystemResources, 1563 | else => |err| return defaultConvertError(err), 1564 | }; 1565 | } 1566 | 1567 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1568 | return 1; 1569 | } 1570 | 1571 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1572 | return ring.poll_add(user_data, op.fd, op.poll_mask); 1573 | } 1574 | }; 1575 | 1576 | pub const PollRemove = struct { 1577 | poll_id: u64, 1578 | 1579 | const Error = DefaultError; 1580 | 1581 | pub fn convertError(linux_err: os.E) ?DefaultError { 1582 | return switch (linux_err) { 1583 | else => |err| return defaultConvertError(err), 1584 | }; 1585 | } 1586 | 1587 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1588 | return 1; 1589 | } 1590 | 1591 | pub fn enqueueSubmissionQueueEntries(op: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1592 | return ring.poll_update(user_data, op.poll_id); 1593 | } 1594 | }; 1595 | 1596 | pub const Nop = struct { 1597 | const Error = DefaultError; 1598 | 1599 | pub fn convertError(linux_err: os.E) ?Error { 1600 | return defaultConvertError(linux_err); 1601 | } 1602 | 1603 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1604 | return 1; 1605 | } 1606 | 1607 | pub fn enqueueSubmissionQueueEntries(_: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1608 | return ring.nop(user_data); 1609 | } 1610 | }; 1611 | 1612 | pub const EpollCtl = struct { 1613 | epfd: os.fd_t, 1614 | fd: os.fd_t, 1615 | op: u32, 1616 | ev: ?*linux.epoll_event, 1617 | 1618 | const Error = std.os.EpollCtlError || DefaultError; 1619 | 1620 | pub fn convertError(linux_err: os.E) ?Error { 1621 | // Copied from std.os.epoll_ctl. 1622 | return switch (linux_err) { 1623 | .BADF => unreachable, // always a race condition if this happens 1624 | .EXIST => error.FileDescriptorAlreadyPresentInSet, 1625 | .INVAL => unreachable, 1626 | .LOOP => error.OperationCausesCircularLoop, 1627 | .NOENT => error.FileDescriptorNotRegistered, 1628 | .NOMEM => error.SystemResources, 1629 | .NOSPC => error.UserResourceLimitReached, 1630 | .PERM => error.FileDescriptorIncompatibleWithEpoll, 1631 | else => |err| defaultConvertError(err), 1632 | }; 1633 | } 1634 | 1635 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1636 | return 1; 1637 | } 1638 | 1639 | pub fn enqueueSubmissionQueueEntries(this: @This(), ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1640 | return ring.epoll_ctl(user_data, this.epfd, this.fd, this.op, this.ev); 1641 | } 1642 | }; 1643 | 1644 | fn testWrite(ring: *AsyncIOUring) !void { 1645 | const path = "test_io_uring_write_read"; 1646 | const file = try std.fs.cwd().createFile(path, .{ .read = true, .truncate = true }); 1647 | defer file.close(); 1648 | defer std.fs.cwd().deleteFile(path) catch {}; 1649 | const fd = file.handle; 1650 | 1651 | const write_buffer = [_]u8{9} ** 20; 1652 | const cqe_write = try ring.write(fd, write_buffer[0..], 0, null, null); 1653 | try std.testing.expectEqual(cqe_write.res, write_buffer.len); 1654 | 1655 | var read_buffer = [_]u8{0} ** 20; 1656 | // Do an ordinary blocking read to check that the bytes were written. 1657 | const num_bytes_read = try file.readAll(read_buffer[0..]); 1658 | try std.testing.expectEqualSlices(u8, read_buffer[0..num_bytes_read], write_buffer[0..]); 1659 | } 1660 | 1661 | test "write" { 1662 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1663 | 1664 | var ring = IO_Uring.init(1, 0) catch |err| switch (err) { 1665 | error.SystemOutdated => return error.SkipZigTest, 1666 | error.PermissionDenied => return error.SkipZigTest, 1667 | else => return err, 1668 | }; 1669 | defer ring.deinit(); 1670 | var async_ring = AsyncIOUring{ .ring = &ring }; 1671 | 1672 | var write_frame = async testWrite(&async_ring); 1673 | 1674 | try async_ring.run_event_loop(); 1675 | 1676 | try nosuspend await write_frame; 1677 | } 1678 | 1679 | test "write handles full submission queue" { 1680 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1681 | 1682 | var ring = IO_Uring.init(4, 0) catch |err| switch (err) { 1683 | error.SystemOutdated => return error.SkipZigTest, 1684 | error.PermissionDenied => return error.SkipZigTest, 1685 | else => return err, 1686 | }; 1687 | defer ring.deinit(); 1688 | var async_ring = AsyncIOUring{ .ring = &ring }; 1689 | 1690 | // Random number to identify the no-ops. 1691 | const nop_user_data = 9; 1692 | var num_submitted: u32 = 0; 1693 | // Fill up the submission queue. 1694 | while (true) { 1695 | var sqe = ring.nop(nop_user_data) catch |err| { 1696 | switch (err) { 1697 | error.SubmissionQueueFull => { 1698 | break; 1699 | }, 1700 | else => { 1701 | return err; 1702 | }, 1703 | } 1704 | }; 1705 | num_submitted += 1; 1706 | sqe.user_data = 9; 1707 | } 1708 | 1709 | try std.testing.expect(num_submitted > 0); 1710 | 1711 | // Try to do a write - we expect this to submit the existing submission 1712 | // queue entries to the kernel and then retry adding the write to the 1713 | // submission queue and succeed. 1714 | var write_frame = async testWrite(&async_ring); 1715 | 1716 | // A bit hacky - make sure the previous no-ops were submitted, but not the 1717 | // write itself. 1718 | var cqes: [256]linux.io_uring_cqe = undefined; 1719 | const num_ready_cqes = try ring.copy_cqes(cqes[0..], num_submitted); 1720 | async_ring.num_outstanding_events -= num_ready_cqes; 1721 | 1722 | try std.testing.expectEqual(num_ready_cqes, num_submitted); 1723 | for (cqes[0..num_ready_cqes]) |cqe| { 1724 | try std.testing.expectEqual(cqe.user_data, nop_user_data); 1725 | } 1726 | 1727 | // Make sure the write itself hasn't been submitted. 1728 | try std.testing.expectEqual(ring.sq_ready(), 1); 1729 | // Inspect the last submission queue entry to check that it's actually a 1730 | // write. There's no way to get this from IO_Uring without directly 1731 | // inspecting its SubmissionQueue, AFAICT, so we do that for now. 1732 | var sqe = &ring.sq.sqes[(ring.sq.sqe_tail - 1) & ring.sq.mask]; 1733 | try std.testing.expectEqual(sqe.opcode, .WRITE); 1734 | 1735 | // This should submit the write and wait for it to occur. 1736 | try async_ring.run_event_loop(); 1737 | 1738 | try nosuspend await write_frame; 1739 | } 1740 | 1741 | fn testRead(ring: *AsyncIOUring) !void { 1742 | const path = "test_io_uring_write_read"; 1743 | const file = try std.fs.cwd().createFile(path, .{ .read = true, .truncate = true }); 1744 | defer file.close(); 1745 | defer std.fs.cwd().deleteFile(path) catch {}; 1746 | const fd = file.handle; 1747 | 1748 | const write_buffer = [_]u8{9} ** 20; 1749 | const cqe_write = try ring.write(fd, write_buffer[0..], 0, null, null); 1750 | try std.testing.expectEqual(cqe_write.res, write_buffer.len); 1751 | 1752 | var read_buffer = [_]u8{0} ** 20; 1753 | 1754 | const read_cqe = try ring.read(fd, read_buffer[0..], 0, null, null); 1755 | const num_bytes_read = @intCast(usize, read_cqe.res); 1756 | try std.testing.expectEqualSlices(u8, read_buffer[0..num_bytes_read], write_buffer[0..]); 1757 | } 1758 | 1759 | fn testReadThatTimesOut(ring: *AsyncIOUring) !void { 1760 | var read_buffer = [_]u8{0} ** 20; 1761 | 1762 | const ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 10000 }; 1763 | // Try to read from stdin - there won't be any input so this should 1764 | // reliably time out. 1765 | const read_cqe = ring.do( 1766 | Read{ .fd = std.io.getStdIn().handle, .buffer = read_buffer[0..], .offset = 0 }, 1767 | Timeout{ .ts = &ts, .flags = 0 }, 1768 | null, 1769 | ); 1770 | try std.testing.expectEqual(read_cqe, error.Cancelled); 1771 | } 1772 | 1773 | test "read" { 1774 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1775 | 1776 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 1777 | error.SystemOutdated => return error.SkipZigTest, 1778 | error.PermissionDenied => return error.SkipZigTest, 1779 | else => return err, 1780 | }; 1781 | defer ring.deinit(); 1782 | var async_ring = AsyncIOUring{ .ring = &ring }; 1783 | 1784 | var read_frame = async testRead(&async_ring); 1785 | 1786 | try async_ring.run_event_loop(); 1787 | 1788 | try nosuspend await read_frame; 1789 | } 1790 | 1791 | fn testReadWithManualAPI(ring: *AsyncIOUring) !void { 1792 | var read_buffer = [_]u8{0} ** 20; 1793 | 1794 | const ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 10000 }; 1795 | // Try to read from stdin - there won't be any input so this should 1796 | // reliably time out. 1797 | const read_cqe = ring.do(Read{ 1798 | .fd = std.io.getStdIn().handle, 1799 | .buffer = read_buffer[0..], 1800 | .offset = 0, 1801 | }, .{ .ts = &ts, .flags = 0 }, null); 1802 | 1803 | try std.testing.expectEqual(read_cqe, error.Cancelled); 1804 | } 1805 | 1806 | fn testReadWithManualAPIAndOverridenEnqueueSqes(ring: *AsyncIOUring) !void { 1807 | var read_buffer = [_]u8{0} ** 20; 1808 | 1809 | var ran_custom_submit: bool = false; 1810 | 1811 | // Make a special op based on read. 1812 | const my_read: struct { 1813 | read: Read, 1814 | value_to_set: *bool, 1815 | 1816 | const convertError = Read.convertError; 1817 | 1818 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1819 | return 1; 1820 | } 1821 | 1822 | pub fn enqueueSubmissionQueueEntries(self: @This(), my_ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1823 | self.value_to_set.* = true; 1824 | return try my_ring.read(user_data, self.read.fd, .{ .buffer = self.read.buffer }, self.read.offset); 1825 | } 1826 | } = .{ 1827 | .read = .{ 1828 | .fd = std.io.getStdIn().handle, 1829 | .buffer = read_buffer[0..], 1830 | .offset = 0, 1831 | }, 1832 | .value_to_set = &ran_custom_submit, 1833 | }; 1834 | 1835 | const ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 10000 }; 1836 | // Try to read from stdin - there won't be any input so this should 1837 | // reliably time out. 1838 | const read_cqe = ring.do(my_read, Timeout{ .ts = &ts, .flags = 0 }, null); 1839 | 1840 | try std.testing.expectEqual(read_cqe, error.Cancelled); 1841 | try std.testing.expectEqual(ran_custom_submit, true); 1842 | } 1843 | 1844 | fn testOverridingNumberOfSQEs(ring: *AsyncIOUring) !void { 1845 | var ran_custom_submit: bool = false; 1846 | 1847 | // Make a special op based on read. 1848 | const double_nop: struct { 1849 | value_to_set: *bool, 1850 | 1851 | const convertError = Read.convertError; 1852 | 1853 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 1854 | return 2; 1855 | } 1856 | 1857 | pub fn enqueueSubmissionQueueEntries(self: @This(), my_ring: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 1858 | self.value_to_set.* = true; 1859 | // TODO: Using this in practice will probably be a bit tricky since 1860 | // the timeout only applies to whatever this function returns, not 1861 | // to the first op. This interface maybe seems more generic than it 1862 | // actually is, which could be a problem. 1863 | _ = try my_ring.nop(0); 1864 | return try my_ring.nop(user_data); 1865 | } 1866 | } = .{ 1867 | .value_to_set = &ran_custom_submit, 1868 | }; 1869 | 1870 | const nop_cqe = try ring.do(double_nop, null, null); 1871 | 1872 | try std.testing.expectEqual(nop_cqe.res, 0); 1873 | try std.testing.expectEqual(ran_custom_submit, true); 1874 | } 1875 | 1876 | test "overriding number of sqes in custom op submits pending entries when queue would be full" { 1877 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1878 | 1879 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 1880 | error.SystemOutdated => return error.SkipZigTest, 1881 | error.PermissionDenied => return error.SkipZigTest, 1882 | else => return err, 1883 | }; 1884 | defer ring.deinit(); 1885 | var async_ring = AsyncIOUring{ .ring = &ring }; 1886 | 1887 | // After this there will only be 1 slot left in the submission queue - if 1888 | // getNumRequiredSubmissionQueueEntries is not implemented/used correctly, 1889 | // this will cause a SubmissionQueueFull error when we try to submit our 1890 | // custom op. 1891 | _ = try ring.nop(0); 1892 | 1893 | var read_frame = async testOverridingNumberOfSQEs(&async_ring); 1894 | 1895 | try async_ring.run_event_loop(); 1896 | 1897 | try nosuspend await read_frame; 1898 | } 1899 | 1900 | test "read with manual API" { 1901 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1902 | 1903 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 1904 | error.SystemOutdated => return error.SkipZigTest, 1905 | error.PermissionDenied => return error.SkipZigTest, 1906 | else => return err, 1907 | }; 1908 | defer ring.deinit(); 1909 | var async_ring = AsyncIOUring{ .ring = &ring }; 1910 | 1911 | var read_frame = async testReadWithManualAPI(&async_ring); 1912 | 1913 | try async_ring.run_event_loop(); 1914 | 1915 | try nosuspend await read_frame; 1916 | } 1917 | 1918 | test "read with manual API and overriden submit" { 1919 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1920 | 1921 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 1922 | error.SystemOutdated => return error.SkipZigTest, 1923 | error.PermissionDenied => return error.SkipZigTest, 1924 | else => return err, 1925 | }; 1926 | defer ring.deinit(); 1927 | var async_ring = AsyncIOUring{ .ring = &ring }; 1928 | 1929 | var read_frame = async testReadWithManualAPIAndOverridenEnqueueSqes(&async_ring); 1930 | 1931 | try async_ring.run_event_loop(); 1932 | 1933 | try nosuspend await read_frame; 1934 | } 1935 | 1936 | test "read with timeout returns cancelled" { 1937 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1938 | 1939 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 1940 | error.SystemOutdated => return error.SkipZigTest, 1941 | error.PermissionDenied => return error.SkipZigTest, 1942 | else => return err, 1943 | }; 1944 | defer ring.deinit(); 1945 | var async_ring = AsyncIOUring{ .ring = &ring }; 1946 | 1947 | var read_frame = async testReadThatTimesOut(&async_ring); 1948 | 1949 | try async_ring.run_event_loop(); 1950 | 1951 | try nosuspend await read_frame; 1952 | } 1953 | 1954 | test "read with timeout returns cancelled with only 1 submission queue entry free" { 1955 | if (builtin.os.tag != .linux) return error.SkipZigTest; 1956 | 1957 | var ring = IO_Uring.init(4, 0) catch |err| switch (err) { 1958 | error.SystemOutdated => return error.SkipZigTest, 1959 | error.PermissionDenied => return error.SkipZigTest, 1960 | else => return err, 1961 | }; 1962 | defer ring.deinit(); 1963 | var async_ring = AsyncIOUring{ .ring = &ring }; 1964 | 1965 | _ = try ring.nop(0); 1966 | _ = try ring.nop(0); 1967 | _ = try ring.nop(0); 1968 | 1969 | // At this point there will only be one submission queue entry free. This 1970 | // should submit the outstanding entries and wait for two slots to be free 1971 | // before submitting the read and its linked timeout. 1972 | var read_frame = async testReadThatTimesOut(&async_ring); 1973 | 1974 | try async_ring.run_event_loop(); 1975 | 1976 | try nosuspend await read_frame; 1977 | } 1978 | 1979 | fn testReadThatIsCancelled(ring: *AsyncIOUring) !void { 1980 | var read_buffer = [_]u8{0} ** 20; 1981 | 1982 | var op_id: u64 = undefined; 1983 | 1984 | // Try to read from stdin - there won't be any input so this operation should 1985 | // reliably hang until cancellation. 1986 | var read_frame = async ring.do( 1987 | Read{ .fd = std.io.getStdIn().handle, .buffer = read_buffer[0..], .offset = 0 }, 1988 | null, 1989 | &op_id, 1990 | ); 1991 | 1992 | const cancel_cqe = try ring.cancel(op_id, 0, null, null); 1993 | // Expect that cancellation succeeded. 1994 | try std.testing.expectEqual(cancel_cqe.res, 0); 1995 | 1996 | const read_cqe = await read_frame; 1997 | try std.testing.expectEqual(read_cqe, error.Cancelled); 1998 | } 1999 | 2000 | test "read that is cancelled returns cancelled" { 2001 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2002 | 2003 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2004 | error.SystemOutdated => return error.SkipZigTest, 2005 | error.PermissionDenied => return error.SkipZigTest, 2006 | else => return err, 2007 | }; 2008 | defer ring.deinit(); 2009 | var async_ring = AsyncIOUring{ .ring = &ring }; 2010 | 2011 | var read_frame = async testReadThatIsCancelled(&async_ring); 2012 | 2013 | try async_ring.run_event_loop(); 2014 | 2015 | try nosuspend await read_frame; 2016 | } 2017 | 2018 | fn testCancellingNonExistentOperation(ring: *AsyncIOUring) !void { 2019 | const op_id: u64 = 32; 2020 | _ = ring.cancel(op_id, 0, null, null) catch |err| { 2021 | try std.testing.expectEqual(err, error.OperationNotFound); 2022 | return; 2023 | }; 2024 | // Cancellation should not succeed so we should never reach this line. 2025 | unreachable; 2026 | } 2027 | 2028 | test "cancelling an operation that doesn't exist returns error.OperationNotFound" { 2029 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2030 | 2031 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2032 | error.SystemOutdated => return error.SkipZigTest, 2033 | error.PermissionDenied => return error.SkipZigTest, 2034 | else => return err, 2035 | }; 2036 | defer ring.deinit(); 2037 | var async_ring = AsyncIOUring{ .ring = &ring }; 2038 | 2039 | var cancel_frame = async testCancellingNonExistentOperation(&async_ring); 2040 | 2041 | try async_ring.run_event_loop(); 2042 | 2043 | try nosuspend await cancel_frame; 2044 | } 2045 | 2046 | pub fn testShortTimeout(ring: *AsyncIOUring) !void { 2047 | const ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 10000 }; 2048 | const cqe = try ring.timeout(&ts, 0, 0, null); 2049 | // If there are no errors, this test passes. 2050 | // Also check that the CQE error result is as expected according to the 2051 | // IO_Uring docs. 2052 | try std.testing.expectEqual(cqe.res, -@intCast(i32, @enumToInt(os.E.TIME))); 2053 | } 2054 | 2055 | test "timeout for short timeout returns success" { 2056 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2057 | 2058 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2059 | error.SystemOutdated => return error.SkipZigTest, 2060 | error.PermissionDenied => return error.SkipZigTest, 2061 | else => return err, 2062 | }; 2063 | defer ring.deinit(); 2064 | var async_ring = AsyncIOUring{ .ring = &ring }; 2065 | 2066 | var cancel_frame = async testShortTimeout(&async_ring); 2067 | 2068 | try async_ring.run_event_loop(); 2069 | 2070 | try nosuspend await cancel_frame; 2071 | } 2072 | 2073 | pub fn testLongTimeoutCancelled(ring: *AsyncIOUring) !void { 2074 | const ts = os.linux.kernel_timespec{ .tv_sec = 100000, .tv_nsec = 0 }; 2075 | var op_id: u64 = undefined; 2076 | var timeout_frame = async ring.timeout(&ts, 0, 0, &op_id); 2077 | 2078 | _ = try ring.cancel(op_id, 0, null, null); 2079 | const timeout_cqe_or_error = await timeout_frame; 2080 | 2081 | try std.testing.expectEqual(timeout_cqe_or_error, error.Cancelled); 2082 | } 2083 | 2084 | test "timeout with long timeout returns error.Cancelled when cancelled" { 2085 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2086 | 2087 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2088 | error.SystemOutdated => return error.SkipZigTest, 2089 | error.PermissionDenied => return error.SkipZigTest, 2090 | else => return err, 2091 | }; 2092 | defer ring.deinit(); 2093 | var async_ring = AsyncIOUring{ .ring = &ring }; 2094 | 2095 | var cancel_frame = async testLongTimeoutCancelled(&async_ring); 2096 | 2097 | try async_ring.run_event_loop(); 2098 | 2099 | try nosuspend await cancel_frame; 2100 | } 2101 | 2102 | pub fn testLongTimeoutRemovedWithTimeoutRemove(ring: *AsyncIOUring) !void { 2103 | const ts = os.linux.kernel_timespec{ .tv_sec = 100000, .tv_nsec = 0 }; 2104 | var op_id: u64 = undefined; 2105 | var timeout_frame = async ring.timeout(&ts, 0, 0, &op_id); 2106 | 2107 | _ = try ring.timeout_remove(op_id, 0, null, null); 2108 | const timeout_cqe_or_error = await timeout_frame; 2109 | 2110 | try std.testing.expectEqual(timeout_cqe_or_error, error.Cancelled); 2111 | } 2112 | 2113 | test "timeout with long timeout returns error.Cancelled when removed with timeout_remove" { 2114 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2115 | 2116 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2117 | error.SystemOutdated => return error.SkipZigTest, 2118 | error.PermissionDenied => return error.SkipZigTest, 2119 | else => return err, 2120 | }; 2121 | defer ring.deinit(); 2122 | var async_ring = AsyncIOUring{ .ring = &ring }; 2123 | 2124 | var cancel_frame = async testLongTimeoutRemovedWithTimeoutRemove(&async_ring); 2125 | 2126 | try async_ring.run_event_loop(); 2127 | 2128 | try nosuspend await cancel_frame; 2129 | } 2130 | 2131 | pub fn testTimeoutRemoveCanUpdateTimeout(ring: *AsyncIOUring) !void { 2132 | const ts = os.linux.kernel_timespec{ .tv_sec = 100000, .tv_nsec = 0 }; 2133 | var op_id: u64 = undefined; 2134 | // Make a long timeout. 2135 | var timeout_frame = async ring.timeout(&ts, 0, 0, &op_id); 2136 | 2137 | const UpdateTimeout = struct { 2138 | timeout_user_data: u64, 2139 | updated_ts: *const os.linux.kernel_timespec, 2140 | 2141 | const convertError = TimeoutRemove.convertError; 2142 | 2143 | pub fn getNumRequiredSubmissionQueueEntries(_: @This()) u32 { 2144 | return 1; 2145 | } 2146 | 2147 | pub fn enqueueSubmissionQueueEntries(op: @This(), r: *IO_Uring, user_data: u64) !*linux.io_uring_sqe { 2148 | // TODO: Create issue to add this to IO_Uring and then add it. 2149 | const IORING_TIMEOUT_UPDATE = 1 << 1; 2150 | var timeout_remove_op = TimeoutRemove{ 2151 | .timeout_user_data = op.timeout_user_data, 2152 | .flags = IORING_TIMEOUT_UPDATE, 2153 | }; 2154 | 2155 | var sqe = try timeout_remove_op.enqueueSubmissionQueueEntries(r, user_data); 2156 | // `off` is the `addr2` field, which is required to store a pointer 2157 | // to the timespec for the new timeout. 2158 | // 2159 | // See docs under IORING_TIMEOUT_REMOVE for details. 2160 | // 2161 | // https://man.archlinux.org/man/io_uring_enter.2.en 2162 | sqe.off = @ptrToInt(op.updated_ts); 2163 | return sqe; 2164 | } 2165 | }; 2166 | 2167 | const short_ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 10000 }; 2168 | // Update to have a shorter timeout. 2169 | const update_cqe = try ring.do( 2170 | UpdateTimeout{ .timeout_user_data = op_id, .updated_ts = &short_ts }, 2171 | null, 2172 | null, 2173 | ); 2174 | 2175 | try std.testing.expectEqual(update_cqe.res, 0); 2176 | 2177 | // Wait for original timeout operation to complete. If update succeeded, 2178 | // this should happen quickly - otherwise it will take a very long time. 2179 | const timeout_cqe = try await timeout_frame; 2180 | 2181 | // If we made it here then it means the timeout expired as expected - the 2182 | // following check is kind of superfluous but nice to make sure things are 2183 | // working as expected. 2184 | try std.testing.expectEqual(timeout_cqe.res, -@intCast(i32, @enumToInt(os.E.TIME))); 2185 | } 2186 | 2187 | test "timeout_remove can update timeout" { 2188 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2189 | 2190 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2191 | error.SystemOutdated => return error.SkipZigTest, 2192 | error.PermissionDenied => return error.SkipZigTest, 2193 | else => return err, 2194 | }; 2195 | defer ring.deinit(); 2196 | var async_ring = AsyncIOUring{ .ring = &ring }; 2197 | 2198 | var cancel_frame = async testTimeoutRemoveCanUpdateTimeout(&async_ring); 2199 | 2200 | try async_ring.run_event_loop(); 2201 | 2202 | try nosuspend await cancel_frame; 2203 | } 2204 | 2205 | pub fn testTimeoutRemoveForExpiredTimeout(ring: *AsyncIOUring) !void { 2206 | const ts = os.linux.kernel_timespec{ .tv_sec = 0, .tv_nsec = 1 }; 2207 | var op_id: u64 = undefined; 2208 | var timeout_frame = async ring.timeout(&ts, 0, 0, &op_id); 2209 | 2210 | // Wait for timeout to expire. This is theoretically racy but should wait 2211 | // long enough that it's not a problem. 2212 | std.os.nanosleep(0, 1000000); 2213 | 2214 | // This is a bit janky but it's needed to process the timeout. The 2215 | // alternative would be to write the test where we block until the timeout 2216 | // completes before continuing but that would be both slightly unrealistic 2217 | // and technically not supported since you're not supposed to use op_id 2218 | // once you've resumed the frame for that op, since after that the same id 2219 | // could technically be reused (though it's unlikely). 2220 | var cqes: [4096]linux.io_uring_cqe = undefined; 2221 | _ = try ring.process_outstanding_events(cqes[0..]); 2222 | 2223 | const tr_cqe_or_error = ring.timeout_remove(op_id, 0, null, null); 2224 | try std.testing.expectEqual(tr_cqe_or_error, error.OperationNotFound); 2225 | const timeout_cqe = try await timeout_frame; 2226 | 2227 | try std.testing.expectEqual(timeout_cqe.res, -@intCast(i32, @enumToInt(os.E.TIME))); 2228 | } 2229 | 2230 | test "timeout_remove returns OperationNotFound if timeout has already expired" { 2231 | if (builtin.os.tag != .linux) return error.SkipZigTest; 2232 | 2233 | var ring = IO_Uring.init(2, 0) catch |err| switch (err) { 2234 | error.SystemOutdated => return error.SkipZigTest, 2235 | error.PermissionDenied => return error.SkipZigTest, 2236 | else => return err, 2237 | }; 2238 | defer ring.deinit(); 2239 | var async_ring = AsyncIOUring{ .ring = &ring }; 2240 | 2241 | var cancel_frame = async testTimeoutRemoveForExpiredTimeout(&async_ring); 2242 | 2243 | try async_ring.run_event_loop(); 2244 | 2245 | try nosuspend await cancel_frame; 2246 | } 2247 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | id: uxgrieq4k5c84z136q2dnfak76d3172m7z1v887wyqapuik1 2 | name: async_io_uring 3 | main: src/async_io_uring.zig 4 | license: MIT 5 | description: An event loop using io_uring and coroutines 6 | dependencies: 7 | --------------------------------------------------------------------------------