Skip to content

Commit e69d5c1

Browse files
committed
[IO_URING] Add support for pollAdd operation
## Motivation 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 ## Modifications 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`. ## Result We can now use IO_URING to poll for events on file descriptors.
1 parent b083113 commit e69d5c1

File tree

4 files changed

+200
-1
lines changed

4 files changed

+200
-1
lines changed

Sources/CSystem/include/io_uring.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe;
134134
#define IORING_FEAT_RW_ATTR (1U << 16)
135135
#define IORING_FEAT_NO_IOWAIT (1U << 17)
136136

137+
#define IORING_POLL_ADD_MULTI (1U << 0)
138+
137139
#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)
138140
typedef uint8_t __u8;
139141
typedef uint16_t __u16;

Sources/System/IORing/IORequest.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ internal enum IORequestCore {
2323
intoSlot: IORing.RegisteredFile,
2424
context: UInt64 = 0
2525
)
26+
case pollAdd(
27+
file: FileDescriptor,
28+
pollEvents: IORing.Request.PollEvents,
29+
isMultiShot: Bool = true,
30+
context: UInt64 = 0
31+
)
2632
case read(
2733
file: FileDescriptor,
2834
buffer: IORing.RegisteredBuffer,
@@ -187,6 +193,72 @@ extension IORing.Request {
187193
.init(core: .nop)
188194
}
189195

196+
/// Adds a poll operation to monitor a file descriptor for specific I/O events.
197+
///
198+
/// This method creates an io_uring poll operation that monitors the specified file descriptor
199+
/// for I/O readiness events. The operation completes when any of the requested events become
200+
/// active on the file descriptor, such as data becoming available for reading or the descriptor
201+
/// becoming ready for writing.
202+
///
203+
/// Poll operations are useful for implementing efficient I/O multiplexing, allowing you to
204+
/// monitor multiple file descriptors concurrently within a single io_uring instance. When used
205+
/// with multishot mode, a single poll operation can deliver multiple completion events without
206+
/// needing to be resubmitted.
207+
///
208+
/// ## Multishot Behavior
209+
///
210+
/// When `isMultiShot` is `true`, the poll operation automatically rearms after each completion
211+
/// event, continuing to monitor the file descriptor for subsequent events. This reduces
212+
/// submission overhead for long-lived monitoring operations. The operation continues until
213+
/// explicitly cancelled or the file descriptor is closed.
214+
///
215+
/// When `isMultiShot` is `false`, the poll operation completes once after the first matching
216+
/// event occurs, requiring resubmission to continue monitoring.
217+
///
218+
/// ## Example Usage
219+
///
220+
/// ```swift
221+
/// // Monitor a socket for incoming connections
222+
/// let pollRequest = IORing.Request.pollAdd(
223+
/// listenSocket,
224+
/// pollEvents: .pollin,
225+
/// isMultiShot: true,
226+
/// context: 1
227+
/// )
228+
/// try ring.submit(pollRequest)
229+
///
230+
/// // Process completions
231+
/// for completion in try ring.completions() {
232+
/// if completion.context == 1 {
233+
/// // Handle incoming connection
234+
/// }
235+
/// }
236+
/// ```
237+
///
238+
/// - Parameters:
239+
/// - file: The file descriptor to monitor for I/O events.
240+
/// - pollEvents: The I/O events to monitor on the file descriptor.
241+
/// - isMultiShot: If `true`, the poll operation automatically rearms after each event,
242+
/// continuing to monitor the file descriptor. If `false`, the operation completes after
243+
/// the first matching event. Defaults to `true`.
244+
/// - context: An application-specific value passed through to the completion event,
245+
/// allowing you to identify which operation completed. Defaults to `0`.
246+
///
247+
/// - Returns: An I/O ring request that monitors the file descriptor for the specified events.
248+
///
249+
/// ## See Also
250+
///
251+
/// - ``PollEvents``: The events that can be monitored.
252+
/// - ``IORing/Request/cancel(_:matching:)``: Cancelling poll operations.
253+
@inlinable public static func pollAdd(
254+
_ file: FileDescriptor,
255+
pollEvents: PollEvents,
256+
isMultiShot: Bool = true,
257+
context: UInt64 = 0
258+
) -> IORing.Request {
259+
.init(core: .pollAdd(file: file, pollEvents: pollEvents, context: context))
260+
}
261+
190262
@inlinable public static func read(
191263
_ file: IORing.RegisteredFile,
192264
into buffer: IORing.RegisteredBuffer,
@@ -488,6 +560,14 @@ extension IORing.Request {
488560
case .cancel(let flags):
489561
request.operation = .asyncCancel
490562
request.cancel_flags = flags
563+
case .pollAdd(let file, let pollEvents, let isMultiShot, let context):
564+
request.operation = .pollAdd
565+
request.fileDescriptor = file
566+
request.rawValue.user_data = context
567+
if isMultiShot {
568+
request.rawValue.len = IORING_POLL_ADD_MULTI
569+
}
570+
request.rawValue.poll32_events = pollEvents.rawValue
491571
}
492572

493573
return request
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
This source file is part of the Swift System open source project
3+
4+
Copyright (c) 2020 Apple Inc. and the Swift System project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
*/
9+
10+
#if compiler(>=6.2) && $Lifetimes
11+
#if os(Linux)
12+
extension IORing.Request {
13+
/// A set of I/O events that can be monitored on a file descriptor.
14+
///
15+
/// `PollEvents` represents the event mask used with io_uring poll operations to specify
16+
/// which I/O conditions to monitor on a file descriptor. These events correspond to the
17+
/// standard Posix poll events defined in the kernel's `poll.h` header.
18+
///
19+
/// Use `PollEvents` with ``IORing/Request/pollAdd(_:pollEvents:isMultiShot:context:)``
20+
/// to register interest in specific I/O events. The poll operation completes when any of
21+
/// the specified events become active on the file descriptor.
22+
///
23+
/// ## Usage
24+
///
25+
/// ```swift
26+
/// // Monitor a socket for incoming data
27+
/// let request = IORing.Request.pollAdd(
28+
/// socketFD,
29+
/// pollEvents: .pollin,
30+
/// isMultiShot: true
31+
/// )
32+
/// ```
33+
public struct PollEvents: OptionSet, Hashable, Codable {
34+
public var rawValue: UInt32
35+
36+
@inlinable
37+
public init(rawValue: UInt32) {
38+
self.rawValue = rawValue
39+
}
40+
41+
/// An event indicating data is available for reading.
42+
///
43+
/// This event becomes active when data arrives on the file descriptor and can be read
44+
/// without blocking. For sockets, this includes when a new connection is available on
45+
/// a listening socket. Corresponds to the Posix `POLLIN` event flag.
46+
@inlinable
47+
public static var pollin: PollEvents { PollEvents(rawValue: 0x0001) }
48+
49+
/// An event indicating the file descriptor is ready for writing.
50+
///
51+
/// This event becomes active when writing to the file descriptor will not block. For
52+
/// sockets, this indicates that send buffer space is available. Corresponds to the
53+
/// Posix `POLLOUT` event flag.
54+
@inlinable
55+
public static var pollout: PollEvents { PollEvents(rawValue: 0x0004) }
56+
}
57+
}
58+
#endif
59+
#endif

Tests/SystemTests/IORingTests.swift

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,69 @@ final class IORingTests: XCTestCase {
122122
let bytesRead = try nonRingFD.read(into: rawBuffer)
123123
XCTAssert(bytesRead == 13)
124124
let result2 = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!)
125-
XCTAssertEqual(result2, "Hello, World!")
125+
XCTAssertEqual(result2, "Hello, World!")
126126
try cleanUpHelloWorldFile(parent)
127127
efdBuf.deallocate()
128128
rawBuffer.deallocate()
129129
}
130+
131+
func testPollAddPollIn() throws {
132+
guard try uringEnabled() else { return }
133+
var ring = try IORing(queueDepth: 32, flags: [])
134+
135+
// Test POLLIN: Create an eventfd to monitor for read readiness
136+
let testEventFD = FileDescriptor(rawValue: eventfd(0, 0))
137+
defer {
138+
// Clean up
139+
try! testEventFD.close()
140+
}
141+
let pollInContext: UInt64 = 42
142+
143+
// Submit a pollAdd request to monitor for POLLIN events (data available for reading)
144+
let enqueued = try ring.submit(linkedRequests:
145+
.pollAdd(testEventFD, pollEvents: .pollin, isMultiShot: false, context: pollInContext))
146+
XCTAssert(enqueued)
147+
148+
// Write to the eventfd to trigger the POLLIN event
149+
var value: UInt64 = 1
150+
withUnsafeBytes(of: &value) { bufferPtr in
151+
_ = try? testEventFD.write(bufferPtr)
152+
}
153+
154+
// Consume the completion from the poll operation
155+
let completion = try ring.blockingConsumeCompletion()
156+
XCTAssertEqual(completion.context, pollInContext)
157+
XCTAssertGreaterThan(completion.result, 0) // Poll should return mask of ready events
158+
}
159+
160+
func testPollAddPollOut() throws {
161+
guard try uringEnabled() else { return }
162+
var ring = try IORing(queueDepth: 32, flags: [])
163+
164+
// Test POLLOUT: Create a pipe to monitor for write readiness
165+
var pipeFDs: [Int32] = [0, 0]
166+
let pipeResult = pipe(&pipeFDs)
167+
XCTAssertEqual(pipeResult, 0)
168+
let writeFD = FileDescriptor(rawValue: pipeFDs[1])
169+
let readFD = FileDescriptor(rawValue: pipeFDs[0])
170+
defer {
171+
// Clean up
172+
try! writeFD.close()
173+
try! readFD.close()
174+
}
175+
let pollOutContext: UInt64 = 43
176+
177+
// Submit a pollAdd request to monitor for POLLOUT events (ready for writing)
178+
// Pipes are typically ready for writing when empty
179+
let enqueuedOut = try ring.submit(linkedRequests:
180+
.pollAdd(writeFD, pollEvents: .pollout, isMultiShot: false, context: pollOutContext))
181+
XCTAssert(enqueuedOut)
182+
183+
// Consume the completion from the poll operation
184+
let completionOut = try ring.blockingConsumeCompletion()
185+
XCTAssertEqual(completionOut.context, pollOutContext)
186+
XCTAssertGreaterThan(completionOut.result, 0) // Poll should return mask of ready events
187+
}
130188
}
131189
#endif // os(Linux)
132190
#endif // compiler(>=6.2) && $Lifetimes

0 commit comments

Comments
 (0)