diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index ce618f3a..6b6988ef 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -134,6 +134,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 17be7927..bcd74051 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, @@ -488,6 +560,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 c5d54d41..b233d785 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -122,11 +122,69 @@ 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() } + + func testPollAddPollIn() throws { + guard try uringEnabled() else { return } + 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 try uringEnabled() else { return } + 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