From ab11b74b391d650cc8aa2530157a68d559b7ee7c Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 8 Dec 2025 11:32:06 +0100 Subject: [PATCH 01/19] [IO_URING] Add support for `pollAdd` operation To observe events on file descriptors IO_URING supports the `pollAdd` operation. This is useful when you want to observe a file descriptor becoming ready to read or write This PR adds a new `IORing.Request.PollEvents` option set to model the poll masks. Furthermore, it adds a new `static func pollAdd` to the `IORing.Request`. We can now use IO_URING to poll for events on file descriptors. --- Sources/CSystem/include/io_uring.h | 2 + Sources/System/IORing/IORequest.swift | 80 ++++++++++++++++++++++++++ Sources/System/IORing/PollEvents.swift | 59 +++++++++++++++++++ Tests/SystemTests/IORingTests.swift | 60 ++++++++++++++++++- 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 Sources/System/IORing/PollEvents.swift diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index defc2848..b4223154 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -135,6 +135,8 @@ typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe; #define IORING_FEAT_RW_ATTR (1U << 16) #define IORING_FEAT_NO_IOWAIT (1U << 17) +#define IORING_POLL_ADD_MULTI (1U << 0) + #if !defined(_ASM_GENERIC_INT_LL64_H) && !defined(_ASM_GENERIC_INT_L64_H) && !defined(_UAPI_ASM_GENERIC_INT_LL64_H) && !defined(_UAPI_ASM_GENERIC_INT_L64_H) typedef uint8_t __u8; typedef uint16_t __u16; diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 4a388c49..100ce7bb 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -23,6 +23,12 @@ internal enum IORequestCore { intoSlot: IORing.RegisteredFile, context: UInt64 = 0 ) + case pollAdd( + file: FileDescriptor, + pollEvents: IORing.Request.PollEvents, + isMultiShot: Bool = true, + context: UInt64 = 0 + ) case read( file: FileDescriptor, buffer: IORing.RegisteredBuffer, @@ -187,6 +193,72 @@ extension IORing.Request { .init(core: .nop) } + /// Adds a poll operation to monitor a file descriptor for specific I/O events. + /// + /// This method creates an io_uring poll operation that monitors the specified file descriptor + /// for I/O readiness events. The operation completes when any of the requested events become + /// active on the file descriptor, such as data becoming available for reading or the descriptor + /// becoming ready for writing. + /// + /// Poll operations are useful for implementing efficient I/O multiplexing, allowing you to + /// monitor multiple file descriptors concurrently within a single io_uring instance. When used + /// with multishot mode, a single poll operation can deliver multiple completion events without + /// needing to be resubmitted. + /// + /// ## Multishot Behavior + /// + /// When `isMultiShot` is `true`, the poll operation automatically rearms after each completion + /// event, continuing to monitor the file descriptor for subsequent events. This reduces + /// submission overhead for long-lived monitoring operations. The operation continues until + /// explicitly cancelled or the file descriptor is closed. + /// + /// When `isMultiShot` is `false`, the poll operation completes once after the first matching + /// event occurs, requiring resubmission to continue monitoring. + /// + /// ## Example Usage + /// + /// ```swift + /// // Monitor a socket for incoming connections + /// let pollRequest = IORing.Request.pollAdd( + /// listenSocket, + /// pollEvents: .pollin, + /// isMultiShot: true, + /// context: 1 + /// ) + /// try ring.submit(pollRequest) + /// + /// // Process completions + /// for completion in try ring.completions() { + /// if completion.context == 1 { + /// // Handle incoming connection + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - file: The file descriptor to monitor for I/O events. + /// - pollEvents: The I/O events to monitor on the file descriptor. + /// - isMultiShot: If `true`, the poll operation automatically rearms after each event, + /// continuing to monitor the file descriptor. If `false`, the operation completes after + /// the first matching event. Defaults to `false`. + /// - context: An application-specific value passed through to the completion event, + /// allowing you to identify which operation completed. Defaults to `0`. + /// + /// - Returns: An I/O ring request that monitors the file descriptor for the specified events. + /// + /// ## See Also + /// + /// - ``PollEvents``: The events that can be monitored. + /// - ``IORing/Request/cancel(_:matching:)``: Cancelling poll operations. + @inlinable public static func pollAdd( + _ file: FileDescriptor, + pollEvents: PollEvents, + isMultiShot: Bool = false, + context: UInt64 = 0 + ) -> IORing.Request { + .init(core: .pollAdd(file: file, pollEvents: pollEvents, context: context)) + } + @inlinable public static func read( _ file: IORing.RegisteredFile, into buffer: IORing.RegisteredBuffer, @@ -477,6 +549,14 @@ extension IORing.Request { case .cancel(let flags): request.operation = .asyncCancel request.cancel_flags = flags + case .pollAdd(let file, let pollEvents, let isMultiShot, let context): + request.operation = .pollAdd + request.fileDescriptor = file + request.rawValue.user_data = context + if isMultiShot { + request.rawValue.len = IORING_POLL_ADD_MULTI + } + request.rawValue.poll32_events = pollEvents.rawValue } return request diff --git a/Sources/System/IORing/PollEvents.swift b/Sources/System/IORing/PollEvents.swift new file mode 100644 index 00000000..ea8b8295 --- /dev/null +++ b/Sources/System/IORing/PollEvents.swift @@ -0,0 +1,59 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if compiler(>=6.2) && $Lifetimes +#if os(Linux) +extension IORing.Request { + /// A set of I/O events that can be monitored on a file descriptor. + /// + /// `PollEvents` represents the event mask used with io_uring poll operations to specify + /// which I/O conditions to monitor on a file descriptor. These events correspond to the + /// standard Posix poll events defined in the kernel's `poll.h` header. + /// + /// Use `PollEvents` with ``IORing/Request/pollAdd(_:pollEvents:isMultiShot:context:)`` + /// to register interest in specific I/O events. The poll operation completes when any of + /// the specified events become active on the file descriptor. + /// + /// ## Usage + /// + /// ```swift + /// // Monitor a socket for incoming data + /// let request = IORing.Request.pollAdd( + /// socketFD, + /// pollEvents: .pollin, + /// isMultiShot: true + /// ) + /// ``` + public struct PollEvents: OptionSet, Hashable, Codable { + public var rawValue: UInt32 + + @inlinable + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + /// An event indicating data is available for reading. + /// + /// This event becomes active when data arrives on the file descriptor and can be read + /// without blocking. For sockets, this includes when a new connection is available on + /// a listening socket. Corresponds to the Posix `POLLIN` event flag. + @inlinable + public static var pollIn: PollEvents { PollEvents(rawValue: 0x0001) } + + /// An event indicating the file descriptor is ready for writing. + /// + /// This event becomes active when writing to the file descriptor will not block. For + /// sockets, this indicates that send buffer space is available. Corresponds to the + /// Posix `POLLOUT` event flag. + @inlinable + public static var pollOut: PollEvents { PollEvents(rawValue: 0x0004) } + } +} +#endif +#endif diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 97bf86c7..012a70d2 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -177,7 +177,7 @@ final class IORingTests: XCTestCase { let bytesRead = try nonRingFD.read(into: rawBuffer) XCTAssert(bytesRead == 13) let result2 = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) - XCTAssertEqual(result2, "Hello, World!") + XCTAssertEqual(result2, "Hello, World!") try cleanUpHelloWorldFile(parent) efdBuf.deallocate() rawBuffer.deallocate() @@ -310,6 +310,64 @@ final class IORingTests: XCTestCase { XCTAssertEqual(error, Errno(rawValue: EBADFD)) } } + + func testPollAddPollIn() throws { + guard uringEnabled else { try XCTSkip("System does not support IOring") } + var ring = try IORing(queueDepth: 32, flags: []) + + // Test POLLIN: Create an eventfd to monitor for read readiness + let testEventFD = FileDescriptor(rawValue: eventfd(0, 0)) + defer { + // Clean up + try! testEventFD.close() + } + let pollInContext: UInt64 = 42 + + // Submit a pollAdd request to monitor for POLLIN events (data available for reading) + let enqueued = try ring.submit(linkedRequests: + .pollAdd(testEventFD, pollEvents: .pollin, isMultiShot: false, context: pollInContext)) + XCTAssert(enqueued) + + // Write to the eventfd to trigger the POLLIN event + var value: UInt64 = 1 + withUnsafeBytes(of: &value) { bufferPtr in + _ = try? testEventFD.write(bufferPtr) + } + + // Consume the completion from the poll operation + let completion = try ring.blockingConsumeCompletion() + XCTAssertEqual(completion.context, pollInContext) + XCTAssertGreaterThan(completion.result, 0) // Poll should return mask of ready events + } + + func testPollAddPollOut() throws { + guard uringEnabled else { try XCTSkip("System does not support IOring") } + var ring = try IORing(queueDepth: 32, flags: []) + + // Test POLLOUT: Create a pipe to monitor for write readiness + var pipeFDs: [Int32] = [0, 0] + let pipeResult = pipe(&pipeFDs) + XCTAssertEqual(pipeResult, 0) + let writeFD = FileDescriptor(rawValue: pipeFDs[1]) + let readFD = FileDescriptor(rawValue: pipeFDs[0]) + defer { + // Clean up + try! writeFD.close() + try! readFD.close() + } + let pollOutContext: UInt64 = 43 + + // Submit a pollAdd request to monitor for POLLOUT events (ready for writing) + // Pipes are typically ready for writing when empty + let enqueuedOut = try ring.submit(linkedRequests: + .pollAdd(writeFD, pollEvents: .pollout, isMultiShot: false, context: pollOutContext)) + XCTAssert(enqueuedOut) + + // Consume the completion from the poll operation + let completionOut = try ring.blockingConsumeCompletion() + XCTAssertEqual(completionOut.context, pollOutContext) + XCTAssertGreaterThan(completionOut.result, 0) // Poll should return mask of ready events + } } #endif // os(Linux) #endif // compiler(>=6.2) && $Lifetimes From e4b352c650b714042aaf2a0007e5683716445cff Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 13 Jan 2026 13:02:23 -0800 Subject: [PATCH 02/19] [test] fix spelling error --- Tests/SystemTests/IORingTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 012a70d2..8bbd3d29 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -325,7 +325,7 @@ final class IORingTests: XCTestCase { // Submit a pollAdd request to monitor for POLLIN events (data available for reading) let enqueued = try ring.submit(linkedRequests: - .pollAdd(testEventFD, pollEvents: .pollin, isMultiShot: false, context: pollInContext)) + .pollAdd(testEventFD, pollEvents: .pollIn, isMultiShot: false, context: pollInContext)) XCTAssert(enqueued) // Write to the eventfd to trigger the POLLIN event @@ -360,7 +360,7 @@ final class IORingTests: XCTestCase { // Submit a pollAdd request to monitor for POLLOUT events (ready for writing) // Pipes are typically ready for writing when empty let enqueuedOut = try ring.submit(linkedRequests: - .pollAdd(writeFD, pollEvents: .pollout, isMultiShot: false, context: pollOutContext)) + .pollAdd(writeFD, pollEvents: .pollOut, isMultiShot: false, context: pollOutContext)) XCTAssert(enqueuedOut) // Consume the completion from the poll operation From e775d1bf59d24370a2879034fa5b64e3d68bbdec Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 14 Jan 2026 16:06:27 +0100 Subject: [PATCH 03/19] Apply suggestion from @MarSe32m Co-authored-by: Martin --- Sources/System/IORing/IORequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 100ce7bb..6a995919 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -256,7 +256,7 @@ extension IORing.Request { isMultiShot: Bool = false, context: UInt64 = 0 ) -> IORing.Request { - .init(core: .pollAdd(file: file, pollEvents: pollEvents, context: context)) + .init(core: .pollAdd(file: file, pollEvents: pollEvents, isMultiShot: isMultiShot, context: context)) } @inlinable public static func read( From 2a069e401bb43761c914c85a8ceb4bd3f08354b1 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 14 Jan 2026 23:03:46 +0100 Subject: [PATCH 04/19] Update Sources/System/IORing/IORequest.swift --- Sources/System/IORing/IORequest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 6a995919..6aedac7c 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -196,8 +196,8 @@ extension IORing.Request { /// Adds a poll operation to monitor a file descriptor for specific I/O events. /// /// This method creates an io_uring poll operation that monitors the specified file descriptor - /// for I/O readiness events. The operation completes when any of the requested events become - /// active on the file descriptor, such as data becoming available for reading or the descriptor + /// for I/O readiness events. The operation completes when any of the requested events + /// occur on the file descriptor, such as data becoming available for reading or the descriptor /// becoming ready for writing. /// /// Poll operations are useful for implementing efficient I/O multiplexing, allowing you to From dd5313284d3e030aede35cc040f9e27b442103ab Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 14:55:39 -0700 Subject: [PATCH 05/19] [cmake] add new file to configuration --- Sources/System/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/System/CMakeLists.txt b/Sources/System/CMakeLists.txt index a904eb1a..d41e7aca 100644 --- a/Sources/System/CMakeLists.txt +++ b/Sources/System/CMakeLists.txt @@ -49,6 +49,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL Linux) IORing/IORequest.swift IORing/IORing.swift IORing/IORing+Util.swift + IORing/PollEvents.swift IORing/RawIORequest.swift) endif() target_sources(SystemPackage PRIVATE From 27b4a4a03a4f5c4a922023c6ffeaf3b21da6e0be Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 15:10:58 -0700 Subject: [PATCH 06/19] Use XCTSkipIf to guard IORing tests --- Tests/SystemTests/IORingTests.swift | 34 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 8bbd3d29..c4ad1dad 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -25,6 +25,8 @@ let uringEnabled: Bool = { } }() +let failureMessage = "Runtime environment does not support IORing." + func isUringEnabled() throws -> Bool { // Even if the kernel supports io_uring, the SystemPackage build may have // been compiled against older kernel headers that lack features it needs @@ -83,12 +85,12 @@ final class IORingTests: XCTestCase { } func testInit() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) _ = try IORing(queueDepth: 32, flags: []) } func testNop() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) _ = try ring.submit(linkedRequests: .nop()) let completion = try ring.blockingConsumeCompletion() @@ -124,7 +126,7 @@ final class IORingTests: XCTestCase { } func testUndersizedSubmissionQueue() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) var ring: IORing = try IORing(queueDepth: 1) let enqueued = ring.prepare(linkedRequests: .nop(), .nop()) XCTAssertFalse(enqueued) @@ -132,7 +134,7 @@ final class IORingTests: XCTestCase { // Exercises opening, reading, closing, registered files, registered buffers, and eventfd func testOpenReadAndWriteFixedFile() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) let (parent, path) = try makeHelloWorldFile() let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) var ring = try setupTestRing(depth: 6, fileSlots: 1, buffers: [rawBuffer]) @@ -189,7 +191,7 @@ final class IORingTests: XCTestCase { // dangling pointer. Here we deliberately let the FilePath go out of scope // between prepare and submit, then churn the heap to make UAFs observable. func testPathBufferLifetimeAcrossPrepareSubmit() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) let (parent, _) = try makeHelloWorldFile() var ring = try IORing(queueDepth: 6) @@ -223,7 +225,7 @@ final class IORingTests: XCTestCase { } func testPathBufferLifetimeAcrossLinkedRequests() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) let (parent, _) = try makeHelloWorldFile() let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) var ring = try setupTestRing(depth: 6, fileSlots: 1, buffers: [rawBuffer]) @@ -260,12 +262,12 @@ final class IORingTests: XCTestCase { // Timeout test for `blockingConsumeCompletion(timeout:)`: func testBlockingConsumeCompletionWithTimeoutOnIdleRing() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) let ring = try IORing(queueDepth: 4, flags: []) - guard ring.supportedFeatures.contains(.extendedArguments) else { - // Kernel < 5.11: timeouts in io_uring_enter aren't supported. - return - } + try XCTSkipIf( + !ring.supportedFeatures.contains(.extendedArguments), + "Kernel < 5.11: timeouts in io_uring_enter aren't supported." + ) let clock = ContinuousClock() let start = clock.now @@ -285,7 +287,7 @@ final class IORingTests: XCTestCase { } func testRegisterEventFDTwiceThrows() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 4) let efd = FileDescriptor(rawValue: eventfd(0, Int32(EFD_SEMAPHORE))) defer { try? efd.close() } @@ -300,10 +302,10 @@ final class IORingTests: XCTestCase { } func testSubmitOnDisabledRingThrows() throws { - guard uringEnabled else { return } + try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 4, flags: [.startDisabled]) - do throws(Errno) { + do throws(Errno) { _ = try ring.submit(linkedRequests: .nop()) XCTFail("expected submit on a disabled ring to throw") } catch { @@ -312,7 +314,7 @@ final class IORingTests: XCTestCase { } func testPollAddPollIn() throws { - guard uringEnabled else { try XCTSkip("System does not support IOring") } + try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) // Test POLLIN: Create an eventfd to monitor for read readiness @@ -341,7 +343,7 @@ final class IORingTests: XCTestCase { } func testPollAddPollOut() throws { - guard uringEnabled else { try XCTSkip("System does not support IOring") } + try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) // Test POLLOUT: Create a pipe to monitor for write readiness From 6baba54adb8884d626e2df7c65316c35102f5ba9 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 17:38:31 -0700 Subject: [PATCH 07/19] Reflow or improve some doc-comments --- Sources/System/IORing/IORequest.swift | 126 ++++++++++++++++--------- Sources/System/IORing/PollEvents.swift | 29 +++--- 2 files changed, 100 insertions(+), 55 deletions(-) diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 6aedac7c..ac0c2e10 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -194,41 +194,55 @@ extension IORing.Request { } /// Adds a poll operation to monitor a file descriptor for specific I/O events. + /// Adds a poll operation to monitor a file descriptor for specific I/O + /// events. /// - /// This method creates an io_uring poll operation that monitors the specified file descriptor - /// for I/O readiness events. The operation completes when any of the requested events - /// occur on the file descriptor, such as data becoming available for reading or the descriptor - /// becoming ready for writing. + /// This method creates an io_uring poll operation that monitors the + /// specified file descriptor for I/O readiness events. The operation + /// completes when any of the requested events occur on the file + /// descriptor, such as data becoming available for reading or the + /// descriptor becoming ready for writing. /// - /// Poll operations are useful for implementing efficient I/O multiplexing, allowing you to - /// monitor multiple file descriptors concurrently within a single io_uring instance. When used - /// with multishot mode, a single poll operation can deliver multiple completion events without - /// needing to be resubmitted. + /// Poll operations are useful for implementing efficient I/O + /// multiplexing, allowing you to monitor multiple file descriptors + /// concurrently within a single io_uring instance. When used with + /// multishot mode, a single poll operation can deliver multiple + /// completion events without needing to be resubmitted. /// /// ## Multishot Behavior /// - /// When `isMultiShot` is `true`, the poll operation automatically rearms after each completion - /// event, continuing to monitor the file descriptor for subsequent events. This reduces - /// submission overhead for long-lived monitoring operations. The operation continues until - /// explicitly cancelled or the file descriptor is closed. + /// When `isMultiShot` is `true`, the poll operation automatically rearms + /// after each completion event, continuing to monitor the file descriptor + /// for subsequent events. This reduces submission overhead for long-lived + /// monitoring operations. The operation continues until explicitly + /// cancelled or the file descriptor is closed. /// - /// When `isMultiShot` is `false`, the poll operation completes once after the first matching - /// event occurs, requiring resubmission to continue monitoring. + /// When `isMultiShot` is `false`, the poll operation completes once after + /// the first matching event occurs, requiring resubmission to continue + /// monitoring. /// /// ## Example Usage /// /// ```swift /// // Monitor a socket for incoming connections + /// var ring = try IORing(queueDepth: 32) /// let pollRequest = IORing.Request.pollAdd( /// listenSocket, - /// pollEvents: .pollin, + /// pollEvents: .pollIn, /// isMultiShot: true, /// context: 1 /// ) - /// try ring.submit(pollRequest) + /// guard try ring.submit(linkedRequests: pollRequest) else { + /// // The submission queue was full; retry or drain completions first. + /// return + /// } /// - /// // Process completions - /// for completion in try ring.completions() { + /// // Process completions. A multishot poll stays armed while its + /// // completions contain `.moreCompletions`. + /// var armed = true + /// while armed { + /// let completion = try ring.blockingConsumeCompletion() + /// armed = completion.flags.contains(.moreCompletions) /// if completion.context == 1 { /// // Handle incoming connection /// } @@ -238,13 +252,16 @@ extension IORing.Request { /// - Parameters: /// - file: The file descriptor to monitor for I/O events. /// - pollEvents: The I/O events to monitor on the file descriptor. - /// - isMultiShot: If `true`, the poll operation automatically rearms after each event, - /// continuing to monitor the file descriptor. If `false`, the operation completes after - /// the first matching event. Defaults to `false`. - /// - context: An application-specific value passed through to the completion event, - /// allowing you to identify which operation completed. Defaults to `0`. + /// - isMultiShot: If `true`, the poll operation automatically rearms + /// after each event, continuing to monitor the file descriptor. If + /// `false`, the operation completes after the first matching event. + /// Defaults to `false`. + /// - context: An application-specific value passed through to the + /// completion event, allowing you to identify which operation + /// completed. Defaults to `0`. /// - /// - Returns: An I/O ring request that monitors the file descriptor for the specified events. + /// - Returns: An I/O ring request that monitors the file descriptor for + /// the specified events. /// /// ## See Also /// @@ -388,24 +405,49 @@ extension IORing.Request { // Cancel - /* - * ASYNC_CANCEL flags. - * - * IORING_ASYNC_CANCEL_ALL Cancel all requests that match the given key - * IORING_ASYNC_CANCEL_FD Key off 'fd' for cancellation rather than the - * request 'user_data' - * IORING_ASYNC_CANCEL_ANY Match any request - * IORING_ASYNC_CANCEL_FD_FIXED 'fd' passed in is a fixed descriptor - * IORING_ASYNC_CANCEL_USERDATA Match on user_data, default for no other key - * IORING_ASYNC_CANCEL_OP Match request based on opcode - */ - -@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_ALL: UInt32 { 1 << 0 } -@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_FD: UInt32 { 1 << 1 } -@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_ANY: UInt32 { 1 << 2 } -@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_FD_FIXED: UInt32 { 1 << 3 } -@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_USERDATA: UInt32 { 1 << 4 } -@inlinable internal static var SWIFT_IORING_ASYNC_CANCEL_OP: UInt32 { 1 << 5 } + /// Cancel every request matching the given key, rather than only the + /// first one found. + /// + /// Corresponds to `IORING_ASYNC_CANCEL_ALL`. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_ASYNC_CANCEL_ALL: UInt32 { 1 << 0 } + + /// Match requests on `sqe->fd`, rather than on their `user_data`. + /// + /// Corresponds to `IORING_ASYNC_CANCEL_FD`. Cannot be combined with + /// ``SWIFT_IORING_ASYNC_CANCEL_ANY``. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_ASYNC_CANCEL_FD: UInt32 { 1 << 1 } + + /// Match any request, disregarding every other key. + /// + /// Corresponds to `IORING_ASYNC_CANCEL_ANY`. Cannot be combined with + /// ``SWIFT_IORING_ASYNC_CANCEL_FD`` or ``SWIFT_IORING_ASYNC_CANCEL_OP``. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_ASYNC_CANCEL_ANY: UInt32 { 1 << 2 } + + /// The descriptor to match against is a registered file, so `sqe->fd` + /// carries a slot index rather than a file descriptor. + /// + /// Corresponds to `IORING_ASYNC_CANCEL_FD_FIXED`, and accompanies + /// ``SWIFT_IORING_ASYNC_CANCEL_FD``. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_ASYNC_CANCEL_FD_FIXED: UInt32 { 1 << 3 } + + /// Match requests on their `user_data`. This is the default when no other + /// key is given. + /// + /// Corresponds to `IORING_ASYNC_CANCEL_USERDATA`. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_ASYNC_CANCEL_USERDATA: UInt32 { 1 << 4 } + + /// Match requests by operation. Note that the opcode to match is read + /// from `sqe->len`. + /// + /// Corresponds to `IORING_ASYNC_CANCEL_OP`. Cannot be combined with + /// ``SWIFT_IORING_ASYNC_CANCEL_ANY``. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_ASYNC_CANCEL_OP: UInt32 { 1 << 5 } public enum CancellationMatch { case all diff --git a/Sources/System/IORing/PollEvents.swift b/Sources/System/IORing/PollEvents.swift index ea8b8295..e24f8165 100644 --- a/Sources/System/IORing/PollEvents.swift +++ b/Sources/System/IORing/PollEvents.swift @@ -12,13 +12,15 @@ extension IORing.Request { /// A set of I/O events that can be monitored on a file descriptor. /// - /// `PollEvents` represents the event mask used with io_uring poll operations to specify - /// which I/O conditions to monitor on a file descriptor. These events correspond to the - /// standard Posix poll events defined in the kernel's `poll.h` header. + /// `PollEvents` represents the event mask used with io_uring poll + /// operations to specify which I/O conditions to monitor on a file + /// descriptor. These events correspond to the standard Posix poll events + /// defined in the kernel's `poll.h` header. /// - /// Use `PollEvents` with ``IORing/Request/pollAdd(_:pollEvents:isMultiShot:context:)`` - /// to register interest in specific I/O events. The poll operation completes when any of - /// the specified events become active on the file descriptor. + /// Use `PollEvents` with + /// ``IORing/Request/pollAdd(_:pollEvents:isMultiShot:context:)`` to + /// register interest in specific I/O events. The poll operation completes + /// when any of the specified events become active on the file descriptor. /// /// ## Usage /// @@ -26,7 +28,7 @@ extension IORing.Request { /// // Monitor a socket for incoming data /// let request = IORing.Request.pollAdd( /// socketFD, - /// pollEvents: .pollin, + /// pollEvents: .pollIn, /// isMultiShot: true /// ) /// ``` @@ -40,17 +42,18 @@ extension IORing.Request { /// An event indicating data is available for reading. /// - /// This event becomes active when data arrives on the file descriptor and can be read - /// without blocking. For sockets, this includes when a new connection is available on - /// a listening socket. Corresponds to the Posix `POLLIN` event flag. + /// This event becomes active when data arrives on the file descriptor + /// and can be read without blocking. For sockets, this includes when + /// a new connection is available on a listening socket. Corresponds + /// to the Posix `POLLIN` event flag. @inlinable public static var pollIn: PollEvents { PollEvents(rawValue: 0x0001) } /// An event indicating the file descriptor is ready for writing. /// - /// This event becomes active when writing to the file descriptor will not block. For - /// sockets, this indicates that send buffer space is available. Corresponds to the - /// Posix `POLLOUT` event flag. + /// This event becomes active when writing to the file descriptor will + /// not block. For sockets, this indicates that send buffer space is + /// available. Corresponds to the Posix `POLLOUT` event flag. @inlinable public static var pollOut: PollEvents { PollEvents(rawValue: 0x0004) } } From 5b7e24a6b1b02f627de3a24480eeb59e1275de7d Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 17:39:14 -0700 Subject: [PATCH 08/19] Define a constant in Swift rather than in C --- Sources/CSystem/include/io_uring.h | 2 -- Sources/System/IORing/IORequest.swift | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index b4223154..defc2848 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -135,8 +135,6 @@ typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe; #define IORING_FEAT_RW_ATTR (1U << 16) #define IORING_FEAT_NO_IOWAIT (1U << 17) -#define IORING_POLL_ADD_MULTI (1U << 0) - #if !defined(_ASM_GENERIC_INT_LL64_H) && !defined(_ASM_GENERIC_INT_L64_H) && !defined(_UAPI_ASM_GENERIC_INT_LL64_H) && !defined(_UAPI_ASM_GENERIC_INT_L64_H) typedef uint8_t __u8; typedef uint16_t __u16; diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index ac0c2e10..72ddcaca 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -193,7 +193,18 @@ extension IORing.Request { .init(core: .nop) } - /// Adds a poll operation to monitor a file descriptor for specific I/O events. + // Poll + + /// Multishot poll: the poll handler continues to report CQEs on behalf + /// of the same SQE, each flagged with + /// ``IORing/Completion/Flags/moreCompletions``. + /// + /// Corresponds to `IORING_POLL_ADD_MULTI`. Note that since + /// `sqe->poll_events` is the event space, the command flags for + /// `POLL_ADD` are stored in `sqe->len`. + @_alwaysEmitIntoClient + internal static var SWIFT_IORING_POLL_ADD_MULTI: UInt32 { 1 << 0 } + /// Adds a poll operation to monitor a file descriptor for specific I/O /// events. /// @@ -596,7 +607,7 @@ extension IORing.Request { request.fileDescriptor = file request.rawValue.user_data = context if isMultiShot { - request.rawValue.len = IORING_POLL_ADD_MULTI + request.rawValue.len = Self.SWIFT_IORING_POLL_ADD_MULTI } request.rawValue.poll32_events = pollEvents.rawValue } From 75c85d47e7cff5bdc7c5f654a76459bccace5fcc Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 18:33:21 -0700 Subject: [PATCH 09/19] Account for big-endian architectures for poll event encoding --- Sources/System/IORing/IORequest.swift | 2 +- Sources/System/IORing/RawIORequest.swift | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 72ddcaca..dfe2dba9 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -609,7 +609,7 @@ extension IORing.Request { if isMultiShot { request.rawValue.len = Self.SWIFT_IORING_POLL_ADD_MULTI } - request.rawValue.poll32_events = pollEvents.rawValue + request.pollEvents = pollEvents } return request diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index 1de60136..507fc0db 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -75,6 +75,27 @@ extension RawIORequest { set { rawValue.addr = newValue } } + /// The poll event mask, stored in `sqe->poll32_events`. + /// + /// Big-endian kernels swap the halfwords of this field before reading it. + /// Equivalent to liburing's `__io_uring_prep_poll_mask`. + @_alwaysEmitIntoClient var pollEvents: IORing.Request.PollEvents { + get { .init(rawValue: _applyPollMask(rawValue.poll32_events)) } + set { rawValue.poll32_events = _applyPollMask(newValue.rawValue) } + } + + /// Converts a poll event mask between its in-memory and `sqe` encodings. + /// + /// This is a halfword rotate (`swahw32` from ``), which keeps + /// the byte order within each half. + @_alwaysEmitIntoClient func _applyPollMask(_ mask: UInt32) -> UInt32 { + #if _endian(big) + return (mask &<< 16) | (mask &>> 16) + #else + return mask + #endif + } + @inlinable public var flags: Flags { get { Flags(rawValue: rawValue.flags) } set { rawValue.flags = newValue.rawValue } From 2e81d9414205b30ecfc705e54e745630d68f7e9e Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 18:58:49 -0700 Subject: [PATCH 10/19] Add a test similar to the example in the pollAdd doc-comment --- Tests/SystemTests/IORingTests.swift | 113 ++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index c4ad1dad..8759e229 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -370,6 +370,119 @@ final class IORingTests: XCTestCase { XCTAssertEqual(completionOut.context, pollOutContext) XCTAssertGreaterThan(completionOut.result, 0) // Poll should return mask of ready events } + + // Similar to the multishot example in the documentation for + // `pollAdd(_:pollEvents:isMultiShot:context:)`: arm a multishot poll, + // then consume completions for as long as they carry `.moreCompletions`. + func testPollAddMultiShotRearmsAcrossEvents() throws { + try XCTSkipIf(!uringEnabled, failureMessage) + var ring = try IORing(queueDepth: 32, flags: []) + + // Every wait below has a timeout, so that a poll that fails to fire + // causes a test failure. + try XCTSkipIf( + !ring.supportedFeatures.contains(.extendedArguments), + "Kernel < 5.11: timeouts in io_uring_enter aren't supported." + ) + + var pipeFDs: [Int32] = [0, 0] + XCTAssertEqual(pipe(&pipeFDs), 0) + let readFD = FileDescriptor(rawValue: pipeFDs[0]) + let writeFD = FileDescriptor(rawValue: pipeFDs[1]) + defer { + try? readFD.close() + try? writeFD.close() + } + + func writeByte() throws { + var byte: UInt8 = 1 + try withUnsafeBytes(of: &byte) { + try XCTAssertEqual(writeFD.write($0), 1) + } + } + + func readByte() throws { + var scratch: UInt8 = 0 + try withUnsafeMutableBytes(of: &scratch) { + try XCTAssertEqual(readFD.read(into: $0), 1) + } + } + + let context: UInt64 = 44 + let pollIn = Int32(IORing.Request.PollEvents.pollIn.rawValue) + + let pollRequest = IORing.Request.pollAdd( + readFD, pollEvents: .pollIn, isMultiShot: true, context: context + ) + let enqueued = try ring.submit(linkedRequests: pollRequest) + XCTAssert(enqueued) + + try writeByte() + let first = try ring.blockingConsumeCompletion(timeout: .seconds(1)) + // Kernels before 5.13 reject IORING_POLL_ADD_MULTI + try XCTSkipIf( + first.result == -EINVAL, + "Kernel < 5.13: multishot poll is unsupported." + ) + XCTAssertEqual(first.context, context) + XCTAssertNotEqual( + first.result & pollIn, 0, "expected POLLIN in the result mask" + ) + XCTAssert(first.flags.contains(.moreCompletions)) + + // Drain the pipe, then any extra completions. + try readByte() + while ring.tryConsumeCompletion() != nil {} + + try writeByte() + // This written byte will only be reported by a re-armed poll. + let second = try ring.blockingConsumeCompletion(timeout: .seconds(1)) + XCTAssertEqual(second.context, context) + XCTAssertNotEqual( + second.result & pollIn, 0, "expected POLLIN in the result mask" + ) + + // Drain again + try readByte() + while ring.tryConsumeCompletion() != nil {} + + // Cancel to end the multishot poll. + try XCTAssert( + ring.submit(linkedRequests: .cancel(.all, matchingContext: context)) + ) + var terminal: (result: Int32, flags: IORing.Completion.Flags)? = nil + var observed: [(context: UInt64, result: Int32, flags: UInt32)] = [] + // The cancel posts a completion of its own under a different context, + // and the two may land a moment apart, so retry briefly rather than + // draining exactly once. + for _ in 0..<100 where terminal == nil { + while let completion = ring.tryConsumeCompletion() { + observed.append( + ( + completion.context, + completion.result, + completion.flags.rawValue + ) + ) + if completion.context == context { + terminal = (completion.result, completion.flags) + } + } + if terminal == nil { usleep(1000) } + } + XCTAssertNotNil( + terminal, + "no terminal completion for the cancelled poll; " + + "completions seen: \(observed)" + ) + if let terminal { + XCTAssertEqual(terminal.result, -ECANCELED) + XCTAssertFalse( + terminal.flags.contains(.moreCompletions), + "poll kept its armed state after being cancelled" + ) + } + } } #endif // os(Linux) #endif // compiler(>=6.2) && $Lifetimes From bed8e2886096518866e2cd75ffdd868836804294 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 19:28:02 -0700 Subject: [PATCH 11/19] Clean up permissively --- Tests/SystemTests/IORingTests.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 8759e229..8b97e7f6 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -320,8 +320,7 @@ final class IORingTests: XCTestCase { // Test POLLIN: Create an eventfd to monitor for read readiness let testEventFD = FileDescriptor(rawValue: eventfd(0, 0)) defer { - // Clean up - try! testEventFD.close() + try? testEventFD.close() } let pollInContext: UInt64 = 42 @@ -353,9 +352,8 @@ final class IORingTests: XCTestCase { let writeFD = FileDescriptor(rawValue: pipeFDs[1]) let readFD = FileDescriptor(rawValue: pipeFDs[0]) defer { - // Clean up - try! writeFD.close() - try! readFD.close() + try? writeFD.close() + try? readFD.close() } let pollOutContext: UInt64 = 43 From 0ecb080d5cea42fed8d4cf32fe5823b08d8d6266 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 19:28:43 -0700 Subject: [PATCH 12/19] Test ready events in a better way --- Tests/SystemTests/IORingTests.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 8b97e7f6..83e856c7 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -338,7 +338,10 @@ final class IORingTests: XCTestCase { // Consume the completion from the poll operation let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.context, pollInContext) - XCTAssertGreaterThan(completion.result, 0) // Poll should return mask of ready events + let pollIn = Int32(IORing.Request.PollEvents.pollIn.rawValue) + XCTAssertNotEqual( + completion.result & pollIn, 0, "expected POLLIN in the result mask" + ) } func testPollAddPollOut() throws { @@ -366,7 +369,11 @@ final class IORingTests: XCTestCase { // Consume the completion from the poll operation let completionOut = try ring.blockingConsumeCompletion() XCTAssertEqual(completionOut.context, pollOutContext) - XCTAssertGreaterThan(completionOut.result, 0) // Poll should return mask of ready events + let pollOut = Int32(IORing.Request.PollEvents.pollOut.rawValue) + XCTAssertNotEqual( + completionOut.result & pollOut, 0, + "expected POLLOUT in the result mask" + ) } // Similar to the multishot example in the documentation for From bd49600d622d577592a144724a593654d5a70392 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 27 Jul 2026 19:36:12 -0700 Subject: [PATCH 13/19] Prevent tests from hanging in the wrong conditions --- Tests/SystemTests/IORingTests.swift | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 83e856c7..d3639481 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -317,6 +317,12 @@ final class IORingTests: XCTestCase { try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) + // We use the timeout feature below. + try XCTSkipIf( + !ring.supportedFeatures.contains(.extendedArguments), + "Kernel < 5.11: timeouts in io_uring_enter aren't supported." + ) + // Test POLLIN: Create an eventfd to monitor for read readiness let testEventFD = FileDescriptor(rawValue: eventfd(0, 0)) defer { @@ -336,7 +342,9 @@ final class IORingTests: XCTestCase { } // Consume the completion from the poll operation - let completion = try ring.blockingConsumeCompletion() + let completion = try ring.blockingConsumeCompletion( + timeout: .seconds(1) + ) XCTAssertEqual(completion.context, pollInContext) let pollIn = Int32(IORing.Request.PollEvents.pollIn.rawValue) XCTAssertNotEqual( @@ -348,6 +356,12 @@ final class IORingTests: XCTestCase { try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) + // We use the timeout feature below. + try XCTSkipIf( + !ring.supportedFeatures.contains(.extendedArguments), + "Kernel < 5.11: timeouts in io_uring_enter aren't supported." + ) + // Test POLLOUT: Create a pipe to monitor for write readiness var pipeFDs: [Int32] = [0, 0] let pipeResult = pipe(&pipeFDs) @@ -367,7 +381,9 @@ final class IORingTests: XCTestCase { XCTAssert(enqueuedOut) // Consume the completion from the poll operation - let completionOut = try ring.blockingConsumeCompletion() + let completionOut = try ring.blockingConsumeCompletion( + timeout: .seconds(1) + ) XCTAssertEqual(completionOut.context, pollOutContext) let pollOut = Int32(IORing.Request.PollEvents.pollOut.rawValue) XCTAssertNotEqual( From 980a71f20c0891f983288942a6376a3da6848406 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 28 Jul 2026 13:57:16 -0700 Subject: [PATCH 14/19] Add test to show that `PollEvents` has too few values --- Tests/SystemTests/IORingTests.swift | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index d3639481..f9cedca4 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -504,6 +504,47 @@ final class IORingTests: XCTestCase { ) } } + + func testUnexpectedPollValue() throws { + try XCTSkipIf(!uringEnabled, failureMessage) + var ring = try IORing(queueDepth: 8) + let (readFD, writeFD) = try FileDescriptor.pipe() + defer { + try? readFD.close() + try? writeFD.close() + } + + try XCTSkipIf( + !ring.supportedFeatures.contains(.extendedArguments), + "Kernel < 5.11: timeouts in io_uring_enter aren't supported." + ) + + let request = IORing.Request.pollAdd( + readFD, pollEvents: .pollIn, isMultiShot: false, context: 97 + ) + let success = try ring.submit(linkedRequests: request) + XCTAssertEqual(success, true) + try writeFD.close() + + let dt = Duration.seconds(1) + let completion = try ring.blockingConsumeCompletion(timeout: dt) + + let values = [ // This should be caseIterable. + IORing.Request.PollEvents.pollIn.rawValue, + IORing.Request.PollEvents.pollOut.rawValue + ] + + for value in values { + if completion.result & Int32(value) != 0 { + // success! + return + } + } + + let pollValue = completion.result & 0x10 + XCTAssertEqual(pollValue, 0x10) + XCTFail("Unexpected poll event: 0x\(String(pollValue, radix: 16))") + } } #endif // os(Linux) #endif // compiler(>=6.2) && $Lifetimes From a2ca84e281eb56370197e23d52192159e54d510d Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 28 Jul 2026 14:37:14 -0700 Subject: [PATCH 15/19] Round out `PollEvents` --- Sources/System/IORing/PollEvents.swift | 62 +++++++++++++++++++++++--- Tests/SystemTests/IORingTests.swift | 18 +++----- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/Sources/System/IORing/PollEvents.swift b/Sources/System/IORing/PollEvents.swift index e24f8165..d8325978 100644 --- a/Sources/System/IORing/PollEvents.swift +++ b/Sources/System/IORing/PollEvents.swift @@ -14,7 +14,7 @@ extension IORing.Request { /// /// `PollEvents` represents the event mask used with io_uring poll /// operations to specify which I/O conditions to monitor on a file - /// descriptor. These events correspond to the standard Posix poll events + /// descriptor. These events correspond to the standard POSIX poll events /// defined in the kernel's `poll.h` header. /// /// Use `PollEvents` with @@ -32,7 +32,7 @@ extension IORing.Request { /// isMultiShot: true /// ) /// ``` - public struct PollEvents: OptionSet, Hashable, Codable { + public struct PollEvents: OptionSet, Hashable, Codable, CaseIterable { public var rawValue: UInt32 @inlinable @@ -40,22 +40,72 @@ extension IORing.Request { self.rawValue = rawValue } + @usableFromInline + init(_ event: Event) { + self.rawValue = event.rawValue + } + + @usableFromInline + enum Event: UInt32, RawRepresentable, Hashable, CaseIterable { + case pollIn = 0x0001 + case pollOut = 0x0004 + case pollErr = 0x0008 + case pollHup = 0x0010 + case pollNval = 0x0020 + } + + public static var allCases: [PollEvents] { + Event.allCases.map(PollEvents.init(_:)) + } + /// An event indicating data is available for reading. /// /// This event becomes active when data arrives on the file descriptor /// and can be read without blocking. For sockets, this includes when /// a new connection is available on a listening socket. Corresponds - /// to the Posix `POLLIN` event flag. + /// to the POSIX `POLLIN` event flag. @inlinable - public static var pollIn: PollEvents { PollEvents(rawValue: 0x0001) } + public static var pollIn: PollEvents { PollEvents(.pollIn) } /// An event indicating the file descriptor is ready for writing. /// /// This event becomes active when writing to the file descriptor will /// not block. For sockets, this indicates that send buffer space is - /// available. Corresponds to the Posix `POLLOUT` event flag. + /// available. Corresponds to the POSIX `POLLOUT` event flag. @inlinable - public static var pollOut: PollEvents { PollEvents(rawValue: 0x0004) } + public static var pollOut: PollEvents { PollEvents(.pollOut) } + + /// An event indicating an error condition on the file descriptor. + /// + /// The kernel reports this event whether or not it was requested, so + /// it can appear in a completion's result mask even when the poll + /// asked only for ``pollIn`` or ``pollOut``. Requesting it explicitly + /// has no effect. Corresponds to the POSIX `POLLERR` event flag. + @_alwaysEmitIntoClient + public static var pollErr: PollEvents { PollEvents(.pollErr) } + + /// An event indicating the peer closed its end of the channel. + /// + /// For a pipe this means the writing end was closed; for a socket, that + /// the connection was shut down. A descriptor reporting this event will + /// never become readable again, so treating it as "not ready yet" and + /// polling again will not make progress. + /// + /// The kernel reports this event whether or not it was requested, and + /// requesting it explicitly has no effect. Corresponds to the POSIX + /// `POLLHUP` event flag. + @_alwaysEmitIntoClient + public static var pollHup: PollEvents { PollEvents(.pollHup) } + + /// An event indicating the file descriptor is not open. + /// + /// This usually means the descriptor was closed, or was never valid. + /// + /// The kernel reports this event whether or not it was requested, and + /// requesting it explicitly has no effect. Corresponds to the POSIX + /// `POLLNVAL` event flag. + @_alwaysEmitIntoClient + public static var pollNval: PollEvents { PollEvents(.pollNval) } } } #endif diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index f9cedca4..ab974b94 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -505,7 +505,7 @@ final class IORingTests: XCTestCase { } } - func testUnexpectedPollValue() throws { + func testPollHangup() throws { try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 8) let (readFD, writeFD) = try FileDescriptor.pipe() @@ -529,21 +529,15 @@ final class IORingTests: XCTestCase { let dt = Duration.seconds(1) let completion = try ring.blockingConsumeCompletion(timeout: dt) - let values = [ // This should be caseIterable. - IORing.Request.PollEvents.pollIn.rawValue, - IORing.Request.PollEvents.pollOut.rawValue - ] - - for value in values { - if completion.result & Int32(value) != 0 { - // success! + for event in IORing.Request.PollEvents.allCases { + if completion.result & Int32(event.rawValue) != 0 { + XCTAssertEqual(event, .pollHup) return } } - let pollValue = completion.result & 0x10 - XCTAssertEqual(pollValue, 0x10) - XCTFail("Unexpected poll event: 0x\(String(pollValue, radix: 16))") + let unexpected = completion.result + XCTFail("Unexpected poll event: 0x\(String(unexpected, radix: 16))") } } #endif // os(Linux) From 4df226b09887be1629aff65eec63f86363ee9942 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 28 Jul 2026 14:49:03 -0700 Subject: [PATCH 16/19] Fix copyright notices --- Sources/System/IORing/IOCompletion.swift | 9 +++++++++ Sources/System/IORing/IORequest.swift | 9 +++++++++ Sources/System/IORing/IORing.swift | 9 +++++++++ Sources/System/IORing/PollEvents.swift | 2 +- Sources/System/IORing/RawIORequest.swift | 9 +++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift index d9e69050..7ad858c8 100644 --- a/Sources/System/IORing/IOCompletion.swift +++ b/Sources/System/IORing/IOCompletion.swift @@ -1,3 +1,12 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2023 - 2025 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + #if compiler(>=6.2) && $Lifetimes #if os(Linux) diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index dfe2dba9..8cec4e1c 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -1,3 +1,12 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2023 - 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + #if compiler(>=6.2) && $Lifetimes #if os(Linux) diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index 27d27a0e..bd6087dd 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -1,3 +1,12 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2023 - 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + #if compiler(>=6.2) && $Lifetimes #if os(Linux) diff --git a/Sources/System/IORing/PollEvents.swift b/Sources/System/IORing/PollEvents.swift index d8325978..59f78ac1 100644 --- a/Sources/System/IORing/PollEvents.swift +++ b/Sources/System/IORing/PollEvents.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 Apple Inc. and the Swift System project authors + Copyright (c) 2026 Apple Inc. and the Swift System project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index 507fc0db..ab4eb564 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -1,3 +1,12 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2023 - 2026 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + #if compiler(>=6.2) && $Lifetimes #if os(Linux) From a6d4c153b7e09d70e6a5c102299e08445e226a4c Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 28 Jul 2026 15:19:02 -0700 Subject: [PATCH 17/19] Add test to demonstrate a POLLERR result --- Tests/SystemTests/IORingTests.swift | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index ab974b94..cc6b8d1b 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -539,6 +539,44 @@ final class IORingTests: XCTestCase { let unexpected = completion.result XCTFail("Unexpected poll event: 0x\(String(unexpected, radix: 16))") } + + func testPollError() throws { + try XCTSkipIf(!uringEnabled, failureMessage) + var ring = try IORing(queueDepth: 8) + let (readFD, writeFD) = try FileDescriptor.pipe(options: .nonBlocking) + defer { + try? readFD.close() + try? writeFD.close() + } + + try XCTSkipIf( + !ring.supportedFeatures.contains(.extendedArguments), + "Kernel < 5.11: timeouts in io_uring_enter aren't supported." + ) + + let chunk = [UInt8](repeating: 0, count: 4096) + chunk.withUnsafeBytes { + // Fill the pipe by writing until an operation fails + while let written = try? writeFD.write($0), written > 0 {} + } + + let request = IORing.Request.pollAdd( + writeFD, pollEvents: .pollOut, isMultiShot: false, context: 98 + ) + let success = try ring.submit(linkedRequests: request) + XCTAssertEqual(success, true) + try readFD.close() + + let dt = Duration.seconds(1) + let completion = try ring.blockingConsumeCompletion(timeout: dt) + XCTAssertEqual(completion.context, 98) + + let pollErr = IORing.Request.PollEvents.pollErr.rawValue + let result = completion.result & Int32(pollErr) + if result != pollErr { + XCTFail("expected POLLERR, got 0x\(String(result, radix: 16))") + } + } } #endif // os(Linux) #endif // compiler(>=6.2) && $Lifetimes From d4373e36f6c35d831d3f3e5f2496484ba3f3d937 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 28 Jul 2026 16:20:56 -0700 Subject: [PATCH 18/19] Clarify .pollNval and add test reading from an invalid descriptor --- Sources/System/IORing/IOCompletion.swift | 7 ++++++ Sources/System/IORing/PollEvents.swift | 10 ++++++-- Tests/SystemTests/IORingTests.swift | 32 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift index 7ad858c8..8a1bc71b 100644 --- a/Sources/System/IORing/IOCompletion.swift +++ b/Sources/System/IORing/IOCompletion.swift @@ -54,6 +54,13 @@ public extension IORing.Completion { } } + /// The result of the completed operation. + /// + /// A non-negative value is the operation's success result: a byte count + /// for a read or a write, an event mask for a poll, and so on. + /// + /// A negative value is an `errno` code multiplied by -1. Recover the error + /// by negating it again: `Errno(rawValue: -completion.result)`. @inlinable var result: Int32 { get { rawValue.res diff --git a/Sources/System/IORing/PollEvents.swift b/Sources/System/IORing/PollEvents.swift index 59f78ac1..b9b77e30 100644 --- a/Sources/System/IORing/PollEvents.swift +++ b/Sources/System/IORing/PollEvents.swift @@ -97,9 +97,15 @@ extension IORing.Request { @_alwaysEmitIntoClient public static var pollHup: PollEvents { PollEvents(.pollHup) } - /// An event indicating the file descriptor is not open. + /// An event indicating that the object a descriptor refers to is no + /// longer valid. /// - /// This usually means the descriptor was closed, or was never valid. + /// This arises when the descriptor itself resolves, but the thing it + /// refers to has since become invalid. For example, the disconnection + /// of a sound device could cause this event. + /// + /// Note that a descriptor which simply does not resolve would + /// return the EBADF error code (Errno.badFileDescriptor). /// /// The kernel reports this event whether or not it was requested, and /// requesting it explicitly has no effect. Corresponds to the POSIX diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index cc6b8d1b..961af179 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -577,6 +577,38 @@ final class IORingTests: XCTestCase { XCTFail("expected POLLERR, got 0x\(String(result, radix: 16))") } } + + // A completion's `result` is two things in one field: a non-negative + // value is an event mask, and a negative value is a negated errno. The + // sign is the only thing distinguishing them, so it has to be checked + // before the value is treated as anything else. + func testPollAddOnInvalidDescriptor() throws { + try XCTSkipIf(!uringEnabled, failureMessage) + var ring = try IORing(queueDepth: 8) + + let request = IORing.Request.pollAdd( + FileDescriptor(rawValue: -1), pollEvents: .pollIn, + isMultiShot: false, context: 99 + ) + let success = try ring.submit(linkedRequests: request) + XCTAssertEqual(success, true) + + guard let completion = ring.tryConsumeCompletion() else { + XCTFail("expected a completion for the failed poll") + return + } + XCTAssertEqual(completion.context, 99) + + // A negative value for `result` marks the completion a a failure. + XCTAssertLessThan(completion.result, 0, "expected a failure") + // The negative value is the error code multiplied by -1. + XCTAssertEqual(Errno(rawValue: -completion.result), .badFileDescriptor) + + // A negative result may look like another result code. + // Checking for the error must happen first. + let pollNval = Int32(IORing.Request.PollEvents.pollNval.rawValue) + XCTAssertNotEqual(completion.result & pollNval, 0) + } } #endif // os(Linux) #endif // compiler(>=6.2) && $Lifetimes From 6e3259aec35aae30f61ff641fea63331d72ce913 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Tue, 28 Jul 2026 16:51:19 -0700 Subject: [PATCH 19/19] Better explain a skip condition --- Tests/SystemTests/IORingTests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 961af179..e3b45b5f 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -317,7 +317,7 @@ final class IORingTests: XCTestCase { try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) - // We use the timeout feature below. + // This test case requires timeout support try XCTSkipIf( !ring.supportedFeatures.contains(.extendedArguments), "Kernel < 5.11: timeouts in io_uring_enter aren't supported." @@ -356,7 +356,7 @@ final class IORingTests: XCTestCase { try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) - // We use the timeout feature below. + // This test case requires timeout support try XCTSkipIf( !ring.supportedFeatures.contains(.extendedArguments), "Kernel < 5.11: timeouts in io_uring_enter aren't supported." @@ -399,8 +399,7 @@ final class IORingTests: XCTestCase { try XCTSkipIf(!uringEnabled, failureMessage) var ring = try IORing(queueDepth: 32, flags: []) - // Every wait below has a timeout, so that a poll that fails to fire - // causes a test failure. + // This test case requires timeout support try XCTSkipIf( !ring.supportedFeatures.contains(.extendedArguments), "Kernel < 5.11: timeouts in io_uring_enter aren't supported." @@ -514,6 +513,7 @@ final class IORingTests: XCTestCase { try? writeFD.close() } + // This test case requires timeout support try XCTSkipIf( !ring.supportedFeatures.contains(.extendedArguments), "Kernel < 5.11: timeouts in io_uring_enter aren't supported." @@ -549,6 +549,7 @@ final class IORingTests: XCTestCase { try? writeFD.close() } + // This test case requires timeout support try XCTSkipIf( !ring.supportedFeatures.contains(.extendedArguments), "Kernel < 5.11: timeouts in io_uring_enter aren't supported."