Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/CSystem/include/io_uring.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 80 additions & 0 deletions Sources/System/IORing/IORequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically Swift naming style would be verb-first but if there's a good reason to have it this way it's probably fine

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to match what the io_uring operation was called. I wasn't sure how much we tried to change the naming to fit our Swift naming guidelines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guessed that was probably what you were doing. I remember this coming up during the initial proposal review and iirc folks leaned "don't try to make the names friendlier" so that looking up docs will work better. I'm still torn on it but I see the logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know what you prefer and I am happy to change if needed. I am open to both.

_ 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,
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions Sources/System/IORing/PollEvents.swift
Original file line number Diff line number Diff line change
@@ -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
60 changes: 59 additions & 1 deletion Tests/SystemTests/IORingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading