/IORingSwift

A Swift wrapper for io_uring

Primary LanguageSwiftOtherNOASSERTION

IORingSwift

IORingSwift is a lightweight Swift wrapper for io_uring designed for use cases where performance is more important than portability. It is not intended to be a replacement for libdispatch or SwiftNIO; indeed, it presently requires the former, and it is somewhat less abstracted than the latter.

It was originally designed to support SPI in an embedded application, as the Linux SPI user space driver is synchronous, however it is equally adept at sockets. A discussion which led to its development can be found here.

The package consists of two libraries:

  • IORing, which provides async/await Swift concurrency-aware wrappers for making io_uring requests
  • IORingUtils, an optional library of helper functions
  • IORingFoundation, an optional library for using with Foundation

The intention is that this will also eventually support the real-time I/O subsystem in Zephyr, for use with SwiftIO and its wrapper cousin AsyncSwiftIO.

Architecture

IORing operations are isolated to the IORingActor global actor.

IORing is generally designed to be used as a singleton (IORing.shared), however because of some present API limitations to do with allocating fixed buffers, you may need to allocate separate instances. At present all instances share the same actor context and io_uring work queue, so there is no performance benefit to allocating more instances. (This should be considered an implementation detail, however.)

Public API provides structured concurrency wrappers around common operations such as reading and writing. Multishot APIs, such as accept(2), which can return multiple completions over time return an AnyAsyncSequence. Internally, wrappers allocate a concrete instance of Submission<T>, representing an initialized Submission Queue Entry (SQE), which is then submitted to the io_uring. Completion handlers are handled by having libdispatch monitor an eventfd(2) representing available completions. The user_data in each queue entry is a block, which executes the onCompletion(cqe:) method of the Submission<T> instance in the ring's isolated context. Care must be taken to manager pointer lifetimes across the event lifecycle.

Examples

Here's an example of a TCP echo server, adapted from IORingTCPEcho.

import AsyncExtensions
import IORing
import IORingUtils

let socket = try Socket(ring: IORing.shared, domain: sa_family_t(AF_INET), type: SOCK_STREAM, protocol: 0)
try socket.setReuseAddr()
try socket.setTcpNoDelay()
try socket.bind(port: 10000)
try socket.listen(backlog: 10)

let clients: AnyAsyncSequence<Socket> = try await socket.accept()
for try await client in clients {
    Task {
        repeat {
            let data = try await client.receive(count: bufferSize)
            try await client.send(data)
        } while true
    }
}

Further examples can be found in Examples.

Notes

  • You'll need a recent (6.x) kernel to use some of the functionality, such as multi-shot accept(2)
  • Tests are yet to be written, so caveat emptor

Pull requests are welcome, of course!