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
1 change: 1 addition & 0 deletions Sources/System/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions Sources/System/IORing/IOCompletion.swift
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -45,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
Expand Down
178 changes: 160 additions & 18 deletions Sources/System/IORing/IORequest.swift
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -23,6 +32,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 +202,100 @@ extension IORing.Request {
.init(core: .nop)
}

// 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.
///
/// 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.
///
/// ## 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
/// var ring = try IORing(queueDepth: 32)
/// let pollRequest = IORing.Request.pollAdd(
/// listenSocket,
/// pollEvents: .pollIn,
/// isMultiShot: true,
/// context: 1
/// )
/// guard try ring.submit(linkedRequests: pollRequest) else {
/// // The submission queue was full; retry or drain completions first.
/// return
/// }
///
/// // 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
/// }
/// }
/// ```
///
/// - 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

@glessard glessard Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

pollAdd is somewhat greppable with io_uring docs, but not directly since the names are usually something like io_uring_prep_poll_add or IORING_OP_POLL_ADD. This is the name for the request, so maybe just poll would be better: poll(fd, events: PollEvents, isMultiShot: Bool, context: UInt64).

A future addition would be removal of events, and the constant used for that in the C library is IORING_POLL_UPDATE_EVENTS. I don't think we would want pollUpdate or pollRemove, maybe just update.

_ file: FileDescriptor,
pollEvents: PollEvents,
isMultiShot: Bool = false,
context: UInt64 = 0
) -> IORing.Request {
.init(core: .pollAdd(file: file, pollEvents: pollEvents, isMultiShot: isMultiShot, context: context))
}

@inlinable public static func read(
_ file: IORing.RegisteredFile,
into buffer: IORing.RegisteredBuffer,
Expand Down Expand Up @@ -316,24 +425,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
Expand Down Expand Up @@ -477,6 +611,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 = Self.SWIFT_IORING_POLL_ADD_MULTI
}
request.pollEvents = pollEvents
}

return request
Expand Down
9 changes: 9 additions & 0 deletions Sources/System/IORing/IORing.swift
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
118 changes: 118 additions & 0 deletions Sources/System/IORing/PollEvents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
This source file is part of the Swift System open source project

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
*/

#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, CaseIterable {
public var rawValue: UInt32

@inlinable
public init(rawValue: UInt32) {
Comment thread
Catfish-Man marked this conversation as resolved.
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

@glessard glessard Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added cases for POLLHUP and POLLNVAL. These names are inscrutable in any situation. Should they be pollHup and pollNval, or pollHangup and pollInvalid, or hangUp and invalidDescriptor. For now I continued along with the earlier work in this PR and used what I consider to be the bad names.

Missing are POLLPRI and POLLRDHUP, which would be relevant for sockets.

Better names would be readable, writable, error, hangUp, invalidDescriptor (+ priorityData, and peerClosed)

}

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.
@inlinable
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.
@inlinable
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 that the object a descriptor refers to is no
/// longer 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
/// `POLLNVAL` event flag.
@_alwaysEmitIntoClient
public static var pollNval: PollEvents { PollEvents(.pollNval) }
}
}
#endif
#endif
Loading
Loading