From 15794f5cad319a58a8ed768af3947e2228ced0ce Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Tue, 18 Jul 2023 21:04:39 -0700 Subject: [PATCH 001/172] ioring pitch: first steps --- Package.resolved | 16 ++ Package.swift | 9 +- Sources/CSystem/include/CSystemLinux.h | 1 + Sources/CSystem/include/io_uring.h | 50 ++++ Sources/System/IOCompletion.swift | 50 ++++ Sources/System/IORequest.swift | 140 ++++++++++ Sources/System/IORing.swift | 350 +++++++++++++++++++++++++ Sources/System/IORingBuffer.swift | 0 Sources/System/IORingFileSlot.swift | 8 + Sources/System/Lock.swift | 37 +++ Sources/System/ManagedIORing.swift | 33 +++ 11 files changed, 692 insertions(+), 2 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/CSystem/include/io_uring.h create mode 100644 Sources/System/IOCompletion.swift create mode 100644 Sources/System/IORequest.swift create mode 100644 Sources/System/IORing.swift create mode 100644 Sources/System/IORingBuffer.swift create mode 100644 Sources/System/IORingFileSlot.swift create mode 100644 Sources/System/Lock.swift create mode 100644 Sources/System/ManagedIORing.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..b10a9832 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics", + "state": { + "branch": null, + "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", + "version": "1.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index b081a872..60f1a562 100644 --- a/Package.swift +++ b/Package.swift @@ -17,14 +17,19 @@ let package = Package( products: [ .library(name: "SystemPackage", targets: ["SystemPackage"]), ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/apple/swift-atomics", from: "1.1.0") + ], targets: [ .target( name: "CSystem", dependencies: []), .target( name: "SystemPackage", - dependencies: ["CSystem"], + dependencies: [ + "CSystem", + .product(name: "Atomics", package: "swift-atomics") + ], path: "Sources/System", cSettings: [ .define("_CRT_SECURE_NO_WARNINGS") diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h index b172d658..6489c4f3 100644 --- a/Sources/CSystem/include/CSystemLinux.h +++ b/Sources/CSystem/include/CSystemLinux.h @@ -21,5 +21,6 @@ #include #include #include +#include "io_uring.h" #endif diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h new file mode 100644 index 00000000..9e3d9fb3 --- /dev/null +++ b/Sources/CSystem/include/io_uring.h @@ -0,0 +1,50 @@ +#include +#include +#include + +#include +#include + +#ifdef __alpha__ +/* + * alpha is the only exception, all other architectures + * have common numbers for new system calls. + */ +# ifndef __NR_io_uring_setup +# define __NR_io_uring_setup 535 +# endif +# ifndef __NR_io_uring_enter +# define __NR_io_uring_enter 536 +# endif +# ifndef __NR_io_uring_register +# define __NR_io_uring_register 537 +# endif +#else /* !__alpha__ */ +# ifndef __NR_io_uring_setup +# define __NR_io_uring_setup 425 +# endif +# ifndef __NR_io_uring_enter +# define __NR_io_uring_enter 426 +# endif +# ifndef __NR_io_uring_register +# define __NR_io_uring_register 427 +# endif +#endif + +int io_uring_register(int fd, unsigned int opcode, void *arg, + unsigned int nr_args) +{ + return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args); +} + +int io_uring_setup(unsigned int entries, struct io_uring_params *p) +{ + return syscall(__NR_io_uring_setup, entries, p); +} + +int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, + unsigned int flags, sigset_t *sig) +{ + return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, + flags, sig, _NSIG / 8); +} diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift new file mode 100644 index 00000000..5e226322 --- /dev/null +++ b/Sources/System/IOCompletion.swift @@ -0,0 +1,50 @@ +@_implementationOnly import CSystem + +public struct IOCompletion { + let rawValue: io_uring_cqe +} + +extension IOCompletion { + public struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let allocatedBuffer = Flags(rawValue: 1 << 0) + public static let moreCompletions = Flags(rawValue: 1 << 1) + public static let socketNotEmpty = Flags(rawValue: 1 << 2) + public static let isNotificationEvent = Flags(rawValue: 1 << 3) + } +} + +extension IOCompletion { + public var userData: UInt64 { + get { + return rawValue.user_data + } + } + + public var result: Int32 { + get { + return rawValue.res + } + } + + public var flags: IOCompletion.Flags { + get { + return Flags(rawValue: rawValue.flags & 0x0000FFFF) + } + } + + public var bufferIndex: UInt16? { + get { + if self.flags.contains(.allocatedBuffer) { + return UInt16(rawValue.flags >> 16) + } else { + return nil + } + } + } +} diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift new file mode 100644 index 00000000..efa03f3b --- /dev/null +++ b/Sources/System/IORequest.swift @@ -0,0 +1,140 @@ +@_implementationOnly import CSystem + +public struct IORequest { + internal var rawValue: io_uring_sqe + + public init() { + self.rawValue = io_uring_sqe() + } +} + +extension IORequest { + public enum Operation: UInt8 { + case nop = 0 + case readv = 1 + case writev = 2 + case fsync = 3 + case readFixed = 4 + case writeFixed = 5 + case pollAdd = 6 + case pollRemove = 7 + case syncFileRange = 8 + case sendMessage = 9 + case receiveMessage = 10 + // ... + case openAt = 18 + case read = 22 + case write = 23 + case openAt2 = 28 + + } + + public struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let fixedFile = Flags(rawValue: 1 << 0) + public static let drainQueue = Flags(rawValue: 1 << 1) + public static let linkRequest = Flags(rawValue: 1 << 2) + public static let hardlinkRequest = Flags(rawValue: 1 << 3) + public static let asynchronous = Flags(rawValue: 1 << 4) + public static let selectBuffer = Flags(rawValue: 1 << 5) + public static let skipSuccess = Flags(rawValue: 1 << 6) + } + + public var operation: Operation { + get { Operation(rawValue: rawValue.opcode)! } + set { rawValue.opcode = newValue.rawValue } + } + + public var flags: Flags { + get { Flags(rawValue: rawValue.flags) } + set { rawValue.flags = newValue.rawValue } + } + + public var fileDescriptor: FileDescriptor { + get { FileDescriptor(rawValue: rawValue.fd) } + set { rawValue.fd = newValue.rawValue } + } + + public var offset: UInt64? { + get { + if (rawValue.off == UInt64.max) { + return nil + } else { + return rawValue.off + } + } + set { + if let val = newValue { + rawValue.off = val + } else { + rawValue.off = UInt64.max + } + } + } + + public var buffer: UnsafeMutableRawBufferPointer { + get { + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(exactly: rawValue.addr)!) + return UnsafeMutableRawBufferPointer(start: ptr, count: Int(rawValue.len)) + } + + set { + // TODO: cleanup? + rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) + rawValue.len = UInt32(exactly: newValue.count)! + } + } +} + +extension IORequest { + static func nop() -> IORequest { + var req = IORequest() + req.operation = .nop + return req + } + + static func read( + from fileDescriptor: FileDescriptor, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64? = nil + ) -> IORequest { + var req = IORequest.readWrite( + op: Operation.read, + fd: fileDescriptor, + buffer: buffer, + offset: offset + ) + fatalError() + } + + static func read( + fixedFile: Int // TODO: AsyncFileDescriptor + ) -> IORequest { + fatalError() + } + + static func write( + + ) -> IORequest { + fatalError() + } + + internal static func readWrite( + op: Operation, + fd: FileDescriptor, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64? = nil + ) -> IORequest { + var req = IORequest() + req.operation = op + req.fileDescriptor = fd + req.offset = offset + req.buffer = buffer + return req + } +} \ No newline at end of file diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift new file mode 100644 index 00000000..6945d8c0 --- /dev/null +++ b/Sources/System/IORing.swift @@ -0,0 +1,350 @@ +@_implementationOnly import CSystem +import Glibc +import Atomics + +// XXX: this *really* shouldn't be here. oh well. +extension UnsafeMutableRawPointer { + func advanced(by offset: UInt32) -> UnsafeMutableRawPointer { + return advanced(by: Int(offset)) + } +} + +// all pointers in this struct reference kernel-visible memory +struct SQRing { + let kernelHead: UnsafeAtomic + let kernelTail: UnsafeAtomic + var userTail: UInt32 + + // from liburing: the kernel should never change these + // might change in the future with resizable rings? + let ringMask: UInt32 + // let ringEntries: UInt32 - absorbed into array.count + + // ring flags bitfield + // currently used by the kernel only in SQPOLL mode to indicate + // when the polling thread needs to be woken up + let flags: UnsafeAtomic + + // ring array + // maps indexes between the actual ring and the submissionQueueEntries list, + // allowing the latter to be used as a kind of freelist with enough work? + // currently, just 1:1 mapping (0.. +} + +struct CQRing { + let kernelHead: UnsafeAtomic + let kernelTail: UnsafeAtomic + + // TODO: determine if this is actually used + var userHead: UInt32 + + let ringMask: UInt32 + + let cqes: UnsafeBufferPointer +} + +// XXX: This should be a non-copyable type (?) +// demo only runs on Swift 5.8.1 +public final class IORing: Sendable { + let ringFlags: UInt32 + let ringDescriptor: Int32 + + var submissionRing: SQRing + var submissionMutex: Mutex + // FEAT: set this eventually + let submissionPolling: Bool = false + + var completionRing: CQRing + var completionMutex: Mutex + + let submissionQueueEntries: UnsafeMutableBufferPointer + + var registeredFiles: UnsafeMutableBufferPointer? + + // kept around for unmap / cleanup + let ringSize: Int + let ringPtr: UnsafeMutableRawPointer + + public init(queueDepth: UInt32) throws { + var params = io_uring_params() + + ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { + return io_uring_setup(queueDepth, $0); + } + + if (params.features & IORING_FEAT_SINGLE_MMAP == 0 + || params.features & IORING_FEAT_NODROP == 0) { + close(ringDescriptor) + // TODO: error handling + fatalError("kernel not new enough") + } + + if (ringDescriptor < 0) { + // TODO: error handling + } + + let submitRingSize = params.sq_off.array + + params.sq_entries * UInt32(MemoryLayout.size) + + let completionRingSize = params.cq_off.cqes + + params.cq_entries * UInt32(MemoryLayout.size) + + ringSize = Int(max(submitRingSize, completionRingSize)) + + ringPtr = mmap( + /* addr: */ nil, + /* len: */ ringSize, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_SQ_RING) + ); + + if (ringPtr == MAP_FAILED) { + perror("mmap"); + // TODO: error handling + fatalError() + } + + let kernelHead = UnsafeAtomic(at: + ringPtr.advanced(by: params.sq_off.head) + .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + ) + + submissionRing = SQRing( + kernelHead: UnsafeAtomic( + at: ringPtr.advanced(by: params.sq_off.head) + .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + ), + kernelTail: UnsafeAtomic( + at: ringPtr.advanced(by: params.sq_off.tail) + .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + ), + userTail: 0, // no requests yet + ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) + .assumingMemoryBound(to: UInt32.self).pointee, + flags: UnsafeAtomic( + at: ringPtr.advanced(by: params.sq_off.flags) + .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + ), + array: UnsafeMutableBufferPointer( + start: ringPtr.advanced(by: params.sq_off.array) + .assumingMemoryBound(to: UInt32.self), + count: Int(ringPtr.advanced(by: params.sq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) + ) + ) + + // fill submission ring array with 1:1 map to underlying SQEs + for i in 0...size, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_SQES) + ); + + if (sqes == MAP_FAILED) { + perror("mmap"); + // TODO: error handling + fatalError() + } + + submissionQueueEntries = UnsafeMutableBufferPointer( + start: sqes!.assumingMemoryBound(to: io_uring_sqe.self), + count: Int(params.sq_entries) + ) + + completionRing = CQRing( + kernelHead: UnsafeAtomic( + at: ringPtr.advanced(by: params.cq_off.head) + .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + ), + kernelTail: UnsafeAtomic( + at: ringPtr.advanced(by: params.cq_off.tail) + .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + ), + userHead: 0, // no completions yet + ringMask: ringPtr.advanced(by: params.cq_off.ring_mask) + .assumingMemoryBound(to: UInt32.self).pointee, + cqes: UnsafeBufferPointer( + start: ringPtr.advanced(by: params.cq_off.cqes) + .assumingMemoryBound(to: io_uring_cqe.self), + count: Int(ringPtr.advanced(by: params.cq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) + ) + ) + + self.submissionMutex = Mutex() + self.completionMutex = Mutex() + + self.ringFlags = params.flags + } + + func blockingConsumeCompletion() -> IOCompletion { + self.completionMutex.lock() + defer { self.completionMutex.unlock() } + + if let completion = _tryConsumeCompletion() { + return completion + } else { + _waitForCompletion() + return _tryConsumeCompletion().unsafelyUnwrapped + } + } + + func _waitForCompletion() { + // TODO: error handling + io_uring_enter(ringDescriptor, 0, 1, IORING_ENTER_GETEVENTS, nil) + } + + func tryConsumeCompletion() -> IOCompletion? { + self.completionMutex.lock() + defer { self.completionMutex.unlock() } + return _tryConsumeCompletion() + } + + func _tryConsumeCompletion() -> IOCompletion? { + let tail = completionRing.kernelTail.load(ordering: .acquiring) + var head = completionRing.kernelHead.load(ordering: .relaxed) + + if tail != head { + // 32 byte copy - oh well + let res = completionRing.cqes[Int(head & completionRing.ringMask)] + completionRing.kernelHead.store(head + 1, ordering: .relaxed) + return IOCompletion(rawValue: res) + } + + return nil + } + + + func registerFiles(count: UInt32) { + // TODO: implement + guard self.registeredFiles == nil else { fatalError() } + let fileBuf = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) + fileBuf.initialize(repeating: UInt32.max) + io_uring_register( + self.ringDescriptor, + IORING_REGISTER_FILES, + fileBuf.baseAddress!, + count + ) + // TODO: error handling + self.registeredFiles = fileBuf + } + + func unregisterFiles() { + if self.registeredFiles != nil { + io_uring_register( + self.ringDescriptor, + IORING_UNREGISTER_FILES, + self.registeredFiles!.baseAddress!, + UInt32(self.registeredFiles!.count) + ) + // TODO: error handling + self.registeredFiles!.deallocate() + self.registeredFiles = nil + } + } + + // register a group of buffers + func registerBuffers(bufSize: UInt32, count: UInt32) { + // + + } + + func getBuffer() -> (index: Int, buf: UnsafeRawBufferPointer) { + fatalError() + } + + // TODO: types + func submitRequests() { + self.submissionMutex.lock() + defer { self.submissionMutex.unlock() } + self._submitRequests() + } + + func _submitRequests() { + let flushedEvents = _flushQueue() + + // Ring always needs enter right now; + // TODO: support SQPOLL here + + let ret = io_uring_enter(ringDescriptor, flushedEvents, 0, 0, nil) + // TODO: handle errors + } + + internal func _flushQueue() -> UInt32 { + self.submissionRing.kernelTail.store( + self.submissionRing.userTail, ordering: .relaxed + ) + return self.submissionRing.userTail - + self.submissionRing.kernelHead.load(ordering: .relaxed) + } + + + func writeRequest(_ request: __owned IORequest) -> Bool { + self.submissionMutex.lock() + defer { self.submissionMutex.unlock() } + return _writeRequest(request) + } + + internal func _writeRequest(_ request: __owned IORequest) -> Bool { + if let entry = _getSubmissionEntry() { + entry.pointee = request.rawValue + return true + } + return false + } + + internal func _blockingGetSubmissionEntry() -> UnsafeMutablePointer { + while true { + if let entry = _getSubmissionEntry() { + return entry + } + // TODO: actually block here instead of spinning + } + + } + + internal func _getSubmissionEntry() -> UnsafeMutablePointer? { + let next = self.submissionRing.userTail + 1 + + // FEAT: smp load when SQPOLL in use (not in MVP) + let kernelHead = self.submissionRing.kernelHead.load(ordering: .relaxed) + + // FEAT: 128-bit event support (not in MVP) + if (next - kernelHead <= self.submissionRing.array.count) { + // let sqe = &sq->sqes[(sq->sqe_tail & sq->ring_mask) << shift]; + let sqeIndex = Int( + self.submissionRing.userTail & self.submissionRing.ringMask + ) + + let sqe = self.submissionQueueEntries + .baseAddress.unsafelyUnwrapped + .advanced(by: sqeIndex) + + self.submissionRing.userTail = next; + return sqe + } + return nil + } + + deinit { + munmap(ringPtr, ringSize); + munmap( + UnsafeMutableRawPointer(submissionQueueEntries.baseAddress!), + submissionQueueEntries.count * MemoryLayout.size + ) + close(ringDescriptor) + } +}; + diff --git a/Sources/System/IORingBuffer.swift b/Sources/System/IORingBuffer.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/System/IORingFileSlot.swift b/Sources/System/IORingFileSlot.swift new file mode 100644 index 00000000..d0a7c666 --- /dev/null +++ b/Sources/System/IORingFileSlot.swift @@ -0,0 +1,8 @@ +class IORingFileSlot { + + + deinit { + // return file slot + + } +} \ No newline at end of file diff --git a/Sources/System/Lock.swift b/Sources/System/Lock.swift new file mode 100644 index 00000000..c5204ba0 --- /dev/null +++ b/Sources/System/Lock.swift @@ -0,0 +1,37 @@ +// TODO: write against kernel APIs directly? +import Glibc + +public final class Mutex { + @usableFromInline let mutex: UnsafeMutablePointer + + @inlinable init() { + self.mutex = UnsafeMutablePointer.allocate(capacity: 1) + self.mutex.initialize(to: pthread_mutex_t()) + pthread_mutex_init(self.mutex, nil) + } + + @inlinable deinit { + defer { mutex.deallocate() } + guard pthread_mutex_destroy(mutex) == 0 else { + preconditionFailure("unable to destroy mutex") + } + } + + // XXX: this is because we need to lock the mutex in the context of a submit() function + // and unlock *before* the UnsafeContinuation returns. + // Code looks like: { + // // prepare request + // io_uring_get_sqe() + // io_uring_prep_foo(...) + // return await withUnsafeContinuation { + // sqe->user_data = ...; io_uring_submit(); unlock(); + // } + // } + @inlinable @inline(__always) public func lock() { + pthread_mutex_lock(mutex) + } + + @inlinable @inline(__always) public func unlock() { + pthread_mutex_unlock(mutex) + } +} diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift new file mode 100644 index 00000000..32c84faf --- /dev/null +++ b/Sources/System/ManagedIORing.swift @@ -0,0 +1,33 @@ +final public class ManagedIORing: @unchecked Sendable { + var internalRing: IORing + + init(queueDepth: UInt32) throws { + self.internalRing = try IORing(queueDepth: queueDepth) + self.startWaiter() + } + + private func startWaiter() { + Task.detached { + while (!Task.isCancelled) { + let cqe = self.internalRing.blockingConsumeCompletion() + + let cont = unsafeBitCast(cqe.userData, to: UnsafeContinuation.self) + cont.resume(returning: cqe) + } + } + } + + @_unsafeInheritExecutor + public func submitAndWait(_ request: __owned IORequest) async -> IOCompletion { + self.internalRing.submissionMutex.lock() + return await withUnsafeContinuation { cont in + let entry = internalRing._blockingGetSubmissionEntry() + entry.pointee = request.rawValue + entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) + self.internalRing._submitRequests() + self.internalRing.submissionMutex.unlock() + } + } + + +} \ No newline at end of file From 6338029b74b84b79795a0cc24bcc189883ec083d Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Mon, 31 Jul 2023 22:28:43 -0700 Subject: [PATCH 002/172] stage 2: IORequest enum --- Sources/System/IORequest.swift | 206 ++++++++++---------------- Sources/System/IORing.swift | 218 +++++++++++++++++++++------- Sources/System/IORingBuffer.swift | 0 Sources/System/IORingError.swift | 3 + Sources/System/IORingFileSlot.swift | 8 - Sources/System/Lock.swift | 2 +- Sources/System/ManagedIORing.swift | 11 +- Sources/System/RawIORequest.swift | 139 ++++++++++++++++++ 8 files changed, 396 insertions(+), 191 deletions(-) delete mode 100644 Sources/System/IORingBuffer.swift create mode 100644 Sources/System/IORingError.swift delete mode 100644 Sources/System/IORingFileSlot.swift create mode 100644 Sources/System/RawIORequest.swift diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index efa03f3b..6b26a4d1 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,140 +1,82 @@ -@_implementationOnly import CSystem - -public struct IORequest { - internal var rawValue: io_uring_sqe +import struct CSystem.io_uring_sqe + +public enum IORequest { + case nop // nothing here + case openat( + atDirectory: FileDescriptor, + path: UnsafePointer, + FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + intoSlot: IORingFileSlot? = nil + ) + case read( + file: File, + buffer: Buffer, + offset: UInt64 = 0 + ) + case write( + file: File, + buffer: Buffer, + offset: UInt64 = 0 + ) + + public enum Buffer { + case registered(IORingBuffer) + case unregistered(UnsafeMutableRawBufferPointer) + } - public init() { - self.rawValue = io_uring_sqe() + public enum File { + case registered(IORingFileSlot) + case unregistered(FileDescriptor) } } extension IORequest { - public enum Operation: UInt8 { - case nop = 0 - case readv = 1 - case writev = 2 - case fsync = 3 - case readFixed = 4 - case writeFixed = 5 - case pollAdd = 6 - case pollRemove = 7 - case syncFileRange = 8 - case sendMessage = 9 - case receiveMessage = 10 - // ... - case openAt = 18 - case read = 22 - case write = 23 - case openAt2 = 28 - - } - - public struct Flags: OptionSet, Hashable, Codable { - public let rawValue: UInt8 - - public init(rawValue: UInt8) { - self.rawValue = rawValue - } - - public static let fixedFile = Flags(rawValue: 1 << 0) - public static let drainQueue = Flags(rawValue: 1 << 1) - public static let linkRequest = Flags(rawValue: 1 << 2) - public static let hardlinkRequest = Flags(rawValue: 1 << 3) - public static let asynchronous = Flags(rawValue: 1 << 4) - public static let selectBuffer = Flags(rawValue: 1 << 5) - public static let skipSuccess = Flags(rawValue: 1 << 6) - } - - public var operation: Operation { - get { Operation(rawValue: rawValue.opcode)! } - set { rawValue.opcode = newValue.rawValue } - } - - public var flags: Flags { - get { Flags(rawValue: rawValue.flags) } - set { rawValue.flags = newValue.rawValue } - } - - public var fileDescriptor: FileDescriptor { - get { FileDescriptor(rawValue: rawValue.fd) } - set { rawValue.fd = newValue.rawValue } - } - - public var offset: UInt64? { - get { - if (rawValue.off == UInt64.max) { - return nil - } else { - return rawValue.off - } - } - set { - if let val = newValue { - rawValue.off = val - } else { - rawValue.off = UInt64.max - } - } - } - - public var buffer: UnsafeMutableRawBufferPointer { - get { - let ptr = UnsafeMutableRawPointer(bitPattern: UInt(exactly: rawValue.addr)!) - return UnsafeMutableRawBufferPointer(start: ptr, count: Int(rawValue.len)) - } - - set { - // TODO: cleanup? - rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) - rawValue.len = UInt32(exactly: newValue.count)! + @inlinable @inline(__always) + public func makeRawRequest() -> RawIORequest { + var request = RawIORequest() + switch self { + case .nop: + request.operation = .nop + case .openat(let atDirectory, let path, let mode, let options, let permissions, let slot): + // TODO: use rawValue less + request.operation = .openAt + request.fileDescriptor = atDirectory + request.rawValue.addr = unsafeBitCast(path, to: UInt64.self) + request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) + request.rawValue.len = permissions?.rawValue ?? 0 + request.rawValue.file_index = UInt32(slot?.index ?? 0) + case .read(let file, let buffer, let offset), .write(let file, let buffer, let offset): + if case .read = self { + if case .registered = buffer { + request.operation = .readFixed + } else { + request.operation = .read + } + } else { + if case .registered = buffer { + request.operation = .writeFixed + } else { + request.operation = .write + } + } + switch file { + case .registered(let regFile): + request.rawValue.fd = Int32(exactly: regFile.index)! + request.flags = .fixedFile + case .unregistered(let fd): + request.fileDescriptor = fd + } + switch buffer { + case .registered(let regBuf): + request.buffer = regBuf.unsafeBuffer + request.rawValue.buf_index = UInt16(exactly: regBuf.index)! + case .unregistered(let buf): + request.buffer = buf + } + request.offset = offset } + return request } } - -extension IORequest { - static func nop() -> IORequest { - var req = IORequest() - req.operation = .nop - return req - } - - static func read( - from fileDescriptor: FileDescriptor, - into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64? = nil - ) -> IORequest { - var req = IORequest.readWrite( - op: Operation.read, - fd: fileDescriptor, - buffer: buffer, - offset: offset - ) - fatalError() - } - - static func read( - fixedFile: Int // TODO: AsyncFileDescriptor - ) -> IORequest { - fatalError() - } - - static func write( - - ) -> IORequest { - fatalError() - } - - internal static func readWrite( - op: Operation, - fd: FileDescriptor, - buffer: UnsafeMutableRawBufferPointer, - offset: UInt64? = nil - ) -> IORequest { - var req = IORequest() - req.operation = op - req.fileDescriptor = fd - req.offset = offset - req.buffer = buffer - return req - } -} \ No newline at end of file diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 6945d8c0..0c2f1384 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,6 +1,8 @@ @_implementationOnly import CSystem -import Glibc -import Atomics +import struct CSystem.io_uring_sqe + +@_implementationOnly import Atomics +import Glibc // needed for mmap // XXX: this *really* shouldn't be here. oh well. extension UnsafeMutableRawPointer { @@ -10,7 +12,7 @@ extension UnsafeMutableRawPointer { } // all pointers in this struct reference kernel-visible memory -struct SQRing { +@usableFromInline struct SQRing { let kernelHead: UnsafeAtomic let kernelTail: UnsafeAtomic var userTail: UInt32 @@ -44,14 +46,90 @@ struct CQRing { let cqes: UnsafeBufferPointer } +internal class ResourceManager: @unchecked Sendable { + typealias Resource = T + let resourceList: UnsafeMutableBufferPointer + var freeList: [Int] + let mutex: Mutex + + init(_ res: UnsafeMutableBufferPointer) { + self.resourceList = res + self.freeList = [Int](resourceList.indices) + self.mutex = Mutex() + } + + func getResource() -> IOResource? { + self.mutex.lock() + defer { self.mutex.unlock() } + if let index = freeList.popLast() { + return IOResource( + rescource: resourceList[index], + index: index, + manager: self + ) + } else { + return nil + } + } + + func releaseResource(index: Int) { + self.mutex.lock() + defer { self.mutex.unlock() } + self.freeList.append(index) + } +} + +public class IOResource { + typealias Resource = T + @usableFromInline let resource: T + @usableFromInline let index: Int + let manager: ResourceManager + + internal init( + rescource: T, + index: Int, + manager: ResourceManager + ) { + self.resource = rescource + self.index = index + self.manager = manager + } + + func withResource() { + + } + + deinit { + self.manager.releaseResource(index: self.index) + } +} + +public typealias IORingFileSlot = IOResource +public typealias IORingBuffer = IOResource + +extension IORingFileSlot { + public var unsafeFileSlot: Int { + return index + } +} +extension IORingBuffer { + public var unsafeBuffer: UnsafeMutableRawBufferPointer { + get { + return .init(start: resource.iov_base, count: resource.iov_len) + } + } +} + + + // XXX: This should be a non-copyable type (?) // demo only runs on Swift 5.8.1 -public final class IORing: Sendable { +public final class IORing: @unchecked Sendable { let ringFlags: UInt32 let ringDescriptor: Int32 - var submissionRing: SQRing - var submissionMutex: Mutex + @usableFromInline var submissionRing: SQRing + @usableFromInline var submissionMutex: Mutex // FEAT: set this eventually let submissionPolling: Bool = false @@ -59,13 +137,14 @@ public final class IORing: Sendable { var completionMutex: Mutex let submissionQueueEntries: UnsafeMutableBufferPointer - - var registeredFiles: UnsafeMutableBufferPointer? // kept around for unmap / cleanup let ringSize: Int let ringPtr: UnsafeMutableRawPointer + var registeredFiles: ResourceManager? + var registeredBuffers: ResourceManager? + public init(queueDepth: UInt32) throws { var params = io_uring_params() @@ -77,7 +156,7 @@ public final class IORing: Sendable { || params.features & IORING_FEAT_NODROP == 0) { close(ringDescriptor) // TODO: error handling - fatalError("kernel not new enough") + throw IORingError.missingRequiredFeatures } if (ringDescriptor < 0) { @@ -104,14 +183,9 @@ public final class IORing: Sendable { if (ringPtr == MAP_FAILED) { perror("mmap"); // TODO: error handling - fatalError() + fatalError("mmap failed in ring setup") } - let kernelHead = UnsafeAtomic(at: - ringPtr.advanced(by: params.sq_off.head) - .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) - ) - submissionRing = SQRing( kernelHead: UnsafeAtomic( at: ringPtr.advanced(by: params.sq_off.head) @@ -154,7 +228,7 @@ public final class IORing: Sendable { if (sqes == MAP_FAILED) { perror("mmap"); // TODO: error handling - fatalError() + fatalError("sqe mmap failed in ring setup") } submissionQueueEntries = UnsafeMutableBufferPointer( @@ -195,16 +269,29 @@ public final class IORing: Sendable { if let completion = _tryConsumeCompletion() { return completion } else { - _waitForCompletion() + while true { + let res = io_uring_enter(ringDescriptor, 0, 1, IORING_ENTER_GETEVENTS, nil) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + // EBUSY (not enough space for events; implies events filled + // by kernel between kernelTail load and now) + if res >= 0 || res == -EBUSY { + break + } else if res == -EAGAIN || res == -EINTR { + continue + } + fatalError("fatal error in receiving requests: " + + Errno(rawValue: -res).debugDescription + ) + } return _tryConsumeCompletion().unsafelyUnwrapped } } - func _waitForCompletion() { - // TODO: error handling - io_uring_enter(ringDescriptor, 0, 1, IORING_ENTER_GETEVENTS, nil) - } - func tryConsumeCompletion() -> IOCompletion? { self.completionMutex.lock() defer { self.completionMutex.unlock() } @@ -213,7 +300,7 @@ public final class IORing: Sendable { func _tryConsumeCompletion() -> IOCompletion? { let tail = completionRing.kernelTail.load(ordering: .acquiring) - var head = completionRing.kernelHead.load(ordering: .relaxed) + let head = completionRing.kernelHead.load(ordering: .relaxed) if tail != head { // 32 byte copy - oh well @@ -227,7 +314,6 @@ public final class IORing: Sendable { func registerFiles(count: UInt32) { - // TODO: implement guard self.registeredFiles == nil else { fatalError() } let fileBuf = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) fileBuf.initialize(repeating: UInt32.max) @@ -238,31 +324,43 @@ public final class IORing: Sendable { count ) // TODO: error handling - self.registeredFiles = fileBuf + self.registeredFiles = ResourceManager(fileBuf) } func unregisterFiles() { - if self.registeredFiles != nil { - io_uring_register( - self.ringDescriptor, - IORING_UNREGISTER_FILES, - self.registeredFiles!.baseAddress!, - UInt32(self.registeredFiles!.count) - ) - // TODO: error handling - self.registeredFiles!.deallocate() - self.registeredFiles = nil - } + fatalError("failed to unregister files") + } + + func getFile() -> IORingFileSlot? { + return self.registeredFiles?.getResource() } // register a group of buffers func registerBuffers(bufSize: UInt32, count: UInt32) { - // + let iovecs = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) + let intBufSize = Int(bufSize) + for i in 0.. IORingBuffer? { + return self.registeredBuffers?.getResource() } - func getBuffer() -> (index: Int, buf: UnsafeRawBufferPointer) { - fatalError() + func unregisterBuffers() { + fatalError("failed to unregister buffers: TODO") } // TODO: types @@ -277,9 +375,22 @@ public final class IORing: Sendable { // Ring always needs enter right now; // TODO: support SQPOLL here - - let ret = io_uring_enter(ringDescriptor, flushedEvents, 0, 0, nil) - // TODO: handle errors + while true { + let ret = io_uring_enter(ringDescriptor, flushedEvents, 0, 0, nil) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + if ret == -EAGAIN || ret == -EINTR { + continue + } else if ret < 0 { + fatalError("fatal error in submitting requests: " + + Errno(rawValue: -ret).debugDescription + ) + } + } } internal func _flushQueue() -> UInt32 { @@ -291,20 +402,28 @@ public final class IORing: Sendable { } + @inlinable @inline(__always) func writeRequest(_ request: __owned IORequest) -> Bool { self.submissionMutex.lock() defer { self.submissionMutex.unlock() } - return _writeRequest(request) + return _writeRequest(request.makeRawRequest()) } - internal func _writeRequest(_ request: __owned IORequest) -> Bool { - if let entry = _getSubmissionEntry() { - entry.pointee = request.rawValue - return true - } - return false + @inlinable @inline(__always) + func writeAndSubmit(_ request: __owned IORequest) -> Bool { + self.submissionMutex.lock() + defer { self.submissionMutex.unlock() } + return _writeRequest(request.makeRawRequest()) + } + + @inlinable @inline(__always) + internal func _writeRequest(_ request: __owned RawIORequest) -> Bool { + let entry = _blockingGetSubmissionEntry() + entry.pointee = request.rawValue + return true } + @inlinable @inline(__always) internal func _blockingGetSubmissionEntry() -> UnsafeMutablePointer { while true { if let entry = _getSubmissionEntry() { @@ -315,6 +434,7 @@ public final class IORing: Sendable { } + @usableFromInline @inline(__always) internal func _getSubmissionEntry() -> UnsafeMutablePointer? { let next = self.submissionRing.userTail + 1 diff --git a/Sources/System/IORingBuffer.swift b/Sources/System/IORingBuffer.swift deleted file mode 100644 index e69de29b..00000000 diff --git a/Sources/System/IORingError.swift b/Sources/System/IORingError.swift new file mode 100644 index 00000000..d87b2938 --- /dev/null +++ b/Sources/System/IORingError.swift @@ -0,0 +1,3 @@ +enum IORingError: Error { + case missingRequiredFeatures +} diff --git a/Sources/System/IORingFileSlot.swift b/Sources/System/IORingFileSlot.swift deleted file mode 100644 index d0a7c666..00000000 --- a/Sources/System/IORingFileSlot.swift +++ /dev/null @@ -1,8 +0,0 @@ -class IORingFileSlot { - - - deinit { - // return file slot - - } -} \ No newline at end of file diff --git a/Sources/System/Lock.swift b/Sources/System/Lock.swift index c5204ba0..fd20c641 100644 --- a/Sources/System/Lock.swift +++ b/Sources/System/Lock.swift @@ -1,7 +1,7 @@ // TODO: write against kernel APIs directly? import Glibc -public final class Mutex { +@usableFromInline final class Mutex { @usableFromInline let mutex: UnsafeMutablePointer @inlinable init() { diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 32c84faf..580eacc1 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -3,6 +3,8 @@ final public class ManagedIORing: @unchecked Sendable { init(queueDepth: UInt32) throws { self.internalRing = try IORing(queueDepth: queueDepth) + self.internalRing.registerBuffers(bufSize: 655336, count: 4) + self.internalRing.registerFiles(count: 32) self.startWaiter() } @@ -22,12 +24,19 @@ final public class ManagedIORing: @unchecked Sendable { self.internalRing.submissionMutex.lock() return await withUnsafeContinuation { cont in let entry = internalRing._blockingGetSubmissionEntry() - entry.pointee = request.rawValue + entry.pointee = request.makeRawRequest().rawValue entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) self.internalRing._submitRequests() self.internalRing.submissionMutex.unlock() } } + internal func getFileSlot() -> IORingFileSlot? { + self.internalRing.getFile() + } + + internal func getBuffer() -> IORingBuffer? { + self.internalRing.getBuffer() + } } \ No newline at end of file diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift new file mode 100644 index 00000000..fa50b439 --- /dev/null +++ b/Sources/System/RawIORequest.swift @@ -0,0 +1,139 @@ +// TODO: investigate @usableFromInline / @_implementationOnly dichotomy +@_implementationOnly import CSystem +import struct CSystem.io_uring_sqe + +public struct RawIORequest { + @usableFromInline var rawValue: io_uring_sqe + + public init() { + self.rawValue = io_uring_sqe() + } +} + +extension RawIORequest { + public enum Operation: UInt8 { + case nop = 0 + case readv = 1 + case writev = 2 + case fsync = 3 + case readFixed = 4 + case writeFixed = 5 + case pollAdd = 6 + case pollRemove = 7 + case syncFileRange = 8 + case sendMessage = 9 + case receiveMessage = 10 + // ... + case openAt = 18 + case read = 22 + case write = 23 + case openAt2 = 28 + + } + + public struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt8 + + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let fixedFile = Flags(rawValue: 1 << 0) + public static let drainQueue = Flags(rawValue: 1 << 1) + public static let linkRequest = Flags(rawValue: 1 << 2) + public static let hardlinkRequest = Flags(rawValue: 1 << 3) + public static let asynchronous = Flags(rawValue: 1 << 4) + public static let selectBuffer = Flags(rawValue: 1 << 5) + public static let skipSuccess = Flags(rawValue: 1 << 6) + } + + public var operation: Operation { + get { Operation(rawValue: rawValue.opcode)! } + set { rawValue.opcode = newValue.rawValue } + } + + public var flags: Flags { + get { Flags(rawValue: rawValue.flags) } + set { rawValue.flags = newValue.rawValue } + } + + public var fileDescriptor: FileDescriptor { + get { FileDescriptor(rawValue: rawValue.fd) } + set { rawValue.fd = newValue.rawValue } + } + + public var offset: UInt64? { + get { + if (rawValue.off == UInt64.max) { + return nil + } else { + return rawValue.off + } + } + set { + if let val = newValue { + rawValue.off = val + } else { + rawValue.off = UInt64.max + } + } + } + + public var buffer: UnsafeMutableRawBufferPointer { + get { + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(exactly: rawValue.addr)!) + return UnsafeMutableRawBufferPointer(start: ptr, count: Int(rawValue.len)) + } + + set { + // TODO: cleanup? + rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) + rawValue.len = UInt32(exactly: newValue.count)! + } + } + + public enum RequestFlags { + case readWriteFlags(ReadWriteFlags) + // case fsyncFlags(FsyncFlags?) + // poll_events + // poll32_events + // sync_range_flags + // msg_flags + // timeout_flags + // accept_flags + // cancel_flags + case openFlags(FileDescriptor.OpenOptions) + // statx_flags + // fadvise_advice + // splice_flags + } + + public struct ReadWriteFlags: OptionSet, Hashable, Codable { + public var rawValue: UInt32 + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let highPriority = ReadWriteFlags(rawValue: 1 << 0) + + // sync with only data integrity + public static let dataSync = ReadWriteFlags(rawValue: 1 << 1) + + // sync with full data + file integrity + public static let fileSync = ReadWriteFlags(rawValue: 1 << 2) + + // return -EAGAIN if operation blocks + public static let noWait = ReadWriteFlags(rawValue: 1 << 3) + + // append to end of the file + public static let append = ReadWriteFlags(rawValue: 1 << 4) + } +} + +extension RawIORequest { + static func nop() -> RawIORequest { + var req = RawIORequest() + req.operation = .nop + return req + } +} \ No newline at end of file From 996e940942f976f7a2de8751375c21413812ac41 Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Mon, 31 Jul 2023 22:28:54 -0700 Subject: [PATCH 003/172] initial AsyncFileDescriptor work --- Sources/System/AsyncFileDescriptor.swift | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 Sources/System/AsyncFileDescriptor.swift diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift new file mode 100644 index 00000000..97427812 --- /dev/null +++ b/Sources/System/AsyncFileDescriptor.swift @@ -0,0 +1,71 @@ +@_implementationOnly import CSystem + +public class AsyncFileDescriptor { + var open: Bool = true + @usableFromInline let fileSlot: IORingFileSlot + @usableFromInline let ring: ManagedIORing + + static func openat( + atDirectory: FileDescriptor = FileDescriptor(rawValue: AT_FDCWD), + path: FilePath, + _ mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + onRing ring: ManagedIORing + ) async throws -> AsyncFileDescriptor { + // todo; real error type + guard let fileSlot = ring.getFileSlot() else { + throw IORingError.missingRequiredFeatures + } + let cstr = path.withCString { + return $0 // bad + } + let res = await ring.submitAndWait(.openat( + atDirectory: atDirectory, + path: cstr, + mode, + options: options, + permissions: permissions, intoSlot: fileSlot + )) + if res.result < 0 { + throw Errno(rawValue: -res.result) + } + + return AsyncFileDescriptor( + fileSlot, ring: ring + ) + } + + internal init(_ fileSlot: IORingFileSlot, ring: ManagedIORing) { + self.fileSlot = fileSlot + self.ring = ring + } + + func close() async throws { + self.open = false + fatalError() + } + + @inlinable @inline(__always) @_unsafeInheritExecutor + func read( + into buffer: IORequest.Buffer, + atAbsoluteOffset offset: UInt64 = UInt64.max + ) async throws -> UInt32 { + let res = await ring.submitAndWait(.read( + file: .registered(self.fileSlot), + buffer: buffer, + offset: offset + )) + if res.result < 0 { + throw Errno(rawValue: -res.result) + } else { + return UInt32(bitPattern: res.result) + } + } + + deinit { + if (self.open) { + // TODO: close + } + } +} From 1f821a8539af62aab858f356ab9aa21592742ccb Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Tue, 8 Aug 2023 09:44:32 -0700 Subject: [PATCH 004/172] AsyncSequence draft implementation --- Sources/System/AsyncFileDescriptor.swift | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index 97427812..6e39e055 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -69,3 +69,55 @@ public class AsyncFileDescriptor { } } } + +extension AsyncFileDescriptor: AsyncSequence { + public func makeAsyncIterator() -> FileIterator { + return .init(self) + } + + public typealias AsyncIterator = FileIterator + public typealias Element = UInt8 +} + +public struct FileIterator: AsyncIteratorProtocol { + @usableFromInline let file: AsyncFileDescriptor + @usableFromInline var buffer: IORingBuffer + @usableFromInline var done: Bool + + @usableFromInline internal var currentByte: UnsafeRawPointer? + @usableFromInline internal var lastByte: UnsafeRawPointer? + + init(_ file: AsyncFileDescriptor) { + self.file = file + self.buffer = file.ring.getBuffer()! + self.done = false + } + + @inlinable @inline(__always) + public mutating func nextBuffer() async throws { + let buffer = self.buffer + + let bytesRead = try await file.read(into: .registered(buffer)) + if _fastPath(bytesRead != 0) { + let bufPointer = buffer.unsafeBuffer.baseAddress.unsafelyUnwrapped + self.currentByte = UnsafeRawPointer(bufPointer) + self.lastByte = UnsafeRawPointer(bufPointer.advanced(by: Int(bytesRead))) + } else { + self.done = true + } + } + + @inlinable @inline(__always) @_unsafeInheritExecutor + public mutating func next() async throws -> UInt8? { + if _fastPath(currentByte != lastByte) { + // SAFETY: both pointers should be non-nil if they're not equal + let byte = currentByte.unsafelyUnwrapped.load(as: UInt8.self) + currentByte = currentByte.unsafelyUnwrapped + 1 + return byte + } else if done { + return nil + } + try await nextBuffer() + return try await next() + } +} From 0762f57d806df3f5ad9e268d18eddbe4f3cb2594 Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Thu, 10 Aug 2023 16:03:21 -0700 Subject: [PATCH 005/172] migrate CSystem to systemLibrary for some reason, the linker on my linux machine fails to link the tests otherwise. investigate / fix before merging. --- Package.swift | 5 ++--- Sources/CSystem/{include => }/CSystemLinux.h | 0 Sources/CSystem/{include => }/CSystemWindows.h | 0 Sources/CSystem/{include => }/io_uring.h | 5 +++++ Sources/CSystem/{include => }/module.modulemap | 0 Sources/CSystem/shims.c | 18 ------------------ 6 files changed, 7 insertions(+), 21 deletions(-) rename Sources/CSystem/{include => }/CSystemLinux.h (100%) rename Sources/CSystem/{include => }/CSystemWindows.h (100%) rename Sources/CSystem/{include => }/io_uring.h (94%) rename Sources/CSystem/{include => }/module.modulemap (100%) delete mode 100644 Sources/CSystem/shims.c diff --git a/Package.swift b/Package.swift index 60f1a562..1c058acf 100644 --- a/Package.swift +++ b/Package.swift @@ -21,9 +21,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-atomics", from: "1.1.0") ], targets: [ - .target( - name: "CSystem", - dependencies: []), + .systemLibrary( + name: "CSystem"), .target( name: "SystemPackage", dependencies: [ diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/CSystemLinux.h similarity index 100% rename from Sources/CSystem/include/CSystemLinux.h rename to Sources/CSystem/CSystemLinux.h diff --git a/Sources/CSystem/include/CSystemWindows.h b/Sources/CSystem/CSystemWindows.h similarity index 100% rename from Sources/CSystem/include/CSystemWindows.h rename to Sources/CSystem/CSystemWindows.h diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/io_uring.h similarity index 94% rename from Sources/CSystem/include/io_uring.h rename to Sources/CSystem/io_uring.h index 9e3d9fb3..5c05ed8b 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/io_uring.h @@ -5,6 +5,9 @@ #include #include +#ifndef SWIFT_IORING_C_WRAPPER +#define SWIFT_IORING_C_WRAPPER + #ifdef __alpha__ /* * alpha is the only exception, all other architectures @@ -48,3 +51,5 @@ int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, flags, sig, _NSIG / 8); } + +#endif diff --git a/Sources/CSystem/include/module.modulemap b/Sources/CSystem/module.modulemap similarity index 100% rename from Sources/CSystem/include/module.modulemap rename to Sources/CSystem/module.modulemap diff --git a/Sources/CSystem/shims.c b/Sources/CSystem/shims.c deleted file mode 100644 index f492a2ae..00000000 --- a/Sources/CSystem/shims.c +++ /dev/null @@ -1,18 +0,0 @@ -/* - 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 -*/ - -#ifdef __linux__ - -#include - -#endif - -#if defined(_WIN32) -#include -#endif From d099546787499e97973db394abb3de388e7e5305 Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Fri, 11 Aug 2023 09:06:17 -0700 Subject: [PATCH 006/172] fix access control --- Sources/System/AsyncFileDescriptor.swift | 14 +++++----- Sources/System/IORing.swift | 34 +++++++++--------------- Sources/System/ManagedIORing.swift | 2 +- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index 6e39e055..2143847f 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -1,12 +1,13 @@ @_implementationOnly import CSystem -public class AsyncFileDescriptor { - var open: Bool = true + +public final class AsyncFileDescriptor { + @usableFromInline var open: Bool = true @usableFromInline let fileSlot: IORingFileSlot @usableFromInline let ring: ManagedIORing - static func openat( - atDirectory: FileDescriptor = FileDescriptor(rawValue: AT_FDCWD), + public static func openat( + atDirectory: FileDescriptor = FileDescriptor(rawValue: -100), path: FilePath, _ mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), @@ -25,7 +26,8 @@ public class AsyncFileDescriptor { path: cstr, mode, options: options, - permissions: permissions, intoSlot: fileSlot + permissions: permissions, + intoSlot: fileSlot )) if res.result < 0 { throw Errno(rawValue: -res.result) @@ -47,7 +49,7 @@ public class AsyncFileDescriptor { } @inlinable @inline(__always) @_unsafeInheritExecutor - func read( + public func read( into buffer: IORequest.Buffer, atAbsoluteOffset offset: UInt64 = UInt64.max ) async throws -> UInt32 { diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 0c2f1384..f91dfc04 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -262,7 +262,7 @@ public final class IORing: @unchecked Sendable { self.ringFlags = params.flags } - func blockingConsumeCompletion() -> IOCompletion { + public func blockingConsumeCompletion() -> IOCompletion { self.completionMutex.lock() defer { self.completionMutex.unlock() } @@ -292,7 +292,7 @@ public final class IORing: @unchecked Sendable { } } - func tryConsumeCompletion() -> IOCompletion? { + public func tryConsumeCompletion() -> IOCompletion? { self.completionMutex.lock() defer { self.completionMutex.unlock() } return _tryConsumeCompletion() @@ -312,8 +312,7 @@ public final class IORing: @unchecked Sendable { return nil } - - func registerFiles(count: UInt32) { + public func registerFiles(count: UInt32) { guard self.registeredFiles == nil else { fatalError() } let fileBuf = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) fileBuf.initialize(repeating: UInt32.max) @@ -327,16 +326,15 @@ public final class IORing: @unchecked Sendable { self.registeredFiles = ResourceManager(fileBuf) } - func unregisterFiles() { + public func unregisterFiles() { fatalError("failed to unregister files") } - func getFile() -> IORingFileSlot? { + public func getFile() -> IORingFileSlot? { return self.registeredFiles?.getResource() } - // register a group of buffers - func registerBuffers(bufSize: UInt32, count: UInt32) { + public func registerBuffers(bufSize: UInt32, count: UInt32) { let iovecs = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) let intBufSize = Int(bufSize) for i in 0.. IORingBuffer? { + public func getBuffer() -> IORingBuffer? { return self.registeredBuffers?.getResource() } - func unregisterBuffers() { + public func unregisterBuffers() { fatalError("failed to unregister buffers: TODO") } - // TODO: types - func submitRequests() { + public func submitRequests() { self.submissionMutex.lock() defer { self.submissionMutex.unlock() } self._submitRequests() } - func _submitRequests() { + internal func _submitRequests() { let flushedEvents = _flushQueue() // Ring always needs enter right now; @@ -389,6 +386,8 @@ public final class IORing: @unchecked Sendable { fatalError("fatal error in submitting requests: " + Errno(rawValue: -ret).debugDescription ) + } else { + break } } } @@ -403,14 +402,7 @@ public final class IORing: @unchecked Sendable { @inlinable @inline(__always) - func writeRequest(_ request: __owned IORequest) -> Bool { - self.submissionMutex.lock() - defer { self.submissionMutex.unlock() } - return _writeRequest(request.makeRawRequest()) - } - - @inlinable @inline(__always) - func writeAndSubmit(_ request: __owned IORequest) -> Bool { + public func writeRequest(_ request: __owned IORequest) -> Bool { self.submissionMutex.lock() defer { self.submissionMutex.unlock() } return _writeRequest(request.makeRawRequest()) diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 580eacc1..7f5bec22 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -1,7 +1,7 @@ final public class ManagedIORing: @unchecked Sendable { var internalRing: IORing - init(queueDepth: UInt32) throws { + public init(queueDepth: UInt32) throws { self.internalRing = try IORing(queueDepth: queueDepth) self.internalRing.registerBuffers(bufSize: 655336, count: 4) self.internalRing.registerFiles(count: 32) From b58450413e04141a7fa4e32b7cb68b3fa90809e1 Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Fri, 11 Aug 2023 09:07:08 -0700 Subject: [PATCH 007/172] fix off-by-one in IORequest.openat --- Sources/System/IORequest.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 6b26a4d1..39d7cdb7 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -46,7 +46,9 @@ extension IORequest { request.rawValue.addr = unsafeBitCast(path, to: UInt64.self) request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) request.rawValue.len = permissions?.rawValue ?? 0 - request.rawValue.file_index = UInt32(slot?.index ?? 0) + if let fileSlot = slot { + request.rawValue.file_index = UInt32(fileSlot.index + 1) + } case .read(let file, let buffer, let offset), .write(let file, let buffer, let offset): if case .read = self { if case .registered = buffer { From b596783bd9a5e9d34f99e06b3624c0bf9e243c7d Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Fri, 11 Aug 2023 09:07:47 -0700 Subject: [PATCH 008/172] implement closing --- Sources/System/AsyncFileDescriptor.swift | 12 +++++++++--- Sources/System/IORequest.swift | 9 +++++++++ Sources/System/RawIORequest.swift | 6 +++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index 2143847f..504aec9f 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -43,9 +43,15 @@ public final class AsyncFileDescriptor { self.ring = ring } - func close() async throws { + @inlinable @inline(__always) @_unsafeInheritExecutor + public func close() async throws { + let res = await ring.submitAndWait(.close( + .registered(self.fileSlot) + )) + if res.result < 0 { + throw Errno(rawValue: -res.result) + } self.open = false - fatalError() } @inlinable @inline(__always) @_unsafeInheritExecutor @@ -67,7 +73,7 @@ public final class AsyncFileDescriptor { deinit { if (self.open) { - // TODO: close + // TODO: close or error? TBD } } } diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 39d7cdb7..9548b3fb 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -20,6 +20,7 @@ public enum IORequest { buffer: Buffer, offset: UInt64 = 0 ) + case close(File) public enum Buffer { case registered(IORingBuffer) @@ -78,6 +79,14 @@ extension IORequest { request.buffer = buf } request.offset = offset + case .close(let file): + request.operation = .close + switch file { + case .registered(let regFile): + request.rawValue.file_index = UInt32(regFile.index + 1) + case .unregistered(let normalFile): + request.fileDescriptor = normalFile + } } return request } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index fa50b439..520cb85c 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -25,10 +25,14 @@ extension RawIORequest { case receiveMessage = 10 // ... case openAt = 18 + case close = 19 + case filesUpdate = 20 + case statx = 21 case read = 22 case write = 23 + // ... case openAt2 = 28 - + // ... } public struct Flags: OptionSet, Hashable, Codable { From 6b4084c69f8a691976d7fe4deeaa80e099649356 Mon Sep 17 00:00:00 2001 From: Lucy Satheesan Date: Fri, 11 Aug 2023 09:08:43 -0700 Subject: [PATCH 009/172] introduce IORing unit tests --- Tests/LinuxMain.swift | 8 -- .../AsyncFileDescriptorTests.swift | 40 ++++++ Tests/SystemTests/IORequestTests.swift | 68 +++++++++ Tests/SystemTests/IORingTests.swift | 21 +++ Tests/SystemTests/ManagedIORingTests.swift | 19 +++ Tests/SystemTests/XCTestManifests.swift | 132 ------------------ 6 files changed, 148 insertions(+), 140 deletions(-) delete mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/SystemTests/AsyncFileDescriptorTests.swift create mode 100644 Tests/SystemTests/IORequestTests.swift create mode 100644 Tests/SystemTests/IORingTests.swift create mode 100644 Tests/SystemTests/ManagedIORingTests.swift delete mode 100644 Tests/SystemTests/XCTestManifests.swift diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 695f4e5b..00000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,8 +0,0 @@ -import XCTest - -import SystemTests - -var tests = [XCTestCaseEntry]() -tests += SystemTests.__allTests() - -XCTMain(tests) diff --git a/Tests/SystemTests/AsyncFileDescriptorTests.swift b/Tests/SystemTests/AsyncFileDescriptorTests.swift new file mode 100644 index 00000000..0f1c103c --- /dev/null +++ b/Tests/SystemTests/AsyncFileDescriptorTests.swift @@ -0,0 +1,40 @@ +import XCTest + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +final class AsyncFileDescriptorTests: XCTestCase { + func testOpen() async throws { + let ring = try ManagedIORing(queueDepth: 32) + let file = try await AsyncFileDescriptor.openat( + path: "/dev/zero", + .readOnly, + onRing: ring + ) + } + + func testOpenClose() async throws { + let ring = try ManagedIORing(queueDepth: 32) + let file = try await AsyncFileDescriptor.openat( + path: "/dev/zero", + .readOnly, + onRing: ring + ) + await try file.close() + } + + func testDevNullEmpty() async throws { + let ring = try ManagedIORing(queueDepth: 32) + let file = try await AsyncFileDescriptor.openat( + path: "/dev/null", + .readOnly, + onRing: ring + ) + for try await _ in file { + XCTFail("/dev/null should be empty") + } + } +} diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift new file mode 100644 index 00000000..a44a607e --- /dev/null +++ b/Tests/SystemTests/IORequestTests.swift @@ -0,0 +1,68 @@ +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +import System +#endif + +func requestBytes(_ request: RawIORequest) -> [UInt8] { + return withUnsafePointer(to: request) { + let requestBuf = UnsafeBufferPointer(start: $0, count: 1) + let rawBytes = UnsafeRawBufferPointer(requestBuf) + return .init(rawBytes) + } +} + +// This test suite compares various IORequests bit-for-bit to IORequests +// that were generated with liburing or manually written out, +// which are known to work correctly. +final class IORequestTests: XCTestCase { + func testNop() { + let req = IORequest.nop.makeRawRequest() + let sourceBytes = requestBytes(req) + // convenient property of nop: it's all zeros! + // for some unknown reason, liburing sets the fd field to -1. + // we're not trying to be bug-compatible with it, so 0 *should* work. + XCTAssertEqual(sourceBytes, .init(repeating: 0, count: 64)) + } + + func testOpenatFixedFile() throws { + // TODO: come up with a better way of getting a FileSlot. + let buf = UnsafeMutableBufferPointer.allocate(capacity: 2) + let resmgr = ResourceManager.init(buf) + + let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! + let fileSlot = resmgr.getResource()! + let req = IORequest.openat( + atDirectory: FileDescriptor(rawValue: -100), + path: pathPtr, + .readOnly, + options: [], + permissions: nil, + intoSlot: fileSlot + ) + + let expectedRequest: [UInt8] = { + var bin = [UInt8].init(repeating: 0, count: 64) + bin[0] = 0x12 // opcode for the request + // bin[1] = 0 - no request flags + // bin[2...3] = 0 - padding + bin[4...7] = [0x9c, 0xff, 0xff, 0xff] // -100 in UInt32 - dirfd + // bin[8...15] = 0 - zeroes + withUnsafeBytes(of: pathPtr) { + // path pointer + bin[16...23] = ArraySlice($0) + } + // bin[24...43] = 0 - zeroes + withUnsafeBytes(of: UInt32(fileSlot.index + 1)) { + // file index + 1 - yes, unfortunately + bin[44...47] = ArraySlice($0) + } + return bin + }() + + let actualRequest = requestBytes(req.makeRawRequest()) + XCTAssertEqual(expectedRequest, actualRequest) + } +} diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift new file mode 100644 index 00000000..78baa984 --- /dev/null +++ b/Tests/SystemTests/IORingTests.swift @@ -0,0 +1,21 @@ +import XCTest + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +final class IORingTests: XCTestCase { + func testInit() throws { + let ring = try IORing(queueDepth: 32) + } + + func testNop() throws { + let ring = try IORing(queueDepth: 32) + ring.writeRequest(.nop) + ring.submitRequests() + let completion = ring.blockingConsumeCompletion() + XCTAssertEqual(completion.result, 0) + } +} diff --git a/Tests/SystemTests/ManagedIORingTests.swift b/Tests/SystemTests/ManagedIORingTests.swift new file mode 100644 index 00000000..e7ad3f59 --- /dev/null +++ b/Tests/SystemTests/ManagedIORingTests.swift @@ -0,0 +1,19 @@ +import XCTest + +#if SYSTEM_PACKAGE +import SystemPackage +#else +import System +#endif + +final class ManagedIORingTests: XCTestCase { + func testInit() throws { + let ring = try ManagedIORing(queueDepth: 32) + } + + func testNop() async throws { + let ring = try ManagedIORing(queueDepth: 32) + let completion = await ring.submitAndWait(.nop) + XCTAssertEqual(completion.result, 0) + } +} diff --git a/Tests/SystemTests/XCTestManifests.swift b/Tests/SystemTests/XCTestManifests.swift deleted file mode 100644 index de99bd81..00000000 --- a/Tests/SystemTests/XCTestManifests.swift +++ /dev/null @@ -1,132 +0,0 @@ -#if !canImport(ObjectiveC) && swift(<5.5) -import XCTest - -extension ErrnoTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__ErrnoTest = [ - ("testConstants", testConstants), - ("testPatternMatching", testPatternMatching), - ] -} - -extension FileDescriptorTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FileDescriptorTest = [ - ("testConstants", testConstants), - ("testStandardDescriptors", testStandardDescriptors), - ] -} - -extension FileOperationsTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FileOperationsTest = [ - ("testAdHocOpen", testAdHocOpen), - ("testAdHocPipe", testAdHocPipe), - ("testGithubIssues", testGithubIssues), - ("testHelpers", testHelpers), - ("testSyscalls", testSyscalls), - ] -} - -extension FilePathComponentsTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePathComponentsTest = [ - ("testAdHocRRC", testAdHocRRC), - ("testCases", testCases), - ("testSeparatorNormalization", testSeparatorNormalization), - ] -} - -extension FilePathParsingTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePathParsingTest = [ - ("testNormalization", testNormalization), - ] -} - -extension FilePathSyntaxTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePathSyntaxTest = [ - ("testAdHocMutations", testAdHocMutations), - ("testFailableStringInitializers", testFailableStringInitializers), - ("testLexicallyRelative", testLexicallyRelative), - ("testPartialWindowsRoots", testPartialWindowsRoots), - ("testPathSyntax", testPathSyntax), - ("testPrefixSuffix", testPrefixSuffix), - ] -} - -extension FilePathTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePathTest = [ - ("testFilePath", testFilePath), - ] -} - -extension FilePermissionsTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__FilePermissionsTest = [ - ("testPermissions", testPermissions), - ] -} - -extension MockingTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__MockingTest = [ - ("testMocking", testMocking), - ] -} - -extension SystemCharTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__SystemCharTest = [ - ("testIsLetter", testIsLetter), - ] -} - -extension SystemStringTest { - // DO NOT MODIFY: This is autogenerated, use: - // `swift test --generate-linuxmain` - // to regenerate. - static let __allTests__SystemStringTest = [ - ("testAdHoc", testAdHoc), - ("testPlatformString", testPlatformString), - ] -} - -public func __allTests() -> [XCTestCaseEntry] { - return [ - testCase(ErrnoTest.__allTests__ErrnoTest), - testCase(FileDescriptorTest.__allTests__FileDescriptorTest), - testCase(FileOperationsTest.__allTests__FileOperationsTest), - testCase(FilePathComponentsTest.__allTests__FilePathComponentsTest), - testCase(FilePathParsingTest.__allTests__FilePathParsingTest), - testCase(FilePathSyntaxTest.__allTests__FilePathSyntaxTest), - testCase(FilePathTest.__allTests__FilePathTest), - testCase(FilePermissionsTest.__allTests__FilePermissionsTest), - testCase(MockingTest.__allTests__MockingTest), - testCase(SystemCharTest.__allTests__SystemCharTest), - testCase(SystemStringTest.__allTests__SystemStringTest), - ] -} -#endif From baab9b26b77bca2622e7f3741ceddc62a51e00fe Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 24 Oct 2024 21:21:07 +0000 Subject: [PATCH 010/172] Starting to move to noncopyable structs, and away from swift-atomics --- Sources/System/IORing.swift | 72 ++++++++++++++--------------- Tests/SystemTests/IORingTests.swift | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index f91dfc04..0ac783af 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,7 +1,7 @@ @_implementationOnly import CSystem import struct CSystem.io_uring_sqe -@_implementationOnly import Atomics +@_implementationOnly import Synchronization import Glibc // needed for mmap // XXX: this *really* shouldn't be here. oh well. @@ -12,9 +12,9 @@ extension UnsafeMutableRawPointer { } // all pointers in this struct reference kernel-visible memory -@usableFromInline struct SQRing { - let kernelHead: UnsafeAtomic - let kernelTail: UnsafeAtomic +@usableFromInline struct SQRing: ~Copyable { + let kernelHead: UnsafePointer> + let kernelTail: UnsafePointer> var userTail: UInt32 // from liburing: the kernel should never change these @@ -25,7 +25,7 @@ extension UnsafeMutableRawPointer { // ring flags bitfield // currently used by the kernel only in SQPOLL mode to indicate // when the polling thread needs to be woken up - let flags: UnsafeAtomic + let flags: UnsafePointer> // ring array // maps indexes between the actual ring and the submissionQueueEntries list, @@ -34,9 +34,9 @@ extension UnsafeMutableRawPointer { let array: UnsafeMutableBufferPointer } -struct CQRing { - let kernelHead: UnsafeAtomic - let kernelTail: UnsafeAtomic +struct CQRing: ~Copyable { + let kernelHead: UnsafePointer> + let kernelTail: UnsafePointer> // TODO: determine if this is actually used var userHead: UInt32 @@ -124,7 +124,7 @@ extension IORingBuffer { // XXX: This should be a non-copyable type (?) // demo only runs on Swift 5.8.1 -public final class IORing: @unchecked Sendable { +public struct IORing: @unchecked Sendable, ~Copyable { let ringFlags: UInt32 let ringDescriptor: Int32 @@ -187,20 +187,20 @@ public final class IORing: @unchecked Sendable { } submissionRing = SQRing( - kernelHead: UnsafeAtomic( - at: ringPtr.advanced(by: params.sq_off.head) - .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + kernelHead: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.head) + .assumingMemoryBound(to: Atomic.self) ), - kernelTail: UnsafeAtomic( - at: ringPtr.advanced(by: params.sq_off.tail) - .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + kernelTail: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.tail) + .assumingMemoryBound(to: Atomic.self) ), userTail: 0, // no requests yet ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, - flags: UnsafeAtomic( - at: ringPtr.advanced(by: params.sq_off.flags) - .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + flags: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.flags) + .assumingMemoryBound(to: Atomic.self) ), array: UnsafeMutableBufferPointer( start: ringPtr.advanced(by: params.sq_off.array) @@ -237,13 +237,13 @@ public final class IORing: @unchecked Sendable { ) completionRing = CQRing( - kernelHead: UnsafeAtomic( - at: ringPtr.advanced(by: params.cq_off.head) - .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + kernelHead: UnsafePointer>( + ringPtr.advanced(by: params.cq_off.head) + .assumingMemoryBound(to: Atomic.self) ), - kernelTail: UnsafeAtomic( - at: ringPtr.advanced(by: params.cq_off.tail) - .assumingMemoryBound(to: UInt32.AtomicRepresentation.self) + kernelTail: UnsafePointer>( + ringPtr.advanced(by: params.cq_off.tail) + .assumingMemoryBound(to: Atomic.self) ), userHead: 0, // no completions yet ringMask: ringPtr.advanced(by: params.cq_off.ring_mask) @@ -299,20 +299,20 @@ public final class IORing: @unchecked Sendable { } func _tryConsumeCompletion() -> IOCompletion? { - let tail = completionRing.kernelTail.load(ordering: .acquiring) - let head = completionRing.kernelHead.load(ordering: .relaxed) + let tail = completionRing.kernelTail.pointee.load(ordering: .acquiring) + let head = completionRing.kernelHead.pointee.load(ordering: .relaxed) if tail != head { // 32 byte copy - oh well let res = completionRing.cqes[Int(head & completionRing.ringMask)] - completionRing.kernelHead.store(head + 1, ordering: .relaxed) + completionRing.kernelHead.pointee.store(head + 1, ordering: .relaxed) return IOCompletion(rawValue: res) } return nil } - public func registerFiles(count: UInt32) { + public mutating func registerFiles(count: UInt32) { guard self.registeredFiles == nil else { fatalError() } let fileBuf = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) fileBuf.initialize(repeating: UInt32.max) @@ -334,7 +334,7 @@ public final class IORing: @unchecked Sendable { return self.registeredFiles?.getResource() } - public func registerBuffers(bufSize: UInt32, count: UInt32) { + public mutating func registerBuffers(bufSize: UInt32, count: UInt32) { let iovecs = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) let intBufSize = Int(bufSize) for i in 0.. UInt32 { - self.submissionRing.kernelTail.store( + self.submissionRing.kernelTail.pointee.store( self.submissionRing.userTail, ordering: .relaxed ) return self.submissionRing.userTail - - self.submissionRing.kernelHead.load(ordering: .relaxed) + self.submissionRing.kernelHead.pointee.load(ordering: .relaxed) } @inlinable @inline(__always) - public func writeRequest(_ request: __owned IORequest) -> Bool { + public mutating func writeRequest(_ request: __owned IORequest) -> Bool { self.submissionMutex.lock() defer { self.submissionMutex.unlock() } return _writeRequest(request.makeRawRequest()) } @inlinable @inline(__always) - internal func _writeRequest(_ request: __owned RawIORequest) -> Bool { + internal mutating func _writeRequest(_ request: __owned RawIORequest) -> Bool { let entry = _blockingGetSubmissionEntry() entry.pointee = request.rawValue return true } @inlinable @inline(__always) - internal func _blockingGetSubmissionEntry() -> UnsafeMutablePointer { + internal mutating func _blockingGetSubmissionEntry() -> UnsafeMutablePointer { while true { if let entry = _getSubmissionEntry() { return entry @@ -427,11 +427,11 @@ public final class IORing: @unchecked Sendable { } @usableFromInline @inline(__always) - internal func _getSubmissionEntry() -> UnsafeMutablePointer? { + internal mutating func _getSubmissionEntry() -> UnsafeMutablePointer? { let next = self.submissionRing.userTail + 1 // FEAT: smp load when SQPOLL in use (not in MVP) - let kernelHead = self.submissionRing.kernelHead.load(ordering: .relaxed) + let kernelHead = self.submissionRing.kernelHead.pointee.load(ordering: .relaxed) // FEAT: 128-bit event support (not in MVP) if (next - kernelHead <= self.submissionRing.array.count) { diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 78baa984..4604f500 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -12,7 +12,7 @@ final class IORingTests: XCTestCase { } func testNop() throws { - let ring = try IORing(queueDepth: 32) + var ring = try IORing(queueDepth: 32) ring.writeRequest(.nop) ring.submitRequests() let completion = ring.blockingConsumeCompletion() From 60299362ed85e53915891a562b60d621afc608df Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 24 Oct 2024 21:27:22 +0000 Subject: [PATCH 011/172] One more noncopyable struct --- Sources/System/IORing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 0ac783af..656e1683 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -79,7 +79,7 @@ internal class ResourceManager: @unchecked Sendable { } } -public class IOResource { +public struct IOResource: ~Copyable { typealias Resource = T @usableFromInline let resource: T @usableFromInline let index: Int From 10c070abd648bb66da947c77b5a5c645ef1d53fd Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Oct 2024 20:00:07 +0000 Subject: [PATCH 012/172] WIP, more ~Copyable adoption --- Sources/System/IORequest.swift | 145 ++++--- Sources/System/IORing.swift | 368 +++++++++--------- Sources/System/Lock.swift | 37 -- Sources/System/ManagedIORing.swift | 25 +- .../AsyncFileDescriptorTests.swift | 2 +- 5 files changed, 295 insertions(+), 282 deletions(-) delete mode 100644 Sources/System/Lock.swift diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 9548b3fb..2ecd3f46 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,9 +1,9 @@ import struct CSystem.io_uring_sqe -public enum IORequest { +public enum IORequest: ~Copyable { case nop // nothing here case openat( - atDirectory: FileDescriptor, + atDirectory: FileDescriptor, path: UnsafePointer, FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), @@ -22,71 +22,106 @@ public enum IORequest { ) case close(File) - public enum Buffer { + public enum Buffer: ~Copyable { case registered(IORingBuffer) case unregistered(UnsafeMutableRawBufferPointer) } - public enum File { + public enum File: ~Copyable { case registered(IORingFileSlot) case unregistered(FileDescriptor) } } +@inlinable @inline(__always) +internal func makeRawRequest_readWrite_registered( + file: consuming IORequest.File, + buffer: consuming IORingBuffer, + offset: UInt64, + request: consuming RawIORequest +) -> RawIORequest { + switch file { + case .registered(let regFile): + request.rawValue.fd = Int32(exactly: regFile.index)! + request.flags = .fixedFile + case .unregistered(let fd): + request.fileDescriptor = fd + } + request.buffer = buffer.unsafeBuffer + request.rawValue.buf_index = UInt16(exactly: buffer.index)! + request.offset = offset + return request +} + +@inlinable @inline(__always) +internal func makeRawRequest_readWrite_unregistered( + file: consuming IORequest.File, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64, + request: consuming RawIORequest +) -> RawIORequest { + switch file { + case .registered(let regFile): + request.rawValue.fd = Int32(exactly: regFile.index)! + request.flags = .fixedFile + case .unregistered(let fd): + request.fileDescriptor = fd + } + request.buffer = buffer + request.offset = offset + return request +} + extension IORequest { @inlinable @inline(__always) - public func makeRawRequest() -> RawIORequest { + public consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() - switch self { - case .nop: - request.operation = .nop - case .openat(let atDirectory, let path, let mode, let options, let permissions, let slot): - // TODO: use rawValue less - request.operation = .openAt - request.fileDescriptor = atDirectory - request.rawValue.addr = unsafeBitCast(path, to: UInt64.self) - request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) - request.rawValue.len = permissions?.rawValue ?? 0 - if let fileSlot = slot { - request.rawValue.file_index = UInt32(fileSlot.index + 1) - } - case .read(let file, let buffer, let offset), .write(let file, let buffer, let offset): - if case .read = self { - if case .registered = buffer { - request.operation = .readFixed - } else { - request.operation = .read - } - } else { - if case .registered = buffer { - request.operation = .writeFixed - } else { - request.operation = .write - } - } - switch file { - case .registered(let regFile): - request.rawValue.fd = Int32(exactly: regFile.index)! - request.flags = .fixedFile - case .unregistered(let fd): - request.fileDescriptor = fd - } - switch buffer { - case .registered(let regBuf): - request.buffer = regBuf.unsafeBuffer - request.rawValue.buf_index = UInt16(exactly: regBuf.index)! - case .unregistered(let buf): - request.buffer = buf - } - request.offset = offset - case .close(let file): - request.operation = .close - switch file { - case .registered(let regFile): - request.rawValue.file_index = UInt32(regFile.index + 1) - case .unregistered(let normalFile): - request.fileDescriptor = normalFile - } + switch consume self { + case .nop: + request.operation = .nop + case .openat(let atDirectory, let path, let mode, let options, let permissions, let slot): + // TODO: use rawValue less + request.operation = .openAt + request.fileDescriptor = atDirectory + request.rawValue.addr = unsafeBitCast(path, to: UInt64.self) + request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) + request.rawValue.len = permissions?.rawValue ?? 0 + if let fileSlot = slot { + request.rawValue.file_index = UInt32(fileSlot.index + 1) + } + case .write(let file, let buffer, let offset): + switch consume buffer { + case .registered(let buffer): + request.operation = .writeFixed + return makeRawRequest_readWrite_registered( + file: file, buffer: buffer, offset: offset, request: request) + + case .unregistered(let buffer): + request.operation = .write + return makeRawRequest_readWrite_unregistered( + file: file, buffer: buffer, offset: offset, request: request) + } + case .read(let file, let buffer, let offset): + + switch consume buffer { + case .registered(let buffer): + request.operation = .readFixed + return makeRawRequest_readWrite_registered( + file: file, buffer: buffer, offset: offset, request: request) + + case .unregistered(let buffer): + request.operation = .read + return makeRawRequest_readWrite_unregistered( + file: file, buffer: buffer, offset: offset, request: request) + } + case .close(let file): + request.operation = .close + switch file { + case .registered(let regFile): + request.rawValue.file_index = UInt32(regFile.index + 1) + case .unregistered(let normalFile): + request.fileDescriptor = normalFile + } } return request } diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 656e1683..3e24408f 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,8 +1,8 @@ @_implementationOnly import CSystem -import struct CSystem.io_uring_sqe +import Glibc // needed for mmap +import Synchronization -@_implementationOnly import Synchronization -import Glibc // needed for mmap +import struct CSystem.io_uring_sqe // XXX: this *really* shouldn't be here. oh well. extension UnsafeMutableRawPointer { @@ -26,7 +26,7 @@ extension UnsafeMutableRawPointer { // currently used by the kernel only in SQPOLL mode to indicate // when the polling thread needs to be woken up let flags: UnsafePointer> - + // ring array // maps indexes between the actual ring and the submissionQueueEntries list, // allowing the latter to be used as a kind of freelist with enough work? @@ -48,34 +48,40 @@ struct CQRing: ~Copyable { internal class ResourceManager: @unchecked Sendable { typealias Resource = T - let resourceList: UnsafeMutableBufferPointer - var freeList: [Int] - let mutex: Mutex + + struct Resources { + let resourceList: UnsafeMutableBufferPointer + var freeList: [Int] + } + + let mutex: Mutex init(_ res: UnsafeMutableBufferPointer) { - self.resourceList = res - self.freeList = [Int](resourceList.indices) - self.mutex = Mutex() + mutex = Mutex( + Resources( + resourceList: res, + freeList: [Int](res.indices) + )) } func getResource() -> IOResource? { - self.mutex.lock() - defer { self.mutex.unlock() } - if let index = freeList.popLast() { - return IOResource( - rescource: resourceList[index], - index: index, - manager: self - ) - } else { - return nil + mutex.withLock { resources in + if let index = resources.freeList.popLast() { + return IOResource( + resource: resources.resourceList[index], + index: index, + manager: self + ) + } else { + return nil + } } } func releaseResource(index: Int) { - self.mutex.lock() - defer { self.mutex.unlock() } - self.freeList.append(index) + mutex.withLock { resources in + resources.freeList.append(index) + } } } @@ -86,11 +92,11 @@ public struct IOResource: ~Copyable { let manager: ResourceManager internal init( - rescource: T, + resource: T, index: Int, manager: ResourceManager ) { - self.resource = rescource + self.resource = resource self.index = index self.manager = manager } @@ -114,13 +120,89 @@ extension IORingFileSlot { } extension IORingBuffer { public var unsafeBuffer: UnsafeMutableRawBufferPointer { - get { - return .init(start: resource.iov_base, count: resource.iov_len) + return .init(start: resource.iov_base, count: resource.iov_len) + } +} + +@inlinable @inline(__always) +internal func _writeRequest(_ request: __owned RawIORequest, ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) + -> Bool +{ + let entry = _blockingGetSubmissionEntry(ring: &ring, submissionQueueEntries: submissionQueueEntries) + entry.pointee = request.rawValue + return true +} + +@inlinable @inline(__always) +internal func _blockingGetSubmissionEntry(ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) -> UnsafeMutablePointer< + io_uring_sqe +> { + while true { + if let entry = _getSubmissionEntry(ring: &ring, submissionQueueEntries: submissionQueueEntries) { + return entry } + // TODO: actually block here instead of spinning } + +} + +internal func _submitRequests(ring: inout SQRing, ringDescriptor: Int32) { + let flushedEvents = _flushQueue(ring: &ring) + + // Ring always needs enter right now; + // TODO: support SQPOLL here + while true { + let ret = io_uring_enter(ringDescriptor, flushedEvents, 0, 0, nil) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + if ret == -EAGAIN || ret == -EINTR { + continue + } else if ret < 0 { + fatalError( + "fatal error in submitting requests: " + Errno(rawValue: -ret).debugDescription + ) + } else { + break + } + } +} + +internal func _flushQueue(ring: inout SQRing) -> UInt32 { + ring.kernelTail.pointee.store( + ring.userTail, ordering: .relaxed + ) + return ring.userTail - ring.kernelHead.pointee.load(ordering: .relaxed) } +@usableFromInline @inline(__always) +internal func _getSubmissionEntry(ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) -> UnsafeMutablePointer< + io_uring_sqe +>? { + let next = ring.userTail + 1 + // FEAT: smp load when SQPOLL in use (not in MVP) + let kernelHead = ring.kernelHead.pointee.load(ordering: .relaxed) + + // FEAT: 128-bit event support (not in MVP) + if next - kernelHead <= ring.array.count { + // let sqe = &sq->sqes[(sq->sqe_tail & sq->ring_mask) << shift]; + let sqeIndex = Int( + ring.userTail & ring.ringMask + ) + + let sqe = submissionQueueEntries + .baseAddress.unsafelyUnwrapped + .advanced(by: sqeIndex) + + ring.userTail = next + return sqe + } + return nil +} // XXX: This should be a non-copyable type (?) // demo only runs on Swift 5.8.1 @@ -128,15 +210,13 @@ public struct IORing: @unchecked Sendable, ~Copyable { let ringFlags: UInt32 let ringDescriptor: Int32 - @usableFromInline var submissionRing: SQRing - @usableFromInline var submissionMutex: Mutex + @usableFromInline let submissionMutex: Mutex // FEAT: set this eventually let submissionPolling: Bool = false - var completionRing: CQRing - var completionMutex: Mutex + let completionMutex: Mutex - let submissionQueueEntries: UnsafeMutableBufferPointer + @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer // kept around for unmap / cleanup let ringSize: Int @@ -149,28 +229,31 @@ public struct IORing: @unchecked Sendable, ~Copyable { var params = io_uring_params() ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { - return io_uring_setup(queueDepth, $0); + return io_uring_setup(queueDepth, $0) } - if (params.features & IORING_FEAT_SINGLE_MMAP == 0 - || params.features & IORING_FEAT_NODROP == 0) { + if params.features & IORING_FEAT_SINGLE_MMAP == 0 + || params.features & IORING_FEAT_NODROP == 0 + { close(ringDescriptor) // TODO: error handling throw IORingError.missingRequiredFeatures } - - if (ringDescriptor < 0) { + + if ringDescriptor < 0 { // TODO: error handling } - let submitRingSize = params.sq_off.array + let submitRingSize = + params.sq_off.array + params.sq_entries * UInt32(MemoryLayout.size) - - let completionRingSize = params.cq_off.cqes + + let completionRingSize = + params.cq_off.cqes + params.cq_entries * UInt32(MemoryLayout.size) ringSize = Int(max(submitRingSize, completionRingSize)) - + ringPtr = mmap( /* addr: */ nil, /* len: */ ringSize, @@ -178,35 +261,36 @@ public struct IORing: @unchecked Sendable, ~Copyable { /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, /* offset: */ __off_t(IORING_OFF_SQ_RING) - ); + ) - if (ringPtr == MAP_FAILED) { - perror("mmap"); + if ringPtr == MAP_FAILED { + perror("mmap") // TODO: error handling fatalError("mmap failed in ring setup") } - submissionRing = SQRing( + let submissionRing = SQRing( kernelHead: UnsafePointer>( ringPtr.advanced(by: params.sq_off.head) - .assumingMemoryBound(to: Atomic.self) + .assumingMemoryBound(to: Atomic.self) ), kernelTail: UnsafePointer>( ringPtr.advanced(by: params.sq_off.tail) - .assumingMemoryBound(to: Atomic.self) + .assumingMemoryBound(to: Atomic.self) ), - userTail: 0, // no requests yet + userTail: 0, // no requests yet ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, flags: UnsafePointer>( ringPtr.advanced(by: params.sq_off.flags) - .assumingMemoryBound(to: Atomic.self) + .assumingMemoryBound(to: Atomic.self) ), array: UnsafeMutableBufferPointer( start: ringPtr.advanced(by: params.sq_off.array) .assumingMemoryBound(to: UInt32.self), - count: Int(ringPtr.advanced(by: params.sq_off.ring_entries) - .assumingMemoryBound(to: UInt32.self).pointee) + count: Int( + ringPtr.advanced(by: params.sq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) ) ) @@ -223,10 +307,10 @@ public struct IORing: @unchecked Sendable, ~Copyable { /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, /* offset: */ __off_t(IORING_OFF_SQES) - ); + ) - if (sqes == MAP_FAILED) { - perror("mmap"); + if sqes == MAP_FAILED { + perror("mmap") // TODO: error handling fatalError("sqe mmap failed in ring setup") } @@ -236,76 +320,77 @@ public struct IORing: @unchecked Sendable, ~Copyable { count: Int(params.sq_entries) ) - completionRing = CQRing( + let completionRing = CQRing( kernelHead: UnsafePointer>( ringPtr.advanced(by: params.cq_off.head) - .assumingMemoryBound(to: Atomic.self) + .assumingMemoryBound(to: Atomic.self) ), kernelTail: UnsafePointer>( ringPtr.advanced(by: params.cq_off.tail) - .assumingMemoryBound(to: Atomic.self) + .assumingMemoryBound(to: Atomic.self) ), - userHead: 0, // no completions yet + userHead: 0, // no completions yet ringMask: ringPtr.advanced(by: params.cq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, cqes: UnsafeBufferPointer( start: ringPtr.advanced(by: params.cq_off.cqes) .assumingMemoryBound(to: io_uring_cqe.self), - count: Int(ringPtr.advanced(by: params.cq_off.ring_entries) - .assumingMemoryBound(to: UInt32.self).pointee) + count: Int( + ringPtr.advanced(by: params.cq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) ) ) - self.submissionMutex = Mutex() - self.completionMutex = Mutex() + self.submissionMutex = Mutex(submissionRing) + self.completionMutex = Mutex(completionRing) self.ringFlags = params.flags } public func blockingConsumeCompletion() -> IOCompletion { - self.completionMutex.lock() - defer { self.completionMutex.unlock() } - - if let completion = _tryConsumeCompletion() { - return completion - } else { - while true { - let res = io_uring_enter(ringDescriptor, 0, 1, IORING_ENTER_GETEVENTS, nil) - // error handling: - // EAGAIN / EINTR (try again), - // EBADF / EBADFD / EOPNOTSUPP / ENXIO - // (failure in ring lifetime management, fatal), - // EINVAL (bad constant flag?, fatal), - // EFAULT (bad address for argument from library, fatal) - // EBUSY (not enough space for events; implies events filled - // by kernel between kernelTail load and now) - if res >= 0 || res == -EBUSY { - break - } else if res == -EAGAIN || res == -EINTR { - continue + completionMutex.withLock { ring in + if let completion = _tryConsumeCompletion(ring: &ring) { + return completion + } else { + while true { + let res = io_uring_enter(ringDescriptor, 0, 1, IORING_ENTER_GETEVENTS, nil) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + // EBUSY (not enough space for events; implies events filled + // by kernel between kernelTail load and now) + if res >= 0 || res == -EBUSY { + break + } else if res == -EAGAIN || res == -EINTR { + continue + } + fatalError( + "fatal error in receiving requests: " + + Errno(rawValue: -res).debugDescription + ) } - fatalError("fatal error in receiving requests: " + - Errno(rawValue: -res).debugDescription - ) + return _tryConsumeCompletion(ring: &ring).unsafelyUnwrapped } - return _tryConsumeCompletion().unsafelyUnwrapped } } public func tryConsumeCompletion() -> IOCompletion? { - self.completionMutex.lock() - defer { self.completionMutex.unlock() } - return _tryConsumeCompletion() + completionMutex.withLock { ring in + return _tryConsumeCompletion(ring: &ring) + } } - func _tryConsumeCompletion() -> IOCompletion? { - let tail = completionRing.kernelTail.pointee.load(ordering: .acquiring) - let head = completionRing.kernelHead.pointee.load(ordering: .relaxed) - + func _tryConsumeCompletion(ring: inout CQRing) -> IOCompletion? { + let tail = ring.kernelTail.pointee.load(ordering: .acquiring) + let head = ring.kernelHead.pointee.load(ordering: .relaxed) + if tail != head { // 32 byte copy - oh well - let res = completionRing.cqes[Int(head & completionRing.ringMask)] - completionRing.kernelHead.pointee.store(head + 1, ordering: .relaxed) + let res = ring.cqes[Int(head & ring.ringMask)] + ring.kernelHead.pointee.store(head + 1, ordering: .relaxed) return IOCompletion(rawValue: res) } @@ -340,7 +425,8 @@ public struct IORing: @unchecked Sendable, ~Copyable { for i in 0.. UInt32 { - self.submissionRing.kernelTail.pointee.store( - self.submissionRing.userTail, ordering: .relaxed - ) - return self.submissionRing.userTail - - self.submissionRing.kernelHead.pointee.load(ordering: .relaxed) - } - - @inlinable @inline(__always) public mutating func writeRequest(_ request: __owned IORequest) -> Bool { - self.submissionMutex.lock() - defer { self.submissionMutex.unlock() } - return _writeRequest(request.makeRawRequest()) - } - - @inlinable @inline(__always) - internal mutating func _writeRequest(_ request: __owned RawIORequest) -> Bool { - let entry = _blockingGetSubmissionEntry() - entry.pointee = request.rawValue - return true - } - - @inlinable @inline(__always) - internal mutating func _blockingGetSubmissionEntry() -> UnsafeMutablePointer { - while true { - if let entry = _getSubmissionEntry() { - return entry - } - // TODO: actually block here instead of spinning + let raw = request.makeRawRequest() + return submissionMutex.withLock { ring in + return _writeRequest(raw, ring: &ring, submissionQueueEntries: submissionQueueEntries) } - - } - - @usableFromInline @inline(__always) - internal mutating func _getSubmissionEntry() -> UnsafeMutablePointer? { - let next = self.submissionRing.userTail + 1 - - // FEAT: smp load when SQPOLL in use (not in MVP) - let kernelHead = self.submissionRing.kernelHead.pointee.load(ordering: .relaxed) - - // FEAT: 128-bit event support (not in MVP) - if (next - kernelHead <= self.submissionRing.array.count) { - // let sqe = &sq->sqes[(sq->sqe_tail & sq->ring_mask) << shift]; - let sqeIndex = Int( - self.submissionRing.userTail & self.submissionRing.ringMask - ) - - let sqe = self.submissionQueueEntries - .baseAddress.unsafelyUnwrapped - .advanced(by: sqeIndex) - - self.submissionRing.userTail = next; - return sqe - } - return nil } deinit { - munmap(ringPtr, ringSize); + munmap(ringPtr, ringSize) munmap( UnsafeMutableRawPointer(submissionQueueEntries.baseAddress!), submissionQueueEntries.count * MemoryLayout.size ) close(ringDescriptor) } -}; - +} diff --git a/Sources/System/Lock.swift b/Sources/System/Lock.swift deleted file mode 100644 index fd20c641..00000000 --- a/Sources/System/Lock.swift +++ /dev/null @@ -1,37 +0,0 @@ -// TODO: write against kernel APIs directly? -import Glibc - -@usableFromInline final class Mutex { - @usableFromInline let mutex: UnsafeMutablePointer - - @inlinable init() { - self.mutex = UnsafeMutablePointer.allocate(capacity: 1) - self.mutex.initialize(to: pthread_mutex_t()) - pthread_mutex_init(self.mutex, nil) - } - - @inlinable deinit { - defer { mutex.deallocate() } - guard pthread_mutex_destroy(mutex) == 0 else { - preconditionFailure("unable to destroy mutex") - } - } - - // XXX: this is because we need to lock the mutex in the context of a submit() function - // and unlock *before* the UnsafeContinuation returns. - // Code looks like: { - // // prepare request - // io_uring_get_sqe() - // io_uring_prep_foo(...) - // return await withUnsafeContinuation { - // sqe->user_data = ...; io_uring_submit(); unlock(); - // } - // } - @inlinable @inline(__always) public func lock() { - pthread_mutex_lock(mutex) - } - - @inlinable @inline(__always) public func unlock() { - pthread_mutex_unlock(mutex) - } -} diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 7f5bec22..88fb72e7 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -5,15 +5,16 @@ final public class ManagedIORing: @unchecked Sendable { self.internalRing = try IORing(queueDepth: queueDepth) self.internalRing.registerBuffers(bufSize: 655336, count: 4) self.internalRing.registerFiles(count: 32) - self.startWaiter() + self.startWaiter() } private func startWaiter() { Task.detached { - while (!Task.isCancelled) { + while !Task.isCancelled { let cqe = self.internalRing.blockingConsumeCompletion() - let cont = unsafeBitCast(cqe.userData, to: UnsafeContinuation.self) + let cont = unsafeBitCast( + cqe.userData, to: UnsafeContinuation.self) cont.resume(returning: cqe) } } @@ -21,14 +22,18 @@ final public class ManagedIORing: @unchecked Sendable { @_unsafeInheritExecutor public func submitAndWait(_ request: __owned IORequest) async -> IOCompletion { - self.internalRing.submissionMutex.lock() + var consumeOnceWorkaround: IORequest? = request return await withUnsafeContinuation { cont in - let entry = internalRing._blockingGetSubmissionEntry() - entry.pointee = request.makeRawRequest().rawValue - entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) - self.internalRing._submitRequests() - self.internalRing.submissionMutex.unlock() + return internalRing.submissionMutex.withLock { ring in + let request = consumeOnceWorkaround.take()! + let entry = _blockingGetSubmissionEntry( + ring: &ring, submissionQueueEntries: internalRing.submissionQueueEntries) + entry.pointee = request.makeRawRequest().rawValue + entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) + _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) + } } + } internal func getFileSlot() -> IORingFileSlot? { @@ -39,4 +44,4 @@ final public class ManagedIORing: @unchecked Sendable { self.internalRing.getBuffer() } -} \ No newline at end of file +} diff --git a/Tests/SystemTests/AsyncFileDescriptorTests.swift b/Tests/SystemTests/AsyncFileDescriptorTests.swift index 0f1c103c..1baba962 100644 --- a/Tests/SystemTests/AsyncFileDescriptorTests.swift +++ b/Tests/SystemTests/AsyncFileDescriptorTests.swift @@ -23,7 +23,7 @@ final class AsyncFileDescriptorTests: XCTestCase { .readOnly, onRing: ring ) - await try file.close() + try await file.close() } func testDevNullEmpty() async throws { From 32966f975d8d4285059c29eaccd1cee46c56b84f Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Oct 2024 23:43:59 +0000 Subject: [PATCH 013/172] It builds again! With some horrible hacks --- Sources/System/AsyncFileDescriptor.swift | 117 +++++++++++------- Sources/System/IOCompletion.swift | 1 + Sources/System/IORequest.swift | 58 ++++----- Sources/System/IORing.swift | 22 ++-- Sources/System/ManagedIORing.swift | 4 +- Sources/System/RawIORequest.swift | 2 +- .../AsyncFileDescriptorTests.swift | 2 +- Tests/SystemTests/IORequestTests.swift | 4 +- 8 files changed, 124 insertions(+), 86 deletions(-) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index 504aec9f..a00f41de 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -1,13 +1,12 @@ @_implementationOnly import CSystem - -public final class AsyncFileDescriptor { +public struct AsyncFileDescriptor: ~Copyable { @usableFromInline var open: Bool = true @usableFromInline let fileSlot: IORingFileSlot @usableFromInline let ring: ManagedIORing - + public static func openat( - atDirectory: FileDescriptor = FileDescriptor(rawValue: -100), + atDirectory: FileDescriptor = FileDescriptor(rawValue: -100), path: FilePath, _ mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), @@ -19,35 +18,37 @@ public final class AsyncFileDescriptor { throw IORingError.missingRequiredFeatures } let cstr = path.withCString { - return $0 // bad + return $0 // bad } - let res = await ring.submitAndWait(.openat( - atDirectory: atDirectory, - path: cstr, - mode, - options: options, - permissions: permissions, - intoSlot: fileSlot - )) + let res = await ring.submitAndWait( + .openat( + atDirectory: atDirectory, + path: cstr, + mode, + options: options, + permissions: permissions, + intoSlot: fileSlot.borrow() + )) if res.result < 0 { throw Errno(rawValue: -res.result) } - + return AsyncFileDescriptor( fileSlot, ring: ring ) } - internal init(_ fileSlot: IORingFileSlot, ring: ManagedIORing) { - self.fileSlot = fileSlot + internal init(_ fileSlot: consuming IORingFileSlot, ring: ManagedIORing) { + self.fileSlot = consume fileSlot self.ring = ring } @inlinable @inline(__always) @_unsafeInheritExecutor - public func close() async throws { - let res = await ring.submitAndWait(.close( - .registered(self.fileSlot) - )) + public consuming func close() async throws { + let res = await ring.submitAndWait( + .close( + .registered(self.fileSlot) + )) if res.result < 0 { throw Errno(rawValue: -res.result) } @@ -56,14 +57,16 @@ public final class AsyncFileDescriptor { @inlinable @inline(__always) @_unsafeInheritExecutor public func read( - into buffer: IORequest.Buffer, + into buffer: inout UnsafeMutableRawBufferPointer, atAbsoluteOffset offset: UInt64 = UInt64.max ) async throws -> UInt32 { - let res = await ring.submitAndWait(.read( - file: .registered(self.fileSlot), - buffer: buffer, - offset: offset - )) + let file = fileSlot.borrow() + let res = await ring.submitAndWait( + .readUnregistered( + file: .registered(file), + buffer: buffer, + offset: offset + )) if res.result < 0 { throw Errno(rawValue: -res.result) } else { @@ -71,23 +74,54 @@ public final class AsyncFileDescriptor { } } - deinit { - if (self.open) { - // TODO: close or error? TBD + @inlinable @inline(__always) @_unsafeInheritExecutor + public func read( + into buffer: borrowing IORingBuffer, //TODO: should be inout? + atAbsoluteOffset offset: UInt64 = UInt64.max + ) async throws -> UInt32 { + let res = await ring.submitAndWait( + .read( + file: .registered(self.fileSlot.borrow()), + buffer: buffer.borrow(), + offset: offset + )) + if res.result < 0 { + throw Errno(rawValue: -res.result) + } else { + return UInt32(bitPattern: res.result) } } + + //TODO: temporary workaround until AsyncSequence supports ~Copyable + public consuming func toBytes() -> AsyncFileDescriptorSequence { + AsyncFileDescriptorSequence(self) + } + + //TODO: can we do the linear types thing and error if they don't consume it manually? + // deinit { + // if self.open { + // TODO: close or error? TBD + // } + // } } -extension AsyncFileDescriptor: AsyncSequence { +public class AsyncFileDescriptorSequence: AsyncSequence { + var descriptor: AsyncFileDescriptor? + public func makeAsyncIterator() -> FileIterator { - return .init(self) + return .init(descriptor.take()!) + } + + internal init(_ descriptor: consuming AsyncFileDescriptor) { + self.descriptor = consume descriptor } public typealias AsyncIterator = FileIterator public typealias Element = UInt8 } -public struct FileIterator: AsyncIteratorProtocol { +//TODO: only a class due to ~Copyable limitations +public class FileIterator: AsyncIteratorProtocol { @usableFromInline let file: AsyncFileDescriptor @usableFromInline var buffer: IORingBuffer @usableFromInline var done: Bool @@ -95,28 +129,27 @@ public struct FileIterator: AsyncIteratorProtocol { @usableFromInline internal var currentByte: UnsafeRawPointer? @usableFromInline internal var lastByte: UnsafeRawPointer? - init(_ file: AsyncFileDescriptor) { - self.file = file + init(_ file: consuming AsyncFileDescriptor) { self.buffer = file.ring.getBuffer()! + self.file = file self.done = false } @inlinable @inline(__always) - public mutating func nextBuffer() async throws { - let buffer = self.buffer - - let bytesRead = try await file.read(into: .registered(buffer)) + public func nextBuffer() async throws { + let bytesRead = Int(try await file.read(into: buffer)) if _fastPath(bytesRead != 0) { - let bufPointer = buffer.unsafeBuffer.baseAddress.unsafelyUnwrapped + let unsafeBuffer = buffer.unsafeBuffer + let bufPointer = unsafeBuffer.baseAddress.unsafelyUnwrapped self.currentByte = UnsafeRawPointer(bufPointer) - self.lastByte = UnsafeRawPointer(bufPointer.advanced(by: Int(bytesRead))) + self.lastByte = UnsafeRawPointer(bufPointer.advanced(by: bytesRead)) } else { - self.done = true + done = true } } @inlinable @inline(__always) @_unsafeInheritExecutor - public mutating func next() async throws -> UInt8? { + public func next() async throws -> UInt8? { if _fastPath(currentByte != lastByte) { // SAFETY: both pointers should be non-nil if they're not equal let byte = currentByte.unsafelyUnwrapped.load(as: UInt8.self) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 5e226322..8bf173c9 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -1,5 +1,6 @@ @_implementationOnly import CSystem +//TODO: should be ~Copyable, but requires UnsafeContinuation add ~Copyable support public struct IOCompletion { let rawValue: io_uring_cqe } diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 2ecd3f46..2a47c62e 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,7 +1,7 @@ import struct CSystem.io_uring_sqe public enum IORequest: ~Copyable { - case nop // nothing here + case nop // nothing here case openat( atDirectory: FileDescriptor, path: UnsafePointer, @@ -12,21 +12,26 @@ public enum IORequest: ~Copyable { ) case read( file: File, - buffer: Buffer, + buffer: IORingBuffer, + offset: UInt64 = 0 + ) + case readUnregistered( + file: File, + buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0 ) case write( file: File, - buffer: Buffer, + buffer: IORingBuffer, + offset: UInt64 = 0 + ) + case writeUnregistered( + file: File, + buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0 ) case close(File) - public enum Buffer: ~Copyable { - case registered(IORingBuffer) - case unregistered(UnsafeMutableRawBufferPointer) - } - public enum File: ~Copyable { case registered(IORingFileSlot) case unregistered(FileDescriptor) @@ -90,30 +95,21 @@ extension IORequest { request.rawValue.file_index = UInt32(fileSlot.index + 1) } case .write(let file, let buffer, let offset): - switch consume buffer { - case .registered(let buffer): - request.operation = .writeFixed - return makeRawRequest_readWrite_registered( - file: file, buffer: buffer, offset: offset, request: request) - - case .unregistered(let buffer): - request.operation = .write - return makeRawRequest_readWrite_unregistered( - file: file, buffer: buffer, offset: offset, request: request) - } + request.operation = .writeFixed + return makeRawRequest_readWrite_registered( + file: file, buffer: buffer, offset: offset, request: request) + case .writeUnregistered(let file, let buffer, let offset): + request.operation = .write + return makeRawRequest_readWrite_unregistered( + file: file, buffer: buffer, offset: offset, request: request) case .read(let file, let buffer, let offset): - - switch consume buffer { - case .registered(let buffer): - request.operation = .readFixed - return makeRawRequest_readWrite_registered( - file: file, buffer: buffer, offset: offset, request: request) - - case .unregistered(let buffer): - request.operation = .read - return makeRawRequest_readWrite_unregistered( - file: file, buffer: buffer, offset: offset, request: request) - } + request.operation = .readFixed + return makeRawRequest_readWrite_registered( + file: file, buffer: buffer, offset: offset, request: request) + case .readUnregistered(let file, let buffer, let offset): + request.operation = .read + return makeRawRequest_readWrite_unregistered( + file: file, buffer: buffer, offset: offset, request: request) case .close(let file): request.operation = .close switch file { diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 3e24408f..2f18cb59 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -51,7 +51,7 @@ internal class ResourceManager: @unchecked Sendable { struct Resources { let resourceList: UnsafeMutableBufferPointer - var freeList: [Int] + var freeList: [Int] //TODO: bitvector? } let mutex: Mutex @@ -90,23 +90,33 @@ public struct IOResource: ~Copyable { @usableFromInline let resource: T @usableFromInline let index: Int let manager: ResourceManager + let isBorrow: Bool //TODO: this is a workaround for lifetime issues and should be removed internal init( resource: T, index: Int, - manager: ResourceManager + manager: ResourceManager, + isBorrow: Bool = false ) { self.resource = resource self.index = index self.manager = manager + self.isBorrow = isBorrow } func withResource() { } + //TODO: this is a workaround for lifetime issues and should be removed + @usableFromInline func borrow() -> IOResource { + IOResource(resource: resource, index: index, manager: manager, isBorrow: true) + } + deinit { - self.manager.releaseResource(index: self.index) + if !isBorrow { + manager.releaseResource(index: self.index) + } } } @@ -204,8 +214,6 @@ internal func _getSubmissionEntry(ring: inout SQRing, submissionQueueEntries: Un return nil } -// XXX: This should be a non-copyable type (?) -// demo only runs on Swift 5.8.1 public struct IORing: @unchecked Sendable, ~Copyable { let ringFlags: UInt32 let ringDescriptor: Int32 @@ -455,9 +463,9 @@ public struct IORing: @unchecked Sendable, ~Copyable { @inlinable @inline(__always) public mutating func writeRequest(_ request: __owned IORequest) -> Bool { - let raw = request.makeRawRequest() + var raw: RawIORequest? = request.makeRawRequest() return submissionMutex.withLock { ring in - return _writeRequest(raw, ring: &ring, submissionQueueEntries: submissionQueueEntries) + return _writeRequest(raw.take()!, ring: &ring, submissionQueueEntries: submissionQueueEntries) } } diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 88fb72e7..73ea2314 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -37,11 +37,11 @@ final public class ManagedIORing: @unchecked Sendable { } internal func getFileSlot() -> IORingFileSlot? { - self.internalRing.getFile() + internalRing.getFile() } internal func getBuffer() -> IORingBuffer? { - self.internalRing.getBuffer() + internalRing.getBuffer() } } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 520cb85c..52407b03 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -2,7 +2,7 @@ @_implementationOnly import CSystem import struct CSystem.io_uring_sqe -public struct RawIORequest { +public struct RawIORequest: ~Copyable { @usableFromInline var rawValue: io_uring_sqe public init() { diff --git a/Tests/SystemTests/AsyncFileDescriptorTests.swift b/Tests/SystemTests/AsyncFileDescriptorTests.swift index 1baba962..7de22724 100644 --- a/Tests/SystemTests/AsyncFileDescriptorTests.swift +++ b/Tests/SystemTests/AsyncFileDescriptorTests.swift @@ -33,7 +33,7 @@ final class AsyncFileDescriptorTests: XCTestCase { .readOnly, onRing: ring ) - for try await _ in file { + for try await _ in file.toBytes() { XCTFail("/dev/null should be empty") } } diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index a44a607e..a1e14533 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -6,7 +6,7 @@ import XCTest import System #endif -func requestBytes(_ request: RawIORequest) -> [UInt8] { +func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { return withUnsafePointer(to: request) { let requestBuf = UnsafeBufferPointer(start: $0, count: 1) let rawBytes = UnsafeRawBufferPointer(requestBuf) @@ -40,7 +40,7 @@ final class IORequestTests: XCTestCase { .readOnly, options: [], permissions: nil, - intoSlot: fileSlot + intoSlot: fileSlot.borrow() ) let expectedRequest: [UInt8] = { From 822e4814d18de785c06fe4edbe31f750c3898f1b Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Oct 2024 23:49:37 +0000 Subject: [PATCH 014/172] Adopt isolation parameters --- Sources/System/AsyncFileDescriptor.swift | 16 +++++++++------- Sources/System/ManagedIORing.swift | 3 +-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index a00f41de..68d9e33c 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -43,8 +43,8 @@ public struct AsyncFileDescriptor: ~Copyable { self.ring = ring } - @inlinable @inline(__always) @_unsafeInheritExecutor - public consuming func close() async throws { + @inlinable @inline(__always) + public consuming func close(isolation actor: isolated (any Actor)? = #isolation) async throws { let res = await ring.submitAndWait( .close( .registered(self.fileSlot) @@ -55,10 +55,11 @@ public struct AsyncFileDescriptor: ~Copyable { self.open = false } - @inlinable @inline(__always) @_unsafeInheritExecutor + @inlinable @inline(__always) public func read( into buffer: inout UnsafeMutableRawBufferPointer, - atAbsoluteOffset offset: UInt64 = UInt64.max + atAbsoluteOffset offset: UInt64 = UInt64.max, + isolation actor: isolated (any Actor)? = #isolation ) async throws -> UInt32 { let file = fileSlot.borrow() let res = await ring.submitAndWait( @@ -74,10 +75,11 @@ public struct AsyncFileDescriptor: ~Copyable { } } - @inlinable @inline(__always) @_unsafeInheritExecutor + @inlinable @inline(__always) public func read( into buffer: borrowing IORingBuffer, //TODO: should be inout? - atAbsoluteOffset offset: UInt64 = UInt64.max + atAbsoluteOffset offset: UInt64 = UInt64.max, + isolation actor: isolated (any Actor)? = #isolation ) async throws -> UInt32 { let res = await ring.submitAndWait( .read( @@ -148,7 +150,7 @@ public class FileIterator: AsyncIteratorProtocol { } } - @inlinable @inline(__always) @_unsafeInheritExecutor + @inlinable @inline(__always) public func next() async throws -> UInt8? { if _fastPath(currentByte != lastByte) { // SAFETY: both pointers should be non-nil if they're not equal diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 73ea2314..30ea5ed6 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -20,8 +20,7 @@ final public class ManagedIORing: @unchecked Sendable { } } - @_unsafeInheritExecutor - public func submitAndWait(_ request: __owned IORequest) async -> IOCompletion { + public func submitAndWait(_ request: __owned IORequest, isolation actor: isolated (any Actor)? = #isolation) async -> IOCompletion { var consumeOnceWorkaround: IORequest? = request return await withUnsafeContinuation { cont in return internalRing.submissionMutex.withLock { ring in From a9f92a6e316826c6f80d3963f3207bae4d573818 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Oct 2024 17:23:25 -0700 Subject: [PATCH 015/172] Delete stray Package.resolved changes --- Package.resolved | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index b10a9832..00000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "swift-atomics", - "repositoryURL": "https://github.com/apple/swift-atomics", - "state": { - "branch": null, - "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", - "version": "1.1.0" - } - } - ] - }, - "version": 1 -} From 1a3e37d7919aba47d376200d7c6f998e31aa84ba Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Oct 2024 17:24:46 -0700 Subject: [PATCH 016/172] Fix mismerge --- Sources/CSystem/{ => include}/CSystemLinux.h | 0 Sources/CSystem/{ => include}/CSystemWASI.h | 0 Sources/CSystem/{ => include}/CSystemWindows.h | 0 Sources/CSystem/{ => include}/io_uring.h | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Sources/CSystem/{ => include}/CSystemLinux.h (100%) rename Sources/CSystem/{ => include}/CSystemWASI.h (100%) rename Sources/CSystem/{ => include}/CSystemWindows.h (100%) rename Sources/CSystem/{ => include}/io_uring.h (100%) diff --git a/Sources/CSystem/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h similarity index 100% rename from Sources/CSystem/CSystemLinux.h rename to Sources/CSystem/include/CSystemLinux.h diff --git a/Sources/CSystem/CSystemWASI.h b/Sources/CSystem/include/CSystemWASI.h similarity index 100% rename from Sources/CSystem/CSystemWASI.h rename to Sources/CSystem/include/CSystemWASI.h diff --git a/Sources/CSystem/CSystemWindows.h b/Sources/CSystem/include/CSystemWindows.h similarity index 100% rename from Sources/CSystem/CSystemWindows.h rename to Sources/CSystem/include/CSystemWindows.h diff --git a/Sources/CSystem/io_uring.h b/Sources/CSystem/include/io_uring.h similarity index 100% rename from Sources/CSystem/io_uring.h rename to Sources/CSystem/include/io_uring.h From f369347b73ccbd40740cb905a12b82d09083757e Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Oct 2024 17:25:31 -0700 Subject: [PATCH 017/172] Fix mismerge --- Sources/CSystem/{ => include}/module.modulemap | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/CSystem/{ => include}/module.modulemap (100%) diff --git a/Sources/CSystem/module.modulemap b/Sources/CSystem/include/module.modulemap similarity index 100% rename from Sources/CSystem/module.modulemap rename to Sources/CSystem/include/module.modulemap From f1393b76f37049380f4714cb2c50c6feb5190daa Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Thu, 21 Nov 2024 14:12:06 -0700 Subject: [PATCH 018/172] CSystem: Set build and install interfaces on included headers --- Sources/CSystem/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CSystem/CMakeLists.txt b/Sources/CSystem/CMakeLists.txt index 0f5d8e8a..faf730e3 100644 --- a/Sources/CSystem/CMakeLists.txt +++ b/Sources/CSystem/CMakeLists.txt @@ -9,8 +9,8 @@ See https://swift.org/LICENSE.txt for license information add_library(CSystem INTERFACE) target_include_directories(CSystem INTERFACE - include) - + "$" + $) install(FILES include/CSystemLinux.h From eb3b1fa54bc890cc3d69a8fb34fdfd54fb3aadc7 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Thu, 21 Nov 2024 15:03:28 -0700 Subject: [PATCH 019/172] Internals: Remove non-breaking spaces from Windows syscall adapter This was causing a warning with Swift 6.0.2 --- Sources/System/Internals/WindowsSyscallAdapters.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index d56d33e4..706881ec 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -187,7 +187,7 @@ internal func pwrite( internal func pipe( _ fds: UnsafeMutablePointer, bytesReserved: UInt32 = 4096 ) -> CInt { -  return _pipe(fds, bytesReserved, _O_BINARY | _O_NOINHERIT); + return _pipe(fds, bytesReserved, _O_BINARY | _O_NOINHERIT); } @inline(__always) From ef94a3719363a97cc8b7e6fa30a096a7d74e9f3c Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 5 Dec 2024 03:11:11 +0000 Subject: [PATCH 020/172] Refactoring, and give up on resources being noncopyable structs --- Sources/System/AsyncFileDescriptor.swift | 60 ++-- Sources/System/IOCompletion.swift | 2 +- Sources/System/IORequest.swift | 273 +++++++++++++++--- Sources/System/IORing.swift | 18 +- .../Internals/WindowsSyscallAdapters.swift | 2 +- .../AsyncFileDescriptorTests.swift | 36 ++- Tests/SystemTests/IORequestTests.swift | 14 +- Tests/SystemTests/IORingTests.swift | 4 +- Tests/SystemTests/ManagedIORingTests.swift | 4 +- 9 files changed, 305 insertions(+), 108 deletions(-) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index 68d9e33c..f8075d6c 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -5,29 +5,30 @@ public struct AsyncFileDescriptor: ~Copyable { @usableFromInline let fileSlot: IORingFileSlot @usableFromInline let ring: ManagedIORing - public static func openat( - atDirectory: FileDescriptor = FileDescriptor(rawValue: -100), + public static func open( path: FilePath, - _ mode: FileDescriptor.AccessMode, + in directory: FileDescriptor = FileDescriptor(rawValue: -100), + on ring: ManagedIORing, + mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - onRing ring: ManagedIORing + permissions: FilePermissions? = nil ) async throws -> AsyncFileDescriptor { // todo; real error type guard let fileSlot = ring.getFileSlot() else { throw IORingError.missingRequiredFeatures } + //TODO: need an async-friendly withCString let cstr = path.withCString { return $0 // bad } let res = await ring.submitAndWait( - .openat( - atDirectory: atDirectory, - path: cstr, - mode, - options: options, - permissions: permissions, - intoSlot: fileSlot.borrow() + IORequest( + opening: cstr, + in: directory, + into: fileSlot, + mode: mode, + options: options, + permissions: permissions )) if res.result < 0 { throw Errno(rawValue: -res.result) @@ -45,10 +46,7 @@ public struct AsyncFileDescriptor: ~Copyable { @inlinable @inline(__always) public consuming func close(isolation actor: isolated (any Actor)? = #isolation) async throws { - let res = await ring.submitAndWait( - .close( - .registered(self.fileSlot) - )) + let res = await ring.submitAndWait(IORequest(closing: fileSlot)) if res.result < 0 { throw Errno(rawValue: -res.result) } @@ -61,12 +59,11 @@ public struct AsyncFileDescriptor: ~Copyable { atAbsoluteOffset offset: UInt64 = UInt64.max, isolation actor: isolated (any Actor)? = #isolation ) async throws -> UInt32 { - let file = fileSlot.borrow() let res = await ring.submitAndWait( - .readUnregistered( - file: .registered(file), - buffer: buffer, - offset: offset + IORequest( + reading: fileSlot, + into: buffer, + at: offset )) if res.result < 0 { throw Errno(rawValue: -res.result) @@ -77,15 +74,15 @@ public struct AsyncFileDescriptor: ~Copyable { @inlinable @inline(__always) public func read( - into buffer: borrowing IORingBuffer, //TODO: should be inout? + into buffer: IORingBuffer, //TODO: should be inout? atAbsoluteOffset offset: UInt64 = UInt64.max, isolation actor: isolated (any Actor)? = #isolation ) async throws -> UInt32 { let res = await ring.submitAndWait( - .read( - file: .registered(self.fileSlot.borrow()), - buffer: buffer.borrow(), - offset: offset + IORequest( + reading: fileSlot, + into: buffer, + at: offset )) if res.result < 0 { throw Errno(rawValue: -res.result) @@ -100,11 +97,12 @@ public struct AsyncFileDescriptor: ~Copyable { } //TODO: can we do the linear types thing and error if they don't consume it manually? - // deinit { - // if self.open { - // TODO: close or error? TBD - // } - // } + // deinit { + // if self.open { + // close() + // // TODO: close or error? TBD + // } + // } } public class AsyncFileDescriptorSequence: AsyncSequence { diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 8bf173c9..1702f9e8 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -21,7 +21,7 @@ extension IOCompletion { } extension IOCompletion { - public var userData: UInt64 { + public var userData: UInt64 { //TODO: naming? get { return rawValue.user_data } diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 2a47c62e..0af87176 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,57 +1,90 @@ import struct CSystem.io_uring_sqe -public enum IORequest: ~Copyable { +@usableFromInline +internal enum IORequestCore: ~Copyable { case nop // nothing here case openat( + atDirectory: FileDescriptor, + path: UnsafePointer, + FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil + ) + case openatSlot( atDirectory: FileDescriptor, path: UnsafePointer, FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, - intoSlot: IORingFileSlot? = nil + intoSlot: IORingFileSlot ) case read( - file: File, + file: FileDescriptor, buffer: IORingBuffer, offset: UInt64 = 0 ) case readUnregistered( - file: File, + file: FileDescriptor, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64 = 0 + ) + case readSlot( + file: IORingFileSlot, + buffer: IORingBuffer, + offset: UInt64 = 0 + ) + case readUnregisteredSlot( + file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0 ) case write( - file: File, + file: FileDescriptor, buffer: IORingBuffer, offset: UInt64 = 0 ) case writeUnregistered( - file: File, + file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0 ) - case close(File) - - public enum File: ~Copyable { - case registered(IORingFileSlot) - case unregistered(FileDescriptor) - } + case writeSlot( + file: IORingFileSlot, + buffer: IORingBuffer, + offset: UInt64 = 0 + ) + case writeUnregisteredSlot( + file: IORingFileSlot, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64 = 0 + ) + case close(FileDescriptor) + case closeSlot(IORingFileSlot) } @inlinable @inline(__always) internal func makeRawRequest_readWrite_registered( - file: consuming IORequest.File, - buffer: consuming IORingBuffer, + file: FileDescriptor, + buffer: IORingBuffer, offset: UInt64, request: consuming RawIORequest ) -> RawIORequest { - switch file { - case .registered(let regFile): - request.rawValue.fd = Int32(exactly: regFile.index)! - request.flags = .fixedFile - case .unregistered(let fd): - request.fileDescriptor = fd - } + request.fileDescriptor = file + request.buffer = buffer.unsafeBuffer + request.rawValue.buf_index = UInt16(exactly: buffer.index)! + request.offset = offset + return request +} + +@inlinable @inline(__always) +internal func makeRawRequest_readWrite_registered_slot( + file: IORingFileSlot, + buffer: IORingBuffer, + offset: UInt64, + request: consuming RawIORequest +) -> RawIORequest { + request.rawValue.fd = Int32(exactly: file.index)! + request.flags = .fixedFile request.buffer = buffer.unsafeBuffer request.rawValue.buf_index = UInt16(exactly: buffer.index)! request.offset = offset @@ -60,64 +93,222 @@ internal func makeRawRequest_readWrite_registered( @inlinable @inline(__always) internal func makeRawRequest_readWrite_unregistered( - file: consuming IORequest.File, + file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, offset: UInt64, request: consuming RawIORequest ) -> RawIORequest { - switch file { - case .registered(let regFile): - request.rawValue.fd = Int32(exactly: regFile.index)! - request.flags = .fixedFile - case .unregistered(let fd): - request.fileDescriptor = fd - } + request.fileDescriptor = file request.buffer = buffer request.offset = offset return request } +@inlinable @inline(__always) +internal func makeRawRequest_readWrite_unregistered_slot( + file: IORingFileSlot, + buffer: UnsafeMutableRawBufferPointer, + offset: UInt64, + request: consuming RawIORequest +) -> RawIORequest { + request.rawValue.fd = Int32(exactly: file.index)! + request.flags = .fixedFile + request.buffer = buffer + request.offset = offset + return request +} + +public struct IORequest : ~Copyable { + @usableFromInline var core: IORequestCore + + @inlinable internal consuming func extractCore() -> IORequestCore { + return core + } +} + extension IORequest { + public init() { //TODO: why do we have nop? + core = .nop + } + + public init( + reading file: IORingFileSlot, + into buffer: IORingBuffer, + at offset: UInt64 = 0 + ) { + core = .readSlot(file: file, buffer: buffer, offset: offset) + } + + public init( + reading file: FileDescriptor, + into buffer: IORingBuffer, + at offset: UInt64 = 0 + ) { + core = .read(file: file, buffer: buffer, offset: offset) + } + + public init( + reading file: IORingFileSlot, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0 + ) { + core = .readUnregisteredSlot(file: file, buffer: buffer, offset: offset) + } + + public init( + reading file: FileDescriptor, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0 + ) { + core = .readUnregistered(file: file, buffer: buffer, offset: offset) + } + + public init( + writing buffer: IORingBuffer, + into file: IORingFileSlot, + at offset: UInt64 = 0 + ) { + core = .writeSlot(file: file, buffer: buffer, offset: offset) + } + + public init( + writing buffer: IORingBuffer, + into file: FileDescriptor, + at offset: UInt64 = 0 + ) { + core = .write(file: file, buffer: buffer, offset: offset) + } + + public init( + writing buffer: UnsafeMutableRawBufferPointer, + into file: IORingFileSlot, + at offset: UInt64 = 0 + ) { + core = .writeUnregisteredSlot(file: file, buffer: buffer, offset: offset) + } + + public init( + writing buffer: UnsafeMutableRawBufferPointer, + into file: FileDescriptor, + at offset: UInt64 = 0 + ) { + core = .writeUnregistered(file: file, buffer: buffer, offset: offset) + } + + public init( + closing file: FileDescriptor + ) { + core = .close(file) + } + + public init( + closing file: IORingFileSlot + ) { + core = .closeSlot(file) + } + + + public init( + opening path: UnsafePointer, + in directory: FileDescriptor, + into slot: IORingFileSlot, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil + ) { + core = .openatSlot(atDirectory: directory, path: path, mode, options: options, permissions: permissions, intoSlot: slot) + } + + public init( + opening path: UnsafePointer, + in directory: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil + ) { + core = .openat(atDirectory: directory, path: path, mode, options: options, permissions: permissions) + } + + + public init( + opening path: FilePath, + in directory: FileDescriptor, + into slot: IORingFileSlot, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil + ) { + fatalError("Implement me") + } + + public init( + opening path: FilePath, + in directory: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil + ) { + fatalError("Implement me") + } + @inlinable @inline(__always) public consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() - switch consume self { + switch extractCore() { case .nop: request.operation = .nop - case .openat(let atDirectory, let path, let mode, let options, let permissions, let slot): + case .openatSlot(let atDirectory, let path, let mode, let options, let permissions, let fileSlot): // TODO: use rawValue less request.operation = .openAt request.fileDescriptor = atDirectory - request.rawValue.addr = unsafeBitCast(path, to: UInt64.self) + request.rawValue.addr = UInt64(UInt(bitPattern: path)) + request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) + request.rawValue.len = permissions?.rawValue ?? 0 + request.rawValue.file_index = UInt32(fileSlot.index + 1) + case .openat(let atDirectory, let path, let mode, let options, let permissions): + request.operation = .openAt + request.fileDescriptor = atDirectory + request.rawValue.addr = UInt64(UInt(bitPattern: path)) request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) request.rawValue.len = permissions?.rawValue ?? 0 - if let fileSlot = slot { - request.rawValue.file_index = UInt32(fileSlot.index + 1) - } case .write(let file, let buffer, let offset): request.operation = .writeFixed return makeRawRequest_readWrite_registered( file: file, buffer: buffer, offset: offset, request: request) + case .writeSlot(let file, let buffer, let offset): + request.operation = .writeFixed + return makeRawRequest_readWrite_registered_slot( + file: file, buffer: buffer, offset: offset, request: request) case .writeUnregistered(let file, let buffer, let offset): request.operation = .write return makeRawRequest_readWrite_unregistered( file: file, buffer: buffer, offset: offset, request: request) + case .writeUnregisteredSlot(let file, let buffer, let offset): + request.operation = .write + return makeRawRequest_readWrite_unregistered_slot( + file: file, buffer: buffer, offset: offset, request: request) case .read(let file, let buffer, let offset): request.operation = .readFixed return makeRawRequest_readWrite_registered( file: file, buffer: buffer, offset: offset, request: request) + case .readSlot(let file, let buffer, let offset): + request.operation = .readFixed + return makeRawRequest_readWrite_registered_slot( + file: file, buffer: buffer, offset: offset, request: request) case .readUnregistered(let file, let buffer, let offset): request.operation = .read return makeRawRequest_readWrite_unregistered( file: file, buffer: buffer, offset: offset, request: request) + case .readUnregisteredSlot(let file, let buffer, let offset): + request.operation = .read + return makeRawRequest_readWrite_unregistered_slot( + file: file, buffer: buffer, offset: offset, request: request) case .close(let file): request.operation = .close - switch file { - case .registered(let regFile): - request.rawValue.file_index = UInt32(regFile.index + 1) - case .unregistered(let normalFile): - request.fileDescriptor = normalFile - } + request.fileDescriptor = file + case .closeSlot(let file): + request.operation = .close + request.rawValue.file_index = UInt32(file.index + 1) } return request } diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 2f18cb59..57c21611 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -46,7 +46,7 @@ struct CQRing: ~Copyable { let cqes: UnsafeBufferPointer } -internal class ResourceManager: @unchecked Sendable { +internal final class ResourceManager: @unchecked Sendable { typealias Resource = T struct Resources { @@ -85,38 +85,28 @@ internal class ResourceManager: @unchecked Sendable { } } -public struct IOResource: ~Copyable { +public class IOResource { typealias Resource = T @usableFromInline let resource: T @usableFromInline let index: Int let manager: ResourceManager - let isBorrow: Bool //TODO: this is a workaround for lifetime issues and should be removed internal init( resource: T, index: Int, - manager: ResourceManager, - isBorrow: Bool = false + manager: ResourceManager ) { self.resource = resource self.index = index self.manager = manager - self.isBorrow = isBorrow } func withResource() { } - //TODO: this is a workaround for lifetime issues and should be removed - @usableFromInline func borrow() -> IOResource { - IOResource(resource: resource, index: index, manager: manager, isBorrow: true) - } - deinit { - if !isBorrow { - manager.releaseResource(index: self.index) - } + manager.releaseResource(index: self.index) } } diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index d56d33e4..706881ec 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -187,7 +187,7 @@ internal func pwrite( internal func pipe( _ fds: UnsafeMutablePointer, bytesReserved: UInt32 = 4096 ) -> CInt { -  return _pipe(fds, bytesReserved, _O_BINARY | _O_NOINHERIT); + return _pipe(fds, bytesReserved, _O_BINARY | _O_NOINHERIT); } @inline(__always) diff --git a/Tests/SystemTests/AsyncFileDescriptorTests.swift b/Tests/SystemTests/AsyncFileDescriptorTests.swift index 7de22724..ebd308fc 100644 --- a/Tests/SystemTests/AsyncFileDescriptorTests.swift +++ b/Tests/SystemTests/AsyncFileDescriptorTests.swift @@ -9,32 +9,50 @@ import System final class AsyncFileDescriptorTests: XCTestCase { func testOpen() async throws { let ring = try ManagedIORing(queueDepth: 32) - let file = try await AsyncFileDescriptor.openat( + _ = try await AsyncFileDescriptor.open( path: "/dev/zero", - .readOnly, - onRing: ring + on: ring, + mode: .readOnly ) } func testOpenClose() async throws { let ring = try ManagedIORing(queueDepth: 32) - let file = try await AsyncFileDescriptor.openat( + let file = try await AsyncFileDescriptor.open( path: "/dev/zero", - .readOnly, - onRing: ring + on: ring, + mode: .readOnly ) try await file.close() } func testDevNullEmpty() async throws { let ring = try ManagedIORing(queueDepth: 32) - let file = try await AsyncFileDescriptor.openat( + let file = try await AsyncFileDescriptor.open( path: "/dev/null", - .readOnly, - onRing: ring + on: ring, + mode: .readOnly ) for try await _ in file.toBytes() { XCTFail("/dev/null should be empty") } } + + func testRead() async throws { + let ring = try ManagedIORing(queueDepth: 32) + let file = try await AsyncFileDescriptor.open( + path: "/dev/zero", + on: ring, + mode: .readOnly + ) + let bytes = file.toBytes() + var counter = 0 + for try await byte in bytes { + XCTAssert(byte == 0) + counter &+= 1 + if counter > 16384 { + break + } + } + } } diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index a1e14533..78be4cf7 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -19,7 +19,7 @@ func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { // which are known to work correctly. final class IORequestTests: XCTestCase { func testNop() { - let req = IORequest.nop.makeRawRequest() + let req = IORequest().makeRawRequest() let sourceBytes = requestBytes(req) // convenient property of nop: it's all zeros! // for some unknown reason, liburing sets the fd field to -1. @@ -34,13 +34,13 @@ final class IORequestTests: XCTestCase { let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! let fileSlot = resmgr.getResource()! - let req = IORequest.openat( - atDirectory: FileDescriptor(rawValue: -100), - path: pathPtr, - .readOnly, + let req = IORequest( + opening: pathPtr, + in: FileDescriptor(rawValue: -100), + into: fileSlot, + mode: .readOnly, options: [], - permissions: nil, - intoSlot: fileSlot.borrow() + permissions: nil ) let expectedRequest: [UInt8] = { diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 4604f500..85846d3e 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -8,12 +8,12 @@ import System final class IORingTests: XCTestCase { func testInit() throws { - let ring = try IORing(queueDepth: 32) + _ = try IORing(queueDepth: 32) } func testNop() throws { var ring = try IORing(queueDepth: 32) - ring.writeRequest(.nop) + ring.writeRequest(IORequest()) ring.submitRequests() let completion = ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) diff --git a/Tests/SystemTests/ManagedIORingTests.swift b/Tests/SystemTests/ManagedIORingTests.swift index e7ad3f59..4b8ea28b 100644 --- a/Tests/SystemTests/ManagedIORingTests.swift +++ b/Tests/SystemTests/ManagedIORingTests.swift @@ -8,12 +8,12 @@ import System final class ManagedIORingTests: XCTestCase { func testInit() throws { - let ring = try ManagedIORing(queueDepth: 32) + _ = try ManagedIORing(queueDepth: 32) } func testNop() async throws { let ring = try ManagedIORing(queueDepth: 32) - let completion = await ring.submitAndWait(.nop) + let completion = await ring.submitAndWait(IORequest()) XCTAssertEqual(completion.result, 0) } } From 7ea32aefce1f716cb0147f43b64eacf184f1914f Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 11 Dec 2024 23:00:50 +0000 Subject: [PATCH 021/172] More refactoring, and working timeout support on ManagedIORing --- Sources/System/AsyncFileDescriptor.swift | 11 ++- Sources/System/IORing.swift | 82 +++++++++++++++++----- Sources/System/IORingError.swift | 5 +- Sources/System/ManagedIORing.swift | 74 ++++++++++++++++--- Sources/System/RawIORequest.swift | 42 ++++++++++- Tests/SystemTests/IORingTests.swift | 2 +- Tests/SystemTests/ManagedIORingTests.swift | 31 +++++++- 7 files changed, 208 insertions(+), 39 deletions(-) diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift index f8075d6c..a76d90e5 100644 --- a/Sources/System/AsyncFileDescriptor.swift +++ b/Sources/System/AsyncFileDescriptor.swift @@ -21,8 +21,7 @@ public struct AsyncFileDescriptor: ~Copyable { let cstr = path.withCString { return $0 // bad } - let res = await ring.submitAndWait( - IORequest( + let res = try await ring.submit(request: IORequest( opening: cstr, in: directory, into: fileSlot, @@ -46,7 +45,7 @@ public struct AsyncFileDescriptor: ~Copyable { @inlinable @inline(__always) public consuming func close(isolation actor: isolated (any Actor)? = #isolation) async throws { - let res = await ring.submitAndWait(IORequest(closing: fileSlot)) + let res = try await ring.submit(request: IORequest(closing: fileSlot)) if res.result < 0 { throw Errno(rawValue: -res.result) } @@ -59,8 +58,7 @@ public struct AsyncFileDescriptor: ~Copyable { atAbsoluteOffset offset: UInt64 = UInt64.max, isolation actor: isolated (any Actor)? = #isolation ) async throws -> UInt32 { - let res = await ring.submitAndWait( - IORequest( + let res = try await ring.submit(request: IORequest( reading: fileSlot, into: buffer, at: offset @@ -78,8 +76,7 @@ public struct AsyncFileDescriptor: ~Copyable { atAbsoluteOffset offset: UInt64 = UInt64.max, isolation actor: isolated (any Actor)? = #isolation ) async throws -> UInt32 { - let res = await ring.submitAndWait( - IORequest( + let res = try await ring.submit(request: IORequest( reading: fileSlot, into: buffer, at: offset diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 57c21611..00461d14 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -138,7 +138,10 @@ internal func _blockingGetSubmissionEntry(ring: inout SQRing, submissionQueueEnt io_uring_sqe > { while true { - if let entry = _getSubmissionEntry(ring: &ring, submissionQueueEntries: submissionQueueEntries) { + if let entry = _getSubmissionEntry( + ring: &ring, + submissionQueueEntries: submissionQueueEntries + ) { return entry } // TODO: actually block here instead of spinning @@ -146,13 +149,18 @@ internal func _blockingGetSubmissionEntry(ring: inout SQRing, submissionQueueEnt } -internal func _submitRequests(ring: inout SQRing, ringDescriptor: Int32) { - let flushedEvents = _flushQueue(ring: &ring) - - // Ring always needs enter right now; +//TODO: omitting signal mask for now +//Tell the kernel that we've submitted requests and/or are waiting for completions +internal func _enter( + ringDescriptor: Int32, + numEvents: UInt32, + minCompletions: UInt32, + flags: UInt32 +) throws -> Int32 { + // Ring always needs enter right now; // TODO: support SQPOLL here while true { - let ret = io_uring_enter(ringDescriptor, flushedEvents, 0, 0, nil) + let ret = io_uring_enter(ringDescriptor, numEvents, minCompletions, flags, nil) // error handling: // EAGAIN / EINTR (try again), // EBADF / EBADFD / EOPNOTSUPP / ENXIO @@ -160,32 +168,47 @@ internal func _submitRequests(ring: inout SQRing, ringDescriptor: Int32) { // EINVAL (bad constant flag?, fatal), // EFAULT (bad address for argument from library, fatal) if ret == -EAGAIN || ret == -EINTR { + //TODO: should we wait a bit on AGAIN? continue } else if ret < 0 { fatalError( "fatal error in submitting requests: " + Errno(rawValue: -ret).debugDescription ) } else { - break + return ret } } } +internal func _submitRequests(ring: inout SQRing, ringDescriptor: Int32) throws { + let flushedEvents = _flushQueue(ring: &ring) + _ = try _enter(ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) +} + +internal func _getUnconsumedSubmissionCount(ring: inout SQRing) -> UInt32 { + return ring.userTail - ring.kernelHead.pointee.load(ordering: .acquiring) +} + +internal func _getUnconsumedCompletionCount(ring: inout CQRing) -> UInt32 { + return ring.kernelTail.pointee.load(ordering: .acquiring) - ring.kernelHead.pointee.load(ordering: .acquiring) +} + +//TODO: pretty sure this is supposed to do more than it does internal func _flushQueue(ring: inout SQRing) -> UInt32 { ring.kernelTail.pointee.store( - ring.userTail, ordering: .relaxed + ring.userTail, ordering: .releasing ) - return ring.userTail - ring.kernelHead.pointee.load(ordering: .relaxed) + return _getUnconsumedSubmissionCount(ring: &ring) } @usableFromInline @inline(__always) internal func _getSubmissionEntry(ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) -> UnsafeMutablePointer< io_uring_sqe >? { - let next = ring.userTail + 1 + let next = ring.userTail &+ 1 //this is expected to wrap // FEAT: smp load when SQPOLL in use (not in MVP) - let kernelHead = ring.kernelHead.pointee.load(ordering: .relaxed) + let kernelHead = ring.kernelHead.pointee.load(ordering: .acquiring) // FEAT: 128-bit event support (not in MVP) if next - kernelHead <= ring.array.count { @@ -383,18 +406,45 @@ public struct IORing: @unchecked Sendable, ~Copyable { func _tryConsumeCompletion(ring: inout CQRing) -> IOCompletion? { let tail = ring.kernelTail.pointee.load(ordering: .acquiring) - let head = ring.kernelHead.pointee.load(ordering: .relaxed) + let head = ring.kernelHead.pointee.load(ordering: .acquiring) if tail != head { // 32 byte copy - oh well let res = ring.cqes[Int(head & ring.ringMask)] - ring.kernelHead.pointee.store(head + 1, ordering: .relaxed) + ring.kernelHead.pointee.store(head &+ 1, ordering: .releasing) return IOCompletion(rawValue: res) } return nil } + internal func handleRegistrationResult(_ result: Int32) throws { + //TODO: error handling + } + + public mutating func registerEventFD(ring: inout IORing, _ descriptor: FileDescriptor) throws { + var rawfd = descriptor.rawValue + let result = withUnsafePointer(to: &rawfd) { fdptr in + return io_uring_register( + ring.ringDescriptor, + IORING_REGISTER_EVENTFD, + UnsafeMutableRawPointer(mutating: fdptr), + 1 + ) + } + try handleRegistrationResult(result) + } + + public mutating func unregisterEventFD(ring: inout IORing) throws { + let result = io_uring_register( + ring.ringDescriptor, + IORING_UNREGISTER_EVENTFD, + nil, + 0 + ) + try handleRegistrationResult(result) + } + public mutating func registerFiles(count: UInt32) { guard self.registeredFiles == nil else { fatalError() } let fileBuf = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) @@ -445,9 +495,9 @@ public struct IORing: @unchecked Sendable, ~Copyable { fatalError("failed to unregister buffers: TODO") } - public func submitRequests() { - submissionMutex.withLock { ring in - _submitRequests(ring: &ring, ringDescriptor: ringDescriptor) + public func submitRequests() throws { + try submissionMutex.withLock { ring in + try _submitRequests(ring: &ring, ringDescriptor: ringDescriptor) } } diff --git a/Sources/System/IORingError.swift b/Sources/System/IORingError.swift index d87b2938..fda58bcb 100644 --- a/Sources/System/IORingError.swift +++ b/Sources/System/IORingError.swift @@ -1,3 +1,6 @@ -enum IORingError: Error { +//TODO: make this not an enum +public enum IORingError: Error, Equatable { case missingRequiredFeatures + case operationCanceled + case unknown } diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 30ea5ed6..99c112d6 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -1,3 +1,16 @@ +fileprivate func handleCompletionError( + _ result: Int32, + for continuation: UnsafeContinuation) { + var error: IORingError = .unknown + switch result { + case -(_ECANCELED): + error = .operationCanceled + default: + error = .unknown + } + continuation.resume(throwing: error) +} + final public class ManagedIORing: @unchecked Sendable { var internalRing: IORing @@ -11,25 +24,64 @@ final public class ManagedIORing: @unchecked Sendable { private func startWaiter() { Task.detached { while !Task.isCancelled { + //TODO: should timeout handling be sunk into IORing? let cqe = self.internalRing.blockingConsumeCompletion() + if cqe.userData == 0 { + continue + } let cont = unsafeBitCast( - cqe.userData, to: UnsafeContinuation.self) - cont.resume(returning: cqe) + cqe.userData, to: UnsafeContinuation.self) + + if cqe.result < 0 { + var err = system_strerror(cqe.result * -1) + let len = system_strlen(err!) + err!.withMemoryRebound(to: UInt8.self, capacity: len) { + let errStr = String(decoding: UnsafeBufferPointer(start: $0, count: len), as: UTF8.self) + print("\(errStr)") + } + handleCompletionError(cqe.result, for: cont) + } else { + cont.resume(returning: cqe) + } } } } - public func submitAndWait(_ request: __owned IORequest, isolation actor: isolated (any Actor)? = #isolation) async -> IOCompletion { + public func submit( + request: __owned IORequest, + timeout: Duration? = nil, + isolation actor: isolated (any Actor)? = #isolation + ) async throws -> IOCompletion { var consumeOnceWorkaround: IORequest? = request - return await withUnsafeContinuation { cont in - return internalRing.submissionMutex.withLock { ring in - let request = consumeOnceWorkaround.take()! - let entry = _blockingGetSubmissionEntry( - ring: &ring, submissionQueueEntries: internalRing.submissionQueueEntries) - entry.pointee = request.makeRawRequest().rawValue - entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) - _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) + return try await withUnsafeThrowingContinuation { cont in + do { + try internalRing.submissionMutex.withLock { ring in + let request = consumeOnceWorkaround.take()! + let entry = _blockingGetSubmissionEntry( + ring: &ring, submissionQueueEntries: internalRing.submissionQueueEntries) + entry.pointee = request.makeRawRequest().rawValue + entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) + if let timeout { + //TODO: if IORING_FEAT_MIN_TIMEOUT is supported we can do this more efficiently + let timeoutEntry = _blockingGetSubmissionEntry( + ring: &ring, + submissionQueueEntries: internalRing.submissionQueueEntries + ) + try RawIORequest.withTimeoutRequest( + linkedTo: entry, + in: timeoutEntry, + duration: timeout, + flags: .relativeTime + ) { + try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) + } + } else { + try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) + } + } + } catch (let e) { + cont.resume(throwing: e) } } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 52407b03..5a1fd03d 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -24,6 +24,8 @@ extension RawIORequest { case sendMessage = 9 case receiveMessage = 10 // ... + case link_timeout = 15 + // ... case openAt = 18 case close = 19 case filesUpdate = 20 @@ -103,7 +105,7 @@ extension RawIORequest { // poll32_events // sync_range_flags // msg_flags - // timeout_flags + case timeoutFlags(TimeOutFlags) // accept_flags // cancel_flags case openFlags(FileDescriptor.OpenOptions) @@ -132,12 +134,48 @@ extension RawIORequest { // append to end of the file public static let append = ReadWriteFlags(rawValue: 1 << 4) } + + public struct TimeOutFlags: OptionSet, Hashable, Codable { + public var rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let relativeTime: RawIORequest.TimeOutFlags = TimeOutFlags(rawValue: 0) + public static let absoluteTime: RawIORequest.TimeOutFlags = TimeOutFlags(rawValue: 1 << 0) + } } extension RawIORequest { static func nop() -> RawIORequest { - var req = RawIORequest() + var req: RawIORequest = RawIORequest() req.operation = .nop return req } + + //TODO: typed errors + static func withTimeoutRequest( + linkedTo opEntry: UnsafeMutablePointer, + in timeoutEntry: UnsafeMutablePointer, + duration: Duration, + flags: TimeOutFlags, + work: () throws -> R) rethrows -> R { + + opEntry.pointee.flags |= Flags.linkRequest.rawValue + opEntry.pointee.off = 1 + var ts = __kernel_timespec( + tv_sec: duration.components.seconds, + tv_nsec: duration.components.attoseconds / 1_000_000_000 + ) + return try withUnsafePointer(to: &ts) { tsPtr in + var req: RawIORequest = RawIORequest() + req.operation = .link_timeout + req.rawValue.timeout_flags = flags.rawValue + req.rawValue.len = 1 + req.rawValue.addr = UInt64(UInt(bitPattern: tsPtr)) + timeoutEntry.pointee = req.rawValue + return try work() + } + } } \ No newline at end of file diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 85846d3e..d0102a0b 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -14,7 +14,7 @@ final class IORingTests: XCTestCase { func testNop() throws { var ring = try IORing(queueDepth: 32) ring.writeRequest(IORequest()) - ring.submitRequests() + try ring.submitRequests() let completion = ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) } diff --git a/Tests/SystemTests/ManagedIORingTests.swift b/Tests/SystemTests/ManagedIORingTests.swift index 4b8ea28b..24324037 100644 --- a/Tests/SystemTests/ManagedIORingTests.swift +++ b/Tests/SystemTests/ManagedIORingTests.swift @@ -13,7 +13,36 @@ final class ManagedIORingTests: XCTestCase { func testNop() async throws { let ring = try ManagedIORing(queueDepth: 32) - let completion = await ring.submitAndWait(IORequest()) + let completion = try await ring.submit(request: IORequest()) XCTAssertEqual(completion.result, 0) } + + func testTimeout() async throws { + let ring = try ManagedIORing(queueDepth: 32) + var pipes: (Int32, Int32) = (0, 0) + withUnsafeMutableBytes(of: &pipes) { ptr in + ptr.withMemoryRebound(to: UInt32.self) { tptr in + let res = pipe(tptr.baseAddress!) + XCTAssertEqual(res, 0) + } + } + let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 128, alignment: 16) + do { + let completion = try await ring.submit( + request: IORequest(reading: FileDescriptor(rawValue: pipes.0), into: buffer), + timeout: .seconds(0.1) + ) + print("\(completion)") + XCTFail("An error should be thrown") + } catch (let e) { + if let err = e as? IORingError { + XCTAssertEqual(err, .operationCanceled) + } else { + XCTFail() + } + buffer.deallocate() + close(pipes.0) + close(pipes.1) + } + } } From 55fd6e7150cb6f48fa0203c1df29746be35b1a00 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 12 Dec 2024 01:20:24 +0000 Subject: [PATCH 022/172] Add support for timeout-on-wait to IORing, don't have tests yet --- Sources/System/IORing.swift | 37 ++++++++++++++++++++-- Sources/System/ManagedIORing.swift | 33 ++++++++++--------- Tests/SystemTests/IORingTests.swift | 2 +- Tests/SystemTests/ManagedIORingTests.swift | 2 +- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 00461d14..872d9960 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -368,13 +368,26 @@ public struct IORing: @unchecked Sendable, ~Copyable { self.ringFlags = params.flags } - public func blockingConsumeCompletion() -> IOCompletion { + private func _blockingConsumeCompletionGuts( + extraArgs: UnsafeMutablePointer? = nil + ) throws(IORingError) -> IOCompletion { completionMutex.withLock { ring in if let completion = _tryConsumeCompletion(ring: &ring) { return completion } else { while true { - let res = io_uring_enter(ringDescriptor, 0, 1, IORING_ENTER_GETEVENTS, nil) + var sz = 0 + if extraArgs != nil { + sz = MemoryLayout.size + } + let res = io_uring_enter2( + ringDescriptor, + 0, + 1, + IORING_ENTER_GETEVENTS, + extraArgs, + sz + ) // error handling: // EAGAIN / EINTR (try again), // EBADF / EBADFD / EOPNOTSUPP / ENXIO @@ -398,6 +411,26 @@ public struct IORing: @unchecked Sendable, ~Copyable { } } + public func blockingConsumeCompletion(timeout: Duration? = nil) throws -> IOCompletion { + if let timeout { + var ts = __kernel_timespec( + tv_sec: timeout.components.seconds, + tv_nsec: timeout.components.attoseconds / 1_000_000_000 + ) + return try withUnsafePointer(to: &ts) { tsPtr in + var args = io_uring_getevents_arg( + sigmask: 0, + sigmask_sz: 0, + pad: 0, + ts: UInt64(UInt(bitPattern: tsPtr)) + ) + return try _blockingConsumeCompletionGuts(extraArgs: &args) + } + } else { + return try _blockingConsumeCompletionGuts() + } + } + public func tryConsumeCompletion() -> IOCompletion? { completionMutex.withLock { ring in return _tryConsumeCompletion(ring: &ring) diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift index 99c112d6..b6df140d 100644 --- a/Sources/System/ManagedIORing.swift +++ b/Sources/System/ManagedIORing.swift @@ -25,7 +25,8 @@ final public class ManagedIORing: @unchecked Sendable { Task.detached { while !Task.isCancelled { //TODO: should timeout handling be sunk into IORing? - let cqe = self.internalRing.blockingConsumeCompletion() + //TODO: sort out the error handling here + let cqe = try! self.internalRing.blockingConsumeCompletion() if cqe.userData == 0 { continue @@ -34,12 +35,6 @@ final public class ManagedIORing: @unchecked Sendable { cqe.userData, to: UnsafeContinuation.self) if cqe.result < 0 { - var err = system_strerror(cqe.result * -1) - let len = system_strlen(err!) - err!.withMemoryRebound(to: UInt8.self, capacity: len) { - let errStr = String(decoding: UnsafeBufferPointer(start: $0, count: len), as: UTF8.self) - print("\(errStr)") - } handleCompletionError(cqe.result, for: cont) } else { cont.resume(returning: cqe) @@ -64,17 +59,21 @@ final public class ManagedIORing: @unchecked Sendable { entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) if let timeout { //TODO: if IORING_FEAT_MIN_TIMEOUT is supported we can do this more efficiently - let timeoutEntry = _blockingGetSubmissionEntry( - ring: &ring, - submissionQueueEntries: internalRing.submissionQueueEntries - ) - try RawIORequest.withTimeoutRequest( - linkedTo: entry, - in: timeoutEntry, - duration: timeout, - flags: .relativeTime - ) { + if true { //replace with IORING_FEAT_MIN_TIMEOUT feature check try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) + } else { + let timeoutEntry = _blockingGetSubmissionEntry( + ring: &ring, + submissionQueueEntries: internalRing.submissionQueueEntries + ) + try RawIORequest.withTimeoutRequest( + linkedTo: entry, + in: timeoutEntry, + duration: timeout, + flags: .relativeTime + ) { + try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) + } } } else { try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index d0102a0b..2c25e2b3 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -15,7 +15,7 @@ final class IORingTests: XCTestCase { var ring = try IORing(queueDepth: 32) ring.writeRequest(IORequest()) try ring.submitRequests() - let completion = ring.blockingConsumeCompletion() + let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) } } diff --git a/Tests/SystemTests/ManagedIORingTests.swift b/Tests/SystemTests/ManagedIORingTests.swift index 24324037..a499f6c6 100644 --- a/Tests/SystemTests/ManagedIORingTests.swift +++ b/Tests/SystemTests/ManagedIORingTests.swift @@ -17,7 +17,7 @@ final class ManagedIORingTests: XCTestCase { XCTAssertEqual(completion.result, 0) } - func testTimeout() async throws { + func testSubmitTimeout() async throws { let ring = try ManagedIORing(queueDepth: 32) var pipes: (Int32, Int32) = (0, 0) withUnsafeMutableBytes(of: &pipes) { ptr in From 85b83fddb16c6ac0254f6e23279e6fd395d20b98 Mon Sep 17 00:00:00 2001 From: xuty Date: Tue, 17 Dec 2024 15:13:15 +0800 Subject: [PATCH 023/172] Use GetTempPathW for better compatibility --- Sources/System/FilePath/FilePathTempWindows.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/FilePath/FilePathTempWindows.swift b/Sources/System/FilePath/FilePathTempWindows.swift index 0d97edcb..d6e45f4f 100644 --- a/Sources/System/FilePath/FilePathTempWindows.swift +++ b/Sources/System/FilePath/FilePathTempWindows.swift @@ -17,7 +17,7 @@ internal func _getTemporaryDirectory() throws -> FilePath { capacity: Int(MAX_PATH) + 1) { buffer in - guard GetTempPath2W(DWORD(buffer.count), buffer.baseAddress) != 0 else { + guard GetTempPathW(DWORD(buffer.count), buffer.baseAddress) != 0 else { throw Errno(windowsError: GetLastError()) } From 7e73a336fb73f94558083d3f64baf71ad74f59ee Mon Sep 17 00:00:00 2001 From: Michael Chiu Date: Fri, 17 Jan 2025 14:30:51 -0800 Subject: [PATCH 024/172] Guard errnos not available on FreeBSD --- Sources/System/Errno.swift | 6 +++--- Sources/System/Internals/Constants.swift | 6 +++--- Sources/System/Internals/Syscalls.swift | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index 093023d0..62233c04 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -1294,7 +1294,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { @available(*, unavailable, renamed: "multiHop") public static var EMULTIHOP: Errno { multiHop } -#if !os(WASI) +#if !os(WASI) && !os(FreeBSD) /// No message available. /// /// No message was available to be received by the requested operation. @@ -1320,7 +1320,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { @available(*, unavailable, renamed: "noLink") public static var ENOLINK: Errno { noLink } -#if !os(WASI) +#if !os(WASI) && !os(FreeBSD) /// Reserved. /// /// This error is reserved for future use. @@ -1361,7 +1361,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { @available(*, unavailable, renamed: "protocolError") public static var EPROTO: Errno { protocolError } -#if !os(OpenBSD) && !os(WASI) +#if !os(FreeBSD) && !os(OpenBSD) && !os(WASI) /// Reserved. /// /// This error is reserved for future use. diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 904e5b22..171d01dc 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -422,7 +422,7 @@ internal var _EBADMSG: CInt { EBADMSG } @_alwaysEmitIntoClient internal var _EMULTIHOP: CInt { EMULTIHOP } -#if !os(WASI) +#if !os(WASI) && !os(FreeBSD) @_alwaysEmitIntoClient internal var _ENODATA: CInt { ENODATA } #endif @@ -430,7 +430,7 @@ internal var _ENODATA: CInt { ENODATA } @_alwaysEmitIntoClient internal var _ENOLINK: CInt { ENOLINK } -#if !os(WASI) +#if !os(WASI) && !os(FreeBSD) @_alwaysEmitIntoClient internal var _ENOSR: CInt { ENOSR } @@ -442,7 +442,7 @@ internal var _ENOSTR: CInt { ENOSTR } @_alwaysEmitIntoClient internal var _EPROTO: CInt { EPROTO } -#if !os(OpenBSD) && !os(WASI) +#if !os(OpenBSD) && !os(WASI) && !os(FreeBSD) @_alwaysEmitIntoClient internal var _ETIME: CInt { ETIME } #endif diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index 6f885be5..e90e1d19 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -191,7 +191,7 @@ internal func system_confstr( internal let SYSTEM_AT_REMOVE_DIR = AT_REMOVEDIR internal let SYSTEM_DT_DIR = DT_DIR internal typealias system_dirent = dirent -#if os(Linux) || os(Android) +#if os(Linux) || os(Android) || os(FreeBSD) internal typealias system_DIRPtr = OpaquePointer #else internal typealias system_DIRPtr = UnsafeMutablePointer From a4fb6cb206d4c6b44db3dabaca760015a9f9b2fb Mon Sep 17 00:00:00 2001 From: Michael Chiu Date: Fri, 17 Jan 2025 14:43:34 -0800 Subject: [PATCH 025/172] Keep order consistent --- Sources/System/Errno.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index 62233c04..d9fe1c81 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -1361,7 +1361,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { @available(*, unavailable, renamed: "protocolError") public static var EPROTO: Errno { protocolError } -#if !os(FreeBSD) && !os(OpenBSD) && !os(WASI) +#if !os(OpenBSD) && !os(WASI) && !os(FreeBSD) /// Reserved. /// /// This error is reserved for future use. From eb566de2d35ddcf3fc485070f2b03def8e8c1506 Mon Sep 17 00:00:00 2001 From: Michael Chiu Date: Fri, 17 Jan 2025 14:43:40 -0800 Subject: [PATCH 026/172] Fix tests on FreeBSD --- Tests/SystemTests/ErrnoTest.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/ErrnoTest.swift b/Tests/SystemTests/ErrnoTest.swift index 80e3e84f..c900f907 100644 --- a/Tests/SystemTests/ErrnoTest.swift +++ b/Tests/SystemTests/ErrnoTest.swift @@ -164,12 +164,14 @@ final class ErrnoTest: XCTestCase { #if !os(Windows) XCTAssert(EBADMSG == Errno.badMessage.rawValue) XCTAssert(EMULTIHOP == Errno.multiHop.rawValue) - XCTAssert(ENODATA == Errno.noData.rawValue) XCTAssert(ENOLINK == Errno.noLink.rawValue) + XCTAssert(EPROTO == Errno.protocolError.rawValue) +#if !os(FreeBSD) + XCTAssert(ENODATA == Errno.noData.rawValue) XCTAssert(ENOSR == Errno.noStreamResources.rawValue) XCTAssert(ENOSTR == Errno.notStream.rawValue) - XCTAssert(EPROTO == Errno.protocolError.rawValue) XCTAssert(ETIME == Errno.timeout.rawValue) +#endif #endif XCTAssert(EOPNOTSUPP == Errno.notSupportedOnSocket.rawValue) From 0d824eee86e9b42cb11cca3617898808600bc2a9 Mon Sep 17 00:00:00 2001 From: Michael Chiu Date: Fri, 17 Jan 2025 15:06:28 -0800 Subject: [PATCH 027/172] refactor ifdef in test --- Tests/SystemTests/ErrnoTest.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/ErrnoTest.swift b/Tests/SystemTests/ErrnoTest.swift index c900f907..3724e144 100644 --- a/Tests/SystemTests/ErrnoTest.swift +++ b/Tests/SystemTests/ErrnoTest.swift @@ -166,13 +166,15 @@ final class ErrnoTest: XCTestCase { XCTAssert(EMULTIHOP == Errno.multiHop.rawValue) XCTAssert(ENOLINK == Errno.noLink.rawValue) XCTAssert(EPROTO == Errno.protocolError.rawValue) -#if !os(FreeBSD) +#endif + +#if !os(Windows) && !os(FreeBSD) XCTAssert(ENODATA == Errno.noData.rawValue) XCTAssert(ENOSR == Errno.noStreamResources.rawValue) XCTAssert(ENOSTR == Errno.notStream.rawValue) XCTAssert(ETIME == Errno.timeout.rawValue) #endif -#endif + XCTAssert(EOPNOTSUPP == Errno.notSupportedOnSocket.rawValue) // From headers but not man page From bebd7c146e1c692b1fcbec9da8306ee2777d034a Mon Sep 17 00:00:00 2001 From: Fabrice de Gans Date: Thu, 23 Jan 2025 11:45:17 -0800 Subject: [PATCH 028/172] [cmake] Install libraries in standard directories Previously, libs were installed under `lib/swift/${os}`. They should be installed in the default library directory for the relevant target system. In addition, swiftmodules were installed in the older layout format. This changes to use the standard modern layout format for swiftmodules. --- CMakeLists.txt | 2 ++ cmake/modules/SwiftSupport.cmake | 36 ++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ed4f5d1..11347f59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,8 @@ cmake_minimum_required(VERSION 3.16.0) project(SwiftSystem LANGUAGES C Swift) +include(GNUInstallDirs) + list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 8f1e2c4d..cd6c1806 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -69,6 +69,28 @@ function(get_swift_host_os result_var_name) endif() endfunction() +if(NOT Swift_MODULE_TRIPLE) + # Attempt to get the module triple from the Swift compiler. + set(module_triple_command "${CMAKE_Swift_COMPILER}" -print-target-info) + if(CMAKE_Swift_COMPILER_TARGET) + list(APPEND module_triple_command -target ${CMAKE_Swift_COMPILER_TARGET}) + endif() + execute_process(COMMAND ${module_triple_command} + OUTPUT_VARIABLE target_info_json) + string(JSON module_triple GET "${target_info_json}" "target" "moduleTriple") + + # Exit now if we failed to infer the triple. + if(NOT module_triple) + message(FATAL_ERROR + "Failed to get module triple from Swift compiler. " + "Compiler output: ${target_info_json}") + endif() + + # Cache the module triple for future use. + set(Swift_MODULE_TRIPLE "${module_triple}" CACHE STRING "swift module triple used for installed swiftmodule and swiftinterface files") + mark_as_advanced(Swift_MODULE_TRIPLE) +endif() + function(_install_target module) get_swift_host_os(swift_os) get_target_property(type ${module} TYPE) @@ -79,24 +101,20 @@ function(_install_target module) set(swift swift) endif() - install(TARGETS ${module} - ARCHIVE DESTINATION lib/${swift}/${swift_os} - LIBRARY DESTINATION lib/${swift}/${swift_os} - RUNTIME DESTINATION bin) + install(TARGETS ${module}) if(type STREQUAL EXECUTABLE) return() endif() - get_swift_host_arch(swift_arch) get_target_property(module_name ${module} Swift_MODULE_NAME) if(NOT module_name) set(module_name ${module}) endif() install(FILES $/${module_name}.swiftdoc - DESTINATION lib/${swift}/${swift_os}/${module_name}.swiftmodule - RENAME ${swift_arch}.swiftdoc) + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${Swift_MODULE_TRIPLE}.swiftdoc) install(FILES $/${module_name}.swiftmodule - DESTINATION lib/${swift}/${swift_os}/${module_name}.swiftmodule - RENAME ${swift_arch}.swiftmodule) + DESTINATION ${CMAKE_INSTALL_LIBDIR}/${swift}/${swift_os}/${module_name}.swiftmodule + RENAME ${Swift_MODULE_TRIPLE}.swiftmodule) endfunction() From 4d735a663154321e9f2fb609146fae979ab86ee9 Mon Sep 17 00:00:00 2001 From: Michael Chiu Date: Sun, 26 Jan 2025 21:29:13 -0800 Subject: [PATCH 029/172] Add FreeBSD errnos and open options --- Sources/System/Errno.swift | 40 ++++++++++++++++++++++-- Sources/System/FileDescriptor.swift | 33 +++++++++++++++++-- Sources/System/Internals/Constants.swift | 31 +++++++++++++++--- Tests/SystemTests/ErrnoTest.swift | 11 ++++++- Tests/SystemTests/FileTypesTest.swift | 11 +++++-- 5 files changed, 114 insertions(+), 12 deletions(-) diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index d9fe1c81..e88b96d9 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -1067,7 +1067,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { public static var ENOSYS: Errno { noFunction } // BSD -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Inappropriate file type or format. /// /// The file was the wrong type for the operation, @@ -1082,7 +1082,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { public static var EFTYPE: Errno { badFileTypeOrFormat } #endif -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Authentication error. /// /// The authentication ticket used to mount an NFS file system was invalid. @@ -1253,7 +1253,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { @available(*, unavailable, renamed: "illegalByteSequence") public static var EILSEQ: Errno { illegalByteSequence } -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Attribute not found. /// /// The specified extended attribute doesn't exist. @@ -1459,6 +1459,38 @@ extension Errno { public static var EOWNERDEAD: Errno { previousOwnerDied } #endif +#if os(FreeBSD) + /// Capabilities insufficient. + /// + /// The corresponding C error is `ENOTCAPABLE`. + @_alwaysEmitIntoClient + public static var notCapable: Errno { .init(rawValue: _ENOTCAPABLE) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "notCapable") + public static var ENOTCAPABLE: Errno { notCapable } + + /// Not permitted in capability mode. + /// + /// The corresponding C error is `ECAPMODE`. + @_alwaysEmitIntoClient + public static var capabilityMode: Errno { .init(rawValue: _ECAPMODE) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "capabilityMode") + public static var ECAPMODE: Errno { capabilityMode } + + /// Integrity check failed. + /// + /// The corresponding C error is `EINTEGRITY`. + @_alwaysEmitIntoClient + public static var integrityCheckFailed: Errno { .init(rawValue: _EINTEGRITY) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "integrityCheckFailed") + public static var EINTEGRITY: Errno { integrityCheckFailed } +#endif + #if SYSTEM_PACKAGE_DARWIN /// Interface output queue is full. /// @@ -1469,7 +1501,9 @@ extension Errno { @_alwaysEmitIntoClient @available(*, unavailable, renamed: "outputQueueFull") public static var EQFULL: Errno { outputQueueFull } +#endif +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// The largest valid error. /// /// This value is the largest valid value diff --git a/Sources/System/FileDescriptor.swift b/Sources/System/FileDescriptor.swift index d7b931eb..c9953e8b 100644 --- a/Sources/System/FileDescriptor.swift +++ b/Sources/System/FileDescriptor.swift @@ -182,7 +182,7 @@ extension FileDescriptor { @available(*, unavailable, renamed: "exclusiveCreate") public static var O_EXCL: OpenOptions { exclusiveCreate } -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Indicates that opening the file /// atomically obtains a shared lock on the file. /// @@ -250,6 +250,22 @@ extension FileDescriptor { public static var O_DIRECTORY: OpenOptions { directory } #endif +#if os(FreeBSD) + /// Indicates that each write operation is synchronous. + /// + /// If this option is specified, + /// each time you write to the file, + /// the new data is written immediately and synchronously to the disk. + /// + /// The corresponding C constant is `O_SYNC`. + @_alwaysEmitIntoClient + public static var sync: OpenOptions { .init(rawValue: _O_SYNC) } + + @_alwaysEmitIntoClient + @available(*, unavailable, renamed: "sync") + public static var O_SYNC: OpenOptions { sync } +#endif + #if SYSTEM_PACKAGE_DARWIN /// Indicates that opening the file /// opens symbolic links instead of following them. @@ -354,7 +370,7 @@ extension FileDescriptor { // TODO: These are available on some versions of Linux with appropriate // macro defines. -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) /// Indicates that the offset should be set /// to the next hole after the specified number of bytes. /// @@ -456,6 +472,19 @@ extension FileDescriptor.OpenOptions (.truncate, ".truncate"), (.exclusiveCreate, ".exclusiveCreate"), ] +#elseif os(FreeBSD) + let descriptions: [(Element, StaticString)] = [ + (.nonBlocking, ".nonBlocking"), + (.append, ".append"), + (.create, ".create"), + (.truncate, ".truncate"), + (.exclusiveCreate, ".exclusiveCreate"), + (.sharedLock, ".sharedLock"), + (.exclusiveLock, ".exclusiveLock"), + (.sync, ".sync"), + (.noFollow, ".noFollow"), + (.closeOnExec, ".closeOnExec") + ] #else let descriptions: [(Element, StaticString)] = [ (.nonBlocking, ".nonBlocking"), diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 171d01dc..d8cbdcbd 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -359,7 +359,7 @@ internal var _ENOLCK: CInt { ENOLCK } @_alwaysEmitIntoClient internal var _ENOSYS: CInt { ENOSYS } -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient internal var _EFTYPE: CInt { EFTYPE } @@ -368,7 +368,9 @@ internal var _EAUTH: CInt { EAUTH } @_alwaysEmitIntoClient internal var _ENEEDAUTH: CInt { ENEEDAUTH } +#endif +#if SYSTEM_PACKAGE_DARWIN @_alwaysEmitIntoClient internal var _EPWROFF: CInt { EPWROFF } @@ -409,7 +411,7 @@ internal var _ENOMSG: CInt { ENOMSG } @_alwaysEmitIntoClient internal var _EILSEQ: CInt { EILSEQ } -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient internal var _ENOATTR: CInt { ENOATTR } #endif @@ -471,10 +473,23 @@ internal var _ENOTRECOVERABLE: CInt { ENOTRECOVERABLE } internal var _EOWNERDEAD: CInt { EOWNERDEAD } #endif +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _ENOTCAPABLE: CInt { ENOTCAPABLE } + +@_alwaysEmitIntoClient +internal var _ECAPMODE: CInt { ECAPMODE } + +@_alwaysEmitIntoClient +internal var _EINTEGRITY: CInt { EINTEGRITY } +#endif + #if SYSTEM_PACKAGE_DARWIN @_alwaysEmitIntoClient internal var _EQFULL: CInt { EQFULL } +#endif +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient internal var _ELAST: CInt { ELAST } #endif @@ -524,7 +539,7 @@ internal var _O_APPEND: CInt { #endif } -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient internal var _O_SHLOCK: CInt { O_SHLOCK } @@ -543,6 +558,14 @@ internal var _O_ASYNC: CInt { O_ASYNC } internal var _O_NOFOLLOW: CInt { O_NOFOLLOW } #endif +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _O_FSYNC: CInt { O_FSYNC } + +@_alwaysEmitIntoClient +internal var _O_SYNC: CInt { O_SYNC } +#endif + @_alwaysEmitIntoClient internal var _O_CREAT: CInt { #if os(WASI) @@ -609,7 +632,7 @@ internal var _SEEK_CUR: CInt { SEEK_CUR } @_alwaysEmitIntoClient internal var _SEEK_END: CInt { SEEK_END } -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient internal var _SEEK_HOLE: CInt { SEEK_HOLE } diff --git a/Tests/SystemTests/ErrnoTest.swift b/Tests/SystemTests/ErrnoTest.swift index 3724e144..da09657e 100644 --- a/Tests/SystemTests/ErrnoTest.swift +++ b/Tests/SystemTests/ErrnoTest.swift @@ -131,10 +131,13 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENOLCK == Errno.noLocks.rawValue) XCTAssert(ENOSYS == Errno.noFunction.rawValue) -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) XCTAssert(EFTYPE == Errno.badFileTypeOrFormat.rawValue) XCTAssert(EAUTH == Errno.authenticationError.rawValue) XCTAssert(ENEEDAUTH == Errno.needAuthenticator.rawValue) +#endif + +#if SYSTEM_PACKAGE_DARWIN XCTAssert(EPWROFF == Errno.devicePowerIsOff.rawValue) XCTAssert(EDEVERR == Errno.deviceError.rawValue) #endif @@ -196,6 +199,12 @@ final class ErrnoTest: XCTestCase { XCTAssert(EOWNERDEAD == Errno.previousOwnerDied.rawValue) #endif +#if os(FreeBSD) + XCTAssert(ENOTCAPABLE == Errno.notCapable.rawValue) + XCTAssert(ECAPMODE == Errno.capabilityMode.rawValue) + XCTAssert(EINTEGRITY == Errno.integrityCheckFailed.rawValue) +#endif + #if SYSTEM_PACKAGE_DARWIN XCTAssert(EQFULL == Errno.outputQueueFull.rawValue) XCTAssert(ELAST == Errno.lastErrnoValue.rawValue) diff --git a/Tests/SystemTests/FileTypesTest.swift b/Tests/SystemTests/FileTypesTest.swift index 620cfd70..58ceac1b 100644 --- a/Tests/SystemTests/FileTypesTest.swift +++ b/Tests/SystemTests/FileTypesTest.swift @@ -42,18 +42,25 @@ final class FileDescriptorTest: XCTestCase { #endif // BSD only -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) XCTAssertEqual(O_SHLOCK, FileDescriptor.OpenOptions.sharedLock.rawValue) XCTAssertEqual(O_EXLOCK, FileDescriptor.OpenOptions.exclusiveLock.rawValue) +#endif + +#if SYSTEM_PACKAGE_DARWIN XCTAssertEqual(O_SYMLINK, FileDescriptor.OpenOptions.symlink.rawValue) XCTAssertEqual(O_EVTONLY, FileDescriptor.OpenOptions.eventOnly.rawValue) #endif +#if os(FreeBSD) + XCTAssertEqual(O_SYNC, FileDescriptor.OpenOptions.sync.rawValue) +#endif + XCTAssertEqual(SEEK_SET, FileDescriptor.SeekOrigin.start.rawValue) XCTAssertEqual(SEEK_CUR, FileDescriptor.SeekOrigin.current.rawValue) XCTAssertEqual(SEEK_END, FileDescriptor.SeekOrigin.end.rawValue) -#if SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) XCTAssertEqual(SEEK_HOLE, FileDescriptor.SeekOrigin.nextHole.rawValue) XCTAssertEqual(SEEK_DATA, FileDescriptor.SeekOrigin.nextData.rawValue) #endif From df7341ee2b553b09dee6f82bb3b0c84e9b6f0441 Mon Sep 17 00:00:00 2001 From: Michael Chiu Date: Sun, 26 Jan 2025 21:29:19 -0800 Subject: [PATCH 030/172] fix cmake x86_64 build --- cmake/modules/SwiftSupport.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 8f1e2c4d..19cf0223 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -36,7 +36,7 @@ function(get_swift_host_arch result_var_name) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7-a") set("${result_var_name}" "armv7" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "amd64") - set("${result_var_name}" "amd64" PARENT_SCOPE) + set("${result_var_name}" "x86_64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64") set("${result_var_name}" "x86_64" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "IA64") From 90702d6d15678f3c3a715b2d678af128b4bc1edb Mon Sep 17 00:00:00 2001 From: 3405691582 Date: Tue, 4 Feb 2025 19:37:18 -0500 Subject: [PATCH 031/172] Typealias `system_DIRPtr` for OpenBSD. --- Sources/System/Internals/Syscalls.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index e90e1d19..1627273c 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -191,7 +191,7 @@ internal func system_confstr( internal let SYSTEM_AT_REMOVE_DIR = AT_REMOVEDIR internal let SYSTEM_DT_DIR = DT_DIR internal typealias system_dirent = dirent -#if os(Linux) || os(Android) || os(FreeBSD) +#if os(Linux) || os(Android) || os(FreeBSD) || os(OpenBSD) internal typealias system_DIRPtr = OpaquePointer #else internal typealias system_DIRPtr = UnsafeMutablePointer From 3b053e055b2288c8e05b88b6f55fe225d9465195 Mon Sep 17 00:00:00 2001 From: 3405691582 Date: Tue, 4 Feb 2025 19:37:55 -0500 Subject: [PATCH 032/172] ManagedBuffer.capacity is unavailable on OpenBSD. `.capacity` is backed by malloc introspection (`malloc_size`), which is not available on all platforms. The workaround is straightforward here, thankfully: instead, use the minimum capacity already specified. --- Sources/System/Internals/RawBuffer.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/System/Internals/RawBuffer.swift b/Sources/System/Internals/RawBuffer.swift index 7f9461c3..83161c47 100644 --- a/Sources/System/Internals/RawBuffer.swift +++ b/Sources/System/Internals/RawBuffer.swift @@ -80,7 +80,13 @@ extension _RawBuffer { internal static func create(minimumCapacity: Int) -> Storage { Storage.create( minimumCapacity: minimumCapacity, - makingHeaderWith: { $0.capacity } + makingHeaderWith: { +#if os(OpenBSD) + minimumCapacity +#else + $0.capacity +#endif + } ) as! Storage } } From e5fdf9ea96ff918aac8d8f1667909c74c1bc3b7c Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 6 Feb 2025 00:41:30 +0000 Subject: [PATCH 033/172] Remove managed abstractions for now --- Sources/System/AsyncFileDescriptor.swift | 161 ------------------ Sources/System/ManagedIORing.swift | 97 ----------- .../AsyncFileDescriptorTests.swift | 58 ------- Tests/SystemTests/ManagedIORingTests.swift | 48 ------ 4 files changed, 364 deletions(-) delete mode 100644 Sources/System/AsyncFileDescriptor.swift delete mode 100644 Sources/System/ManagedIORing.swift delete mode 100644 Tests/SystemTests/AsyncFileDescriptorTests.swift delete mode 100644 Tests/SystemTests/ManagedIORingTests.swift diff --git a/Sources/System/AsyncFileDescriptor.swift b/Sources/System/AsyncFileDescriptor.swift deleted file mode 100644 index a76d90e5..00000000 --- a/Sources/System/AsyncFileDescriptor.swift +++ /dev/null @@ -1,161 +0,0 @@ -@_implementationOnly import CSystem - -public struct AsyncFileDescriptor: ~Copyable { - @usableFromInline var open: Bool = true - @usableFromInline let fileSlot: IORingFileSlot - @usableFromInline let ring: ManagedIORing - - public static func open( - path: FilePath, - in directory: FileDescriptor = FileDescriptor(rawValue: -100), - on ring: ManagedIORing, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil - ) async throws -> AsyncFileDescriptor { - // todo; real error type - guard let fileSlot = ring.getFileSlot() else { - throw IORingError.missingRequiredFeatures - } - //TODO: need an async-friendly withCString - let cstr = path.withCString { - return $0 // bad - } - let res = try await ring.submit(request: IORequest( - opening: cstr, - in: directory, - into: fileSlot, - mode: mode, - options: options, - permissions: permissions - )) - if res.result < 0 { - throw Errno(rawValue: -res.result) - } - - return AsyncFileDescriptor( - fileSlot, ring: ring - ) - } - - internal init(_ fileSlot: consuming IORingFileSlot, ring: ManagedIORing) { - self.fileSlot = consume fileSlot - self.ring = ring - } - - @inlinable @inline(__always) - public consuming func close(isolation actor: isolated (any Actor)? = #isolation) async throws { - let res = try await ring.submit(request: IORequest(closing: fileSlot)) - if res.result < 0 { - throw Errno(rawValue: -res.result) - } - self.open = false - } - - @inlinable @inline(__always) - public func read( - into buffer: inout UnsafeMutableRawBufferPointer, - atAbsoluteOffset offset: UInt64 = UInt64.max, - isolation actor: isolated (any Actor)? = #isolation - ) async throws -> UInt32 { - let res = try await ring.submit(request: IORequest( - reading: fileSlot, - into: buffer, - at: offset - )) - if res.result < 0 { - throw Errno(rawValue: -res.result) - } else { - return UInt32(bitPattern: res.result) - } - } - - @inlinable @inline(__always) - public func read( - into buffer: IORingBuffer, //TODO: should be inout? - atAbsoluteOffset offset: UInt64 = UInt64.max, - isolation actor: isolated (any Actor)? = #isolation - ) async throws -> UInt32 { - let res = try await ring.submit(request: IORequest( - reading: fileSlot, - into: buffer, - at: offset - )) - if res.result < 0 { - throw Errno(rawValue: -res.result) - } else { - return UInt32(bitPattern: res.result) - } - } - - //TODO: temporary workaround until AsyncSequence supports ~Copyable - public consuming func toBytes() -> AsyncFileDescriptorSequence { - AsyncFileDescriptorSequence(self) - } - - //TODO: can we do the linear types thing and error if they don't consume it manually? - // deinit { - // if self.open { - // close() - // // TODO: close or error? TBD - // } - // } -} - -public class AsyncFileDescriptorSequence: AsyncSequence { - var descriptor: AsyncFileDescriptor? - - public func makeAsyncIterator() -> FileIterator { - return .init(descriptor.take()!) - } - - internal init(_ descriptor: consuming AsyncFileDescriptor) { - self.descriptor = consume descriptor - } - - public typealias AsyncIterator = FileIterator - public typealias Element = UInt8 -} - -//TODO: only a class due to ~Copyable limitations -public class FileIterator: AsyncIteratorProtocol { - @usableFromInline let file: AsyncFileDescriptor - @usableFromInline var buffer: IORingBuffer - @usableFromInline var done: Bool - - @usableFromInline internal var currentByte: UnsafeRawPointer? - @usableFromInline internal var lastByte: UnsafeRawPointer? - - init(_ file: consuming AsyncFileDescriptor) { - self.buffer = file.ring.getBuffer()! - self.file = file - self.done = false - } - - @inlinable @inline(__always) - public func nextBuffer() async throws { - let bytesRead = Int(try await file.read(into: buffer)) - if _fastPath(bytesRead != 0) { - let unsafeBuffer = buffer.unsafeBuffer - let bufPointer = unsafeBuffer.baseAddress.unsafelyUnwrapped - self.currentByte = UnsafeRawPointer(bufPointer) - self.lastByte = UnsafeRawPointer(bufPointer.advanced(by: bytesRead)) - } else { - done = true - } - } - - @inlinable @inline(__always) - public func next() async throws -> UInt8? { - if _fastPath(currentByte != lastByte) { - // SAFETY: both pointers should be non-nil if they're not equal - let byte = currentByte.unsafelyUnwrapped.load(as: UInt8.self) - currentByte = currentByte.unsafelyUnwrapped + 1 - return byte - } else if done { - return nil - } - try await nextBuffer() - return try await next() - } -} diff --git a/Sources/System/ManagedIORing.swift b/Sources/System/ManagedIORing.swift deleted file mode 100644 index b6df140d..00000000 --- a/Sources/System/ManagedIORing.swift +++ /dev/null @@ -1,97 +0,0 @@ -fileprivate func handleCompletionError( - _ result: Int32, - for continuation: UnsafeContinuation) { - var error: IORingError = .unknown - switch result { - case -(_ECANCELED): - error = .operationCanceled - default: - error = .unknown - } - continuation.resume(throwing: error) -} - -final public class ManagedIORing: @unchecked Sendable { - var internalRing: IORing - - public init(queueDepth: UInt32) throws { - self.internalRing = try IORing(queueDepth: queueDepth) - self.internalRing.registerBuffers(bufSize: 655336, count: 4) - self.internalRing.registerFiles(count: 32) - self.startWaiter() - } - - private func startWaiter() { - Task.detached { - while !Task.isCancelled { - //TODO: should timeout handling be sunk into IORing? - //TODO: sort out the error handling here - let cqe = try! self.internalRing.blockingConsumeCompletion() - - if cqe.userData == 0 { - continue - } - let cont = unsafeBitCast( - cqe.userData, to: UnsafeContinuation.self) - - if cqe.result < 0 { - handleCompletionError(cqe.result, for: cont) - } else { - cont.resume(returning: cqe) - } - } - } - } - - public func submit( - request: __owned IORequest, - timeout: Duration? = nil, - isolation actor: isolated (any Actor)? = #isolation - ) async throws -> IOCompletion { - var consumeOnceWorkaround: IORequest? = request - return try await withUnsafeThrowingContinuation { cont in - do { - try internalRing.submissionMutex.withLock { ring in - let request = consumeOnceWorkaround.take()! - let entry = _blockingGetSubmissionEntry( - ring: &ring, submissionQueueEntries: internalRing.submissionQueueEntries) - entry.pointee = request.makeRawRequest().rawValue - entry.pointee.user_data = unsafeBitCast(cont, to: UInt64.self) - if let timeout { - //TODO: if IORING_FEAT_MIN_TIMEOUT is supported we can do this more efficiently - if true { //replace with IORING_FEAT_MIN_TIMEOUT feature check - try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) - } else { - let timeoutEntry = _blockingGetSubmissionEntry( - ring: &ring, - submissionQueueEntries: internalRing.submissionQueueEntries - ) - try RawIORequest.withTimeoutRequest( - linkedTo: entry, - in: timeoutEntry, - duration: timeout, - flags: .relativeTime - ) { - try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) - } - } - } else { - try _submitRequests(ring: &ring, ringDescriptor: internalRing.ringDescriptor) - } - } - } catch (let e) { - cont.resume(throwing: e) - } - } - - } - - internal func getFileSlot() -> IORingFileSlot? { - internalRing.getFile() - } - - internal func getBuffer() -> IORingBuffer? { - internalRing.getBuffer() - } - -} diff --git a/Tests/SystemTests/AsyncFileDescriptorTests.swift b/Tests/SystemTests/AsyncFileDescriptorTests.swift deleted file mode 100644 index ebd308fc..00000000 --- a/Tests/SystemTests/AsyncFileDescriptorTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -import XCTest - -#if SYSTEM_PACKAGE -import SystemPackage -#else -import System -#endif - -final class AsyncFileDescriptorTests: XCTestCase { - func testOpen() async throws { - let ring = try ManagedIORing(queueDepth: 32) - _ = try await AsyncFileDescriptor.open( - path: "/dev/zero", - on: ring, - mode: .readOnly - ) - } - - func testOpenClose() async throws { - let ring = try ManagedIORing(queueDepth: 32) - let file = try await AsyncFileDescriptor.open( - path: "/dev/zero", - on: ring, - mode: .readOnly - ) - try await file.close() - } - - func testDevNullEmpty() async throws { - let ring = try ManagedIORing(queueDepth: 32) - let file = try await AsyncFileDescriptor.open( - path: "/dev/null", - on: ring, - mode: .readOnly - ) - for try await _ in file.toBytes() { - XCTFail("/dev/null should be empty") - } - } - - func testRead() async throws { - let ring = try ManagedIORing(queueDepth: 32) - let file = try await AsyncFileDescriptor.open( - path: "/dev/zero", - on: ring, - mode: .readOnly - ) - let bytes = file.toBytes() - var counter = 0 - for try await byte in bytes { - XCTAssert(byte == 0) - counter &+= 1 - if counter > 16384 { - break - } - } - } -} diff --git a/Tests/SystemTests/ManagedIORingTests.swift b/Tests/SystemTests/ManagedIORingTests.swift deleted file mode 100644 index a499f6c6..00000000 --- a/Tests/SystemTests/ManagedIORingTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -import XCTest - -#if SYSTEM_PACKAGE -import SystemPackage -#else -import System -#endif - -final class ManagedIORingTests: XCTestCase { - func testInit() throws { - _ = try ManagedIORing(queueDepth: 32) - } - - func testNop() async throws { - let ring = try ManagedIORing(queueDepth: 32) - let completion = try await ring.submit(request: IORequest()) - XCTAssertEqual(completion.result, 0) - } - - func testSubmitTimeout() async throws { - let ring = try ManagedIORing(queueDepth: 32) - var pipes: (Int32, Int32) = (0, 0) - withUnsafeMutableBytes(of: &pipes) { ptr in - ptr.withMemoryRebound(to: UInt32.self) { tptr in - let res = pipe(tptr.baseAddress!) - XCTAssertEqual(res, 0) - } - } - let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 128, alignment: 16) - do { - let completion = try await ring.submit( - request: IORequest(reading: FileDescriptor(rawValue: pipes.0), into: buffer), - timeout: .seconds(0.1) - ) - print("\(completion)") - XCTFail("An error should be thrown") - } catch (let e) { - if let err = e as? IORingError { - XCTAssertEqual(err, .operationCanceled) - } else { - XCTFail() - } - buffer.deallocate() - close(pipes.0) - close(pipes.1) - } - } -} From fdbcecab0bf531d5f739f5414969599b9be663aa Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 6 Feb 2025 00:42:21 +0000 Subject: [PATCH 034/172] Eliminate internal locking and add multiple consume support --- Sources/CSystem/include/io_uring.h | 10 +- Sources/System/IORing.swift | 223 ++++++++++++++++++----------- 2 files changed, 150 insertions(+), 83 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index 5c05ed8b..5cab757c 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -45,11 +45,17 @@ int io_uring_setup(unsigned int entries, struct io_uring_params *p) return syscall(__NR_io_uring_setup, entries, p); } +int io_uring_enter2(int fd, unsigned int to_submit, unsigned int min_complete, + unsigned int flags, void *args, size_t sz) +{ + return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, + flags, args, _NSIG / 8); +} + int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig) { - return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, - flags, sig, _NSIG / 8); + return io_uring_enter2(fd, to_submit, min_complete, flags, sig, _NSIG / 8); } #endif diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 872d9960..0328538f 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -51,7 +51,7 @@ internal final class ResourceManager: @unchecked Sendable { struct Resources { let resourceList: UnsafeMutableBufferPointer - var freeList: [Int] //TODO: bitvector? + var freeList: [Int] //TODO: bitvector? } let mutex: Mutex @@ -125,21 +125,27 @@ extension IORingBuffer { } @inlinable @inline(__always) -internal func _writeRequest(_ request: __owned RawIORequest, ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) +internal func _writeRequest( + _ request: __owned RawIORequest, ring: inout SQRing, + submissionQueueEntries: UnsafeMutableBufferPointer +) -> Bool { - let entry = _blockingGetSubmissionEntry(ring: &ring, submissionQueueEntries: submissionQueueEntries) + let entry = _blockingGetSubmissionEntry( + ring: &ring, submissionQueueEntries: submissionQueueEntries) entry.pointee = request.rawValue return true } @inlinable @inline(__always) -internal func _blockingGetSubmissionEntry(ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) -> UnsafeMutablePointer< +internal func _blockingGetSubmissionEntry( + ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer +) -> UnsafeMutablePointer< io_uring_sqe > { while true { if let entry = _getSubmissionEntry( - ring: &ring, + ring: &ring, submissionQueueEntries: submissionQueueEntries ) { return entry @@ -152,12 +158,12 @@ internal func _blockingGetSubmissionEntry(ring: inout SQRing, submissionQueueEnt //TODO: omitting signal mask for now //Tell the kernel that we've submitted requests and/or are waiting for completions internal func _enter( - ringDescriptor: Int32, + ringDescriptor: Int32, numEvents: UInt32, minCompletions: UInt32, flags: UInt32 ) throws -> Int32 { - // Ring always needs enter right now; + // Ring always needs enter right now; // TODO: support SQPOLL here while true { let ret = io_uring_enter(ringDescriptor, numEvents, minCompletions, flags, nil) @@ -180,32 +186,36 @@ internal func _enter( } } -internal func _submitRequests(ring: inout SQRing, ringDescriptor: Int32) throws { - let flushedEvents = _flushQueue(ring: &ring) - _ = try _enter(ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) +internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws { + let flushedEvents = _flushQueue(ring: ring) + _ = try _enter( + ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) } -internal func _getUnconsumedSubmissionCount(ring: inout SQRing) -> UInt32 { +internal func _getUnconsumedSubmissionCount(ring: borrowing SQRing) -> UInt32 { return ring.userTail - ring.kernelHead.pointee.load(ordering: .acquiring) } -internal func _getUnconsumedCompletionCount(ring: inout CQRing) -> UInt32 { - return ring.kernelTail.pointee.load(ordering: .acquiring) - ring.kernelHead.pointee.load(ordering: .acquiring) +internal func _getUnconsumedCompletionCount(ring: borrowing CQRing) -> UInt32 { + return ring.kernelTail.pointee.load(ordering: .acquiring) + - ring.kernelHead.pointee.load(ordering: .acquiring) } //TODO: pretty sure this is supposed to do more than it does -internal func _flushQueue(ring: inout SQRing) -> UInt32 { +internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { ring.kernelTail.pointee.store( ring.userTail, ordering: .releasing ) - return _getUnconsumedSubmissionCount(ring: &ring) + return _getUnconsumedSubmissionCount(ring: ring) } @usableFromInline @inline(__always) -internal func _getSubmissionEntry(ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer) -> UnsafeMutablePointer< +internal func _getSubmissionEntry( + ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer +) -> UnsafeMutablePointer< io_uring_sqe >? { - let next = ring.userTail &+ 1 //this is expected to wrap + let next = ring.userTail &+ 1 //this is expected to wrap // FEAT: smp load when SQPOLL in use (not in MVP) let kernelHead = ring.kernelHead.pointee.load(ordering: .acquiring) @@ -227,15 +237,15 @@ internal func _getSubmissionEntry(ring: inout SQRing, submissionQueueEntries: Un return nil } -public struct IORing: @unchecked Sendable, ~Copyable { +public struct IORing: ~Copyable { let ringFlags: UInt32 let ringDescriptor: Int32 - @usableFromInline let submissionMutex: Mutex + @usableFromInline var submissionRing: SQRing // FEAT: set this eventually let submissionPolling: Bool = false - let completionMutex: Mutex + let completionRing: CQRing @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer @@ -362,82 +372,136 @@ public struct IORing: @unchecked Sendable, ~Copyable { ) ) - self.submissionMutex = Mutex(submissionRing) - self.completionMutex = Mutex(completionRing) + self.submissionRing = submissionRing + self.completionRing = completionRing self.ringFlags = params.flags } private func _blockingConsumeCompletionGuts( - extraArgs: UnsafeMutablePointer? = nil - ) throws(IORingError) -> IOCompletion { - completionMutex.withLock { ring in - if let completion = _tryConsumeCompletion(ring: &ring) { - return completion - } else { - while true { - var sz = 0 - if extraArgs != nil { - sz = MemoryLayout.size - } - let res = io_uring_enter2( - ringDescriptor, - 0, - 1, - IORING_ENTER_GETEVENTS, - extraArgs, - sz - ) - // error handling: - // EAGAIN / EINTR (try again), - // EBADF / EBADFD / EOPNOTSUPP / ENXIO - // (failure in ring lifetime management, fatal), - // EINVAL (bad constant flag?, fatal), - // EFAULT (bad address for argument from library, fatal) - // EBUSY (not enough space for events; implies events filled - // by kernel between kernelTail load and now) - if res >= 0 || res == -EBUSY { - break - } else if res == -EAGAIN || res == -EINTR { - continue - } - fatalError( - "fatal error in receiving requests: " - + Errno(rawValue: -res).debugDescription - ) + minimumCount: UInt32, + extraArgs: UnsafeMutablePointer? = nil, + consumer: (IOCompletion?, IORingError?, Bool) throws -> Void + ) rethrows { + var count = 0 + while let completion = _tryConsumeCompletion(ring: completionRing) { + count += 1 + try consumer(completion, nil, false) + } + + if count < minimumCount { + while count < minimumCount { + var sz = 0 + if extraArgs != nil { + sz = MemoryLayout.size + } + let res = io_uring_enter2( + ringDescriptor, + 0, + minimumCount, + IORING_ENTER_GETEVENTS, + extraArgs, + sz + ) + // error handling: + // EAGAIN / EINTR (try again), + // EBADF / EBADFD / EOPNOTSUPP / ENXIO + // (failure in ring lifetime management, fatal), + // EINVAL (bad constant flag?, fatal), + // EFAULT (bad address for argument from library, fatal) + // EBUSY (not enough space for events; implies events filled + // by kernel between kernelTail load and now) + if res >= 0 || res == -EBUSY { + break + } else if res == -EAGAIN || res == -EINTR { + continue + } + fatalError( + "fatal error in receiving requests: " + + Errno(rawValue: -res).debugDescription + ) + while let completion = _tryConsumeCompletion(ring: completionRing) { + try consumer(completion, nil, false) } - return _tryConsumeCompletion(ring: &ring).unsafelyUnwrapped } + try consumer(nil, nil, true) } } - public func blockingConsumeCompletion(timeout: Duration? = nil) throws -> IOCompletion { + internal func _blockingConsumeOneCompletion( + extraArgs: UnsafeMutablePointer? = nil + ) throws -> IOCompletion { + var result: IOCompletion? = nil + try _blockingConsumeCompletionGuts(minimumCount: 1, extraArgs: extraArgs) { + (completion, error, done) in + if let error { + throw error + } + if let completion { + result = completion + } + } + return result.unsafelyUnwrapped + } + + public func blockingConsumeCompletion( + timeout: Duration? = nil + ) throws -> IOCompletion { if let timeout { var ts = __kernel_timespec( - tv_sec: timeout.components.seconds, - tv_nsec: timeout.components.attoseconds / 1_000_000_000 + tv_sec: timeout.components.seconds, + tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) return try withUnsafePointer(to: &ts) { tsPtr in var args = io_uring_getevents_arg( - sigmask: 0, - sigmask_sz: 0, - pad: 0, + sigmask: 0, + sigmask_sz: 0, + pad: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) - return try _blockingConsumeCompletionGuts(extraArgs: &args) + return try _blockingConsumeOneCompletion(extraArgs: &args) } } else { - return try _blockingConsumeCompletionGuts() + return try _blockingConsumeOneCompletion() } } - public func tryConsumeCompletion() -> IOCompletion? { - completionMutex.withLock { ring in - return _tryConsumeCompletion(ring: &ring) + public func blockingConsumeCompletions( + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (IOCompletion?, IORingError?, Bool) throws -> Void + ) throws { + var x = Glibc.stat() + let y = x.st_size + if let timeout { + var ts = __kernel_timespec( + tv_sec: timeout.components.seconds, + tv_nsec: timeout.components.attoseconds / 1_000_000_000 + ) + return try withUnsafePointer(to: &ts) { tsPtr in + var args = io_uring_getevents_arg( + sigmask: 0, + sigmask_sz: 0, + pad: 0, + ts: UInt64(UInt(bitPattern: tsPtr)) + ) + try _blockingConsumeCompletionGuts( + minimumCount: minimumCount, extraArgs: &args, consumer: consumer) + } + } else { + try _blockingConsumeCompletionGuts(minimumCount: minimumCount, consumer: consumer) } } - func _tryConsumeCompletion(ring: inout CQRing) -> IOCompletion? { + // public func peekNextCompletion() -> IOCompletion { + + // } + + public func tryConsumeCompletion() -> IOCompletion? { + return _tryConsumeCompletion(ring: completionRing) + } + + func _tryConsumeCompletion(ring: borrowing CQRing) -> IOCompletion? { let tail = ring.kernelTail.pointee.load(ordering: .acquiring) let head = ring.kernelHead.pointee.load(ordering: .acquiring) @@ -459,9 +523,9 @@ public struct IORing: @unchecked Sendable, ~Copyable { var rawfd = descriptor.rawValue let result = withUnsafePointer(to: &rawfd) { fdptr in return io_uring_register( - ring.ringDescriptor, + ring.ringDescriptor, IORING_REGISTER_EVENTFD, - UnsafeMutableRawPointer(mutating: fdptr), + UnsafeMutableRawPointer(mutating: fdptr), 1 ) } @@ -470,9 +534,9 @@ public struct IORing: @unchecked Sendable, ~Copyable { public mutating func unregisterEventFD(ring: inout IORing) throws { let result = io_uring_register( - ring.ringDescriptor, + ring.ringDescriptor, IORING_UNREGISTER_EVENTFD, - nil, + nil, 0 ) try handleRegistrationResult(result) @@ -529,17 +593,14 @@ public struct IORing: @unchecked Sendable, ~Copyable { } public func submitRequests() throws { - try submissionMutex.withLock { ring in - try _submitRequests(ring: &ring, ringDescriptor: ringDescriptor) - } + try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } @inlinable @inline(__always) public mutating func writeRequest(_ request: __owned IORequest) -> Bool { var raw: RawIORequest? = request.makeRawRequest() - return submissionMutex.withLock { ring in - return _writeRequest(raw.take()!, ring: &ring, submissionQueueEntries: submissionQueueEntries) - } + return _writeRequest( + raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } deinit { From 7107a570b4ad1a37a0ff86d510bc8159fb18dbb8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 11 Feb 2025 21:01:49 +0000 Subject: [PATCH 035/172] Fix import visibility --- Sources/System/IORequest.swift | 10 +++++----- Sources/System/IORing.swift | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 0af87176..0e1a3b06 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,4 +1,4 @@ -import struct CSystem.io_uring_sqe +@_implementationOnly import struct CSystem.io_uring_sqe @usableFromInline internal enum IORequestCore: ~Copyable { @@ -62,7 +62,7 @@ internal enum IORequestCore: ~Copyable { case closeSlot(IORingFileSlot) } -@inlinable @inline(__always) +@inline(__always) internal func makeRawRequest_readWrite_registered( file: FileDescriptor, buffer: IORingBuffer, @@ -76,7 +76,7 @@ internal func makeRawRequest_readWrite_registered( return request } -@inlinable @inline(__always) +@inline(__always) internal func makeRawRequest_readWrite_registered_slot( file: IORingFileSlot, buffer: IORingBuffer, @@ -104,7 +104,7 @@ internal func makeRawRequest_readWrite_unregistered( return request } -@inlinable @inline(__always) +@inline(__always) internal func makeRawRequest_readWrite_unregistered_slot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, @@ -251,7 +251,7 @@ extension IORequest { fatalError("Implement me") } - @inlinable @inline(__always) + @inline(__always) public consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() switch extractCore() { diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 0328538f..472a69b2 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -471,8 +471,6 @@ public struct IORing: ~Copyable { timeout: Duration? = nil, consumer: (IOCompletion?, IORingError?, Bool) throws -> Void ) throws { - var x = Glibc.stat() - let y = x.st_size if let timeout { var ts = __kernel_timespec( tv_sec: timeout.components.seconds, From 02481e0daabc0fb342c175681347efe8f2d44337 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 11 Feb 2025 21:06:16 +0000 Subject: [PATCH 036/172] More import fixes --- Sources/System/IORing.swift | 12 ++++++------ Sources/System/RawIORequest.swift | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 472a69b2..6dff66f1 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -2,7 +2,7 @@ import Glibc // needed for mmap import Synchronization -import struct CSystem.io_uring_sqe +@_implementationOnly import struct CSystem.io_uring_sqe // XXX: this *really* shouldn't be here. oh well. extension UnsafeMutableRawPointer { @@ -124,7 +124,7 @@ extension IORingBuffer { } } -@inlinable @inline(__always) +@inline(__always) internal func _writeRequest( _ request: __owned RawIORequest, ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer @@ -137,7 +137,7 @@ internal func _writeRequest( return true } -@inlinable @inline(__always) +@inline(__always) internal func _blockingGetSubmissionEntry( ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer ) -> UnsafeMutablePointer< @@ -209,7 +209,7 @@ internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { return _getUnconsumedSubmissionCount(ring: ring) } -@usableFromInline @inline(__always) +@inline(__always) internal func _getSubmissionEntry( ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer ) -> UnsafeMutablePointer< @@ -247,7 +247,7 @@ public struct IORing: ~Copyable { let completionRing: CQRing - @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer + let submissionQueueEntries: UnsafeMutableBufferPointer // kept around for unmap / cleanup let ringSize: Int @@ -594,7 +594,7 @@ public struct IORing: ~Copyable { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } - @inlinable @inline(__always) + @inline(__always) public mutating func writeRequest(_ request: __owned IORequest) -> Bool { var raw: RawIORequest? = request.makeRawRequest() return _writeRequest( diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 5a1fd03d..5b884993 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -1,9 +1,9 @@ // TODO: investigate @usableFromInline / @_implementationOnly dichotomy @_implementationOnly import CSystem -import struct CSystem.io_uring_sqe +@_implementationOnly import struct CSystem.io_uring_sqe public struct RawIORequest: ~Copyable { - @usableFromInline var rawValue: io_uring_sqe + var rawValue: io_uring_sqe public init() { self.rawValue = io_uring_sqe() From bb03f0fba3c480c3a625c9894b2f5dc32e9885c2 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 11 Feb 2025 22:33:10 +0000 Subject: [PATCH 037/172] Redesign registered resources API --- Sources/System/IORing.swift | 148 ++++++++++--------------- Tests/SystemTests/IORequestTests.swift | 6 +- 2 files changed, 60 insertions(+), 94 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 6dff66f1..3305a9bd 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -11,6 +11,12 @@ extension UnsafeMutableRawPointer { } } +extension UnsafeMutableRawBufferPointer { + func to_iovec() -> iovec { + iovec(iov_base: baseAddress, iov_len: count) + } +} + // all pointers in this struct reference kernel-visible memory @usableFromInline struct SQRing: ~Copyable { let kernelHead: UnsafePointer> @@ -46,67 +52,17 @@ struct CQRing: ~Copyable { let cqes: UnsafeBufferPointer } -internal final class ResourceManager: @unchecked Sendable { - typealias Resource = T - - struct Resources { - let resourceList: UnsafeMutableBufferPointer - var freeList: [Int] //TODO: bitvector? - } - - let mutex: Mutex - - init(_ res: UnsafeMutableBufferPointer) { - mutex = Mutex( - Resources( - resourceList: res, - freeList: [Int](res.indices) - )) - } - - func getResource() -> IOResource? { - mutex.withLock { resources in - if let index = resources.freeList.popLast() { - return IOResource( - resource: resources.resourceList[index], - index: index, - manager: self - ) - } else { - return nil - } - } - } - - func releaseResource(index: Int) { - mutex.withLock { resources in - resources.freeList.append(index) - } - } -} - -public class IOResource { +public struct IOResource { typealias Resource = T @usableFromInline let resource: T @usableFromInline let index: Int - let manager: ResourceManager internal init( resource: T, - index: Int, - manager: ResourceManager + index: Int ) { self.resource = resource self.index = index - self.manager = manager - } - - func withResource() { - - } - - deinit { - manager.releaseResource(index: self.index) } } @@ -253,8 +209,8 @@ public struct IORing: ~Copyable { let ringSize: Int let ringPtr: UnsafeMutableRawPointer - var registeredFiles: ResourceManager? - var registeredBuffers: ResourceManager? + var _registeredFiles: [UInt32]? + var _registeredBuffers: [iovec]? public init(queueDepth: UInt32) throws { var params = io_uring_params() @@ -517,11 +473,11 @@ public struct IORing: ~Copyable { //TODO: error handling } - public mutating func registerEventFD(ring: inout IORing, _ descriptor: FileDescriptor) throws { + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws { var rawfd = descriptor.rawValue let result = withUnsafePointer(to: &rawfd) { fdptr in return io_uring_register( - ring.ringDescriptor, + ringDescriptor, IORING_REGISTER_EVENTFD, UnsafeMutableRawPointer(mutating: fdptr), 1 @@ -530,9 +486,9 @@ public struct IORing: ~Copyable { try handleRegistrationResult(result) } - public mutating func unregisterEventFD(ring: inout IORing) throws { + public mutating func unregisterEventFD() throws { let result = io_uring_register( - ring.ringDescriptor, + ringDescriptor, IORING_UNREGISTER_EVENTFD, nil, 0 @@ -540,50 +496,64 @@ public struct IORing: ~Copyable { try handleRegistrationResult(result) } - public mutating func registerFiles(count: UInt32) { - guard self.registeredFiles == nil else { fatalError() } - let fileBuf = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) - fileBuf.initialize(repeating: UInt32.max) - io_uring_register( - self.ringDescriptor, - IORING_REGISTER_FILES, - fileBuf.baseAddress!, - count - ) + public mutating func registerFileSlots(count: Int) { + precondition(_registeredFiles == nil) + precondition(count < UInt32.max) + let files = [UInt32](repeating: UInt32.max, count: count) + + let regResult = files.withUnsafeBufferPointer { bPtr in + io_uring_register( + self.ringDescriptor, + IORING_REGISTER_FILES, + UnsafeMutableRawPointer(mutating:bPtr.baseAddress!), + UInt32(truncatingIfNeeded: count) + ) + } + // TODO: error handling - self.registeredFiles = ResourceManager(fileBuf) + _registeredFiles = files } public func unregisterFiles() { fatalError("failed to unregister files") } - public func getFile() -> IORingFileSlot? { - return self.registeredFiles?.getResource() + public var registeredFileSlots: some RandomAccessCollection { + RegisteredResources(resources: _registeredFiles ?? []) } - public mutating func registerBuffers(bufSize: UInt32, count: UInt32) { - let iovecs = UnsafeMutableBufferPointer.allocate(capacity: Int(count)) - let intBufSize = Int(bufSize) - for i in 0..: RandomAccessCollection { + let resources: [T] + + var startIndex: Int { 0 } + var endIndex: Int { resources.endIndex } + init(resources: [T]) { + self.resources = resources + } + subscript(position: Int) -> IOResource { + IOResource(resource: resources[position], index: position) + } } - public func getBuffer() -> IORingBuffer? { - return self.registeredBuffers?.getResource() + public var registeredBuffers: some RandomAccessCollection { + RegisteredResources(resources: _registeredBuffers ?? []) } public func unregisterBuffers() { diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 78be4cf7..e553a546 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -28,12 +28,8 @@ final class IORequestTests: XCTestCase { } func testOpenatFixedFile() throws { - // TODO: come up with a better way of getting a FileSlot. - let buf = UnsafeMutableBufferPointer.allocate(capacity: 2) - let resmgr = ResourceManager.init(buf) - let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! - let fileSlot = resmgr.getResource()! + let fileSlot: IORingFileSlot = IORingFileSlot(resource: UInt32.max, index: 0) let req = IORequest( opening: pathPtr, in: FileDescriptor(rawValue: -100), From a396967e3a77b780a78b733564137be7a8b4b7dc Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 11 Feb 2025 22:38:01 +0000 Subject: [PATCH 038/172] More registration tweaks --- Sources/System/IORing.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 3305a9bd..292303b7 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -496,7 +496,7 @@ public struct IORing: ~Copyable { try handleRegistrationResult(result) } - public mutating func registerFileSlots(count: Int) { + public mutating func registerFileSlots(count: Int) -> some RandomAccessCollection { precondition(_registeredFiles == nil) precondition(count < UInt32.max) let files = [UInt32](repeating: UInt32.max, count: count) @@ -512,6 +512,7 @@ public struct IORing: ~Copyable { // TODO: error handling _registeredFiles = files + return registeredFileSlots } public func unregisterFiles() { @@ -522,9 +523,10 @@ public struct IORing: ~Copyable { RegisteredResources(resources: _registeredFiles ?? []) } - public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) { + public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) -> some RandomAccessCollection { precondition(buffers.count < UInt32.max) precondition(_registeredBuffers == nil) + //TODO: check if io_uring has preconditions it needs for the buffers (e.g. alignment) let iovecs = buffers.map { $0.to_iovec() } let regResult = iovecs.withUnsafeBufferPointer { bPtr in io_uring_register( @@ -537,6 +539,7 @@ public struct IORing: ~Copyable { // TODO: error handling _registeredBuffers = iovecs + return registeredBuffers } struct RegisteredResources: RandomAccessCollection { From d4ca4129a033c04efb863827e9178101434c44df Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 11 Feb 2025 23:01:33 +0000 Subject: [PATCH 039/172] Some renaming, and implement linked requests --- Sources/System/IORequest.swift | 4 ++-- Sources/System/IORing.swift | 19 +++++++++++++++++-- Sources/System/RawIORequest.swift | 9 +++++++-- Tests/SystemTests/IORingTests.swift | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 0e1a3b06..f406f5a0 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,7 +1,7 @@ @_implementationOnly import struct CSystem.io_uring_sqe @usableFromInline -internal enum IORequestCore: ~Copyable { +internal enum IORequestCore { case nop // nothing here case openat( atDirectory: FileDescriptor, @@ -118,7 +118,7 @@ internal func makeRawRequest_readWrite_unregistered_slot( return request } -public struct IORequest : ~Copyable { +public struct IORequest { @usableFromInline var core: IORequestCore @inlinable internal consuming func extractCore() -> IORequestCore { diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 292303b7..fab2f8e4 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -567,13 +567,28 @@ public struct IORing: ~Copyable { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } - @inline(__always) - public mutating func writeRequest(_ request: __owned IORequest) -> Bool { + public mutating func prepare(request: __owned IORequest) -> Bool { var raw: RawIORequest? = request.makeRawRequest() return _writeRequest( raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } + //@inlinable //TODO: make sure the array allocation gets optimized out... + public mutating func prepare(linkedRequests: IORequest...) { + guard linkedRequests.count > 0 else { + return + } + let last = linkedRequests.last! + for req in linkedRequests.dropLast() { + var raw = req.makeRawRequest() + raw.linkToNextRequest() + _writeRequest( + raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + } + _writeRequest( + last.makeRawRequest(), ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + } + deinit { munmap(ringPtr, ringSize) munmap( diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 5b884993..40b72366 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -2,6 +2,7 @@ @_implementationOnly import CSystem @_implementationOnly import struct CSystem.io_uring_sqe +//TODO: make this internal public struct RawIORequest: ~Copyable { var rawValue: io_uring_sqe @@ -11,7 +12,7 @@ public struct RawIORequest: ~Copyable { } extension RawIORequest { - public enum Operation: UInt8 { + enum Operation: UInt8 { case nop = 0 case readv = 1 case writev = 2 @@ -53,7 +54,7 @@ extension RawIORequest { public static let skipSuccess = Flags(rawValue: 1 << 6) } - public var operation: Operation { + var operation: Operation { get { Operation(rawValue: rawValue.opcode)! } set { rawValue.opcode = newValue.rawValue } } @@ -63,6 +64,10 @@ extension RawIORequest { set { rawValue.flags = newValue.rawValue } } + public mutating func linkToNextRequest() { + flags = Flags(rawValue: flags.rawValue | Flags.linkRequest.rawValue) + } + public var fileDescriptor: FileDescriptor { get { FileDescriptor(rawValue: rawValue.fd) } set { rawValue.fd = newValue.rawValue } diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 2c25e2b3..4ae12fcf 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -13,7 +13,7 @@ final class IORingTests: XCTestCase { func testNop() throws { var ring = try IORing(queueDepth: 32) - ring.writeRequest(IORequest()) + ring.prepare(request: IORequest()) try ring.submitRequests() let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) From 5bfed03c971a5440f8c5e8489466352be8c2f298 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 11 Feb 2025 23:27:03 +0000 Subject: [PATCH 040/172] Switch to static methods for constructing requests --- Sources/System/IORequest.swift | 96 +++++++++++--------------- Tests/SystemTests/IORequestTests.swift | 5 +- Tests/SystemTests/IORingTests.swift | 2 +- 3 files changed, 43 insertions(+), 60 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index f406f5a0..4cdda768 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -127,127 +127,111 @@ public struct IORequest { } extension IORequest { - public init() { //TODO: why do we have nop? - core = .nop + public static func nop() -> IORequest { + IORequest(core: .nop) } - public init( - reading file: IORingFileSlot, + public static func reading(_ file: IORingFileSlot, into buffer: IORingBuffer, at offset: UInt64 = 0 - ) { - core = .readSlot(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset)) } - public init( - reading file: FileDescriptor, + public static func reading(_ file: FileDescriptor, into buffer: IORingBuffer, at offset: UInt64 = 0 - ) { - core = .read(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .read(file: file, buffer: buffer, offset: offset)) } - public init( - reading file: IORingFileSlot, + public static func reading(_ file: IORingFileSlot, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0 - ) { - core = .readUnregisteredSlot(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .readUnregisteredSlot(file: file, buffer: buffer, offset: offset)) } - public init( - reading file: FileDescriptor, + public static func reading(_ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0 - ) { - core = .readUnregistered(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .readUnregistered(file: file, buffer: buffer, offset: offset)) } - public init( - writing buffer: IORingBuffer, + public static func writing(_ buffer: IORingBuffer, into file: IORingFileSlot, at offset: UInt64 = 0 - ) { - core = .writeSlot(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset)) } - public init( - writing buffer: IORingBuffer, + public static func writing(_ buffer: IORingBuffer, into file: FileDescriptor, at offset: UInt64 = 0 - ) { - core = .write(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .write(file: file, buffer: buffer, offset: offset)) } - public init( - writing buffer: UnsafeMutableRawBufferPointer, + public static func writing(_ buffer: UnsafeMutableRawBufferPointer, into file: IORingFileSlot, at offset: UInt64 = 0 - ) { - core = .writeUnregisteredSlot(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .writeUnregisteredSlot(file: file, buffer: buffer, offset: offset)) } - public init( - writing buffer: UnsafeMutableRawBufferPointer, + public static func writing(_ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, at offset: UInt64 = 0 - ) { - core = .writeUnregistered(file: file, buffer: buffer, offset: offset) + ) -> IORequest { + IORequest(core: .writeUnregistered(file: file, buffer: buffer, offset: offset)) } - public init( - closing file: FileDescriptor - ) { - core = .close(file) + public static func closing(_ file: FileDescriptor) -> IORequest { + IORequest(core: .close(file)) } - public init( - closing file: IORingFileSlot - ) { - core = .closeSlot(file) + public static func closing(_ file: IORingFileSlot) -> IORequest { + IORequest(core: .closeSlot(file)) } - public init( - opening path: UnsafePointer, + public static func opening(_ path: UnsafePointer, in directory: FileDescriptor, into slot: IORingFileSlot, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil - ) { - core = .openatSlot(atDirectory: directory, path: path, mode, options: options, permissions: permissions, intoSlot: slot) + ) -> IORequest { + IORequest(core :.openatSlot(atDirectory: directory, path: path, mode, options: options, permissions: permissions, intoSlot: slot)) } - public init( - opening path: UnsafePointer, + public static func opening(_ path: UnsafePointer, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil - ) { - core = .openat(atDirectory: directory, path: path, mode, options: options, permissions: permissions) + ) -> IORequest { + IORequest(core: .openat(atDirectory: directory, path: path, mode, options: options, permissions: permissions)) } - public init( - opening path: FilePath, + public static func opening(_ path: FilePath, in directory: FileDescriptor, into slot: IORingFileSlot, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil - ) { + ) -> IORequest { fatalError("Implement me") } - public init( - opening path: FilePath, + public static func opening(_ path: FilePath, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil - ) { + ) -> IORequest { fatalError("Implement me") } diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index e553a546..f9f95803 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -19,7 +19,7 @@ func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { // which are known to work correctly. final class IORequestTests: XCTestCase { func testNop() { - let req = IORequest().makeRawRequest() + let req = IORequest.nop().makeRawRequest() let sourceBytes = requestBytes(req) // convenient property of nop: it's all zeros! // for some unknown reason, liburing sets the fd field to -1. @@ -30,8 +30,7 @@ final class IORequestTests: XCTestCase { func testOpenatFixedFile() throws { let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! let fileSlot: IORingFileSlot = IORingFileSlot(resource: UInt32.max, index: 0) - let req = IORequest( - opening: pathPtr, + let req = IORequest.opening(pathPtr, in: FileDescriptor(rawValue: -100), into: fileSlot, mode: .readOnly, diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 4ae12fcf..6421e0ca 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -13,7 +13,7 @@ final class IORingTests: XCTestCase { func testNop() throws { var ring = try IORing(queueDepth: 32) - ring.prepare(request: IORequest()) + ring.prepare(request: IORequest.nop()) try ring.submitRequests() let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) From 74366c33418fafe8268d225f56046590ef01d083 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 12 Feb 2025 20:18:51 +0000 Subject: [PATCH 041/172] Improve submit API --- Sources/System/IORing.swift | 15 ++++++++++++--- Tests/SystemTests/IORingTests.swift | 3 +-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index fab2f8e4..e238faba 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -563,7 +563,7 @@ public struct IORing: ~Copyable { fatalError("failed to unregister buffers: TODO") } - public func submitRequests() throws { + public func submitPreparedRequests() throws { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } @@ -573,8 +573,7 @@ public struct IORing: ~Copyable { raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } - //@inlinable //TODO: make sure the array allocation gets optimized out... - public mutating func prepare(linkedRequests: IORequest...) { + mutating func prepare(linkedRequests: some BidirectionalCollection) { guard linkedRequests.count > 0 else { return } @@ -589,6 +588,16 @@ public struct IORing: ~Copyable { last.makeRawRequest(), ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } + //@inlinable //TODO: make sure the array allocation gets optimized out... + public mutating func prepare(linkedRequests: IORequest...) { + prepare(linkedRequests: linkedRequests) + } + + public mutating func submit(linkedRequests: IORequest...) throws { + prepare(linkedRequests: linkedRequests) + try submitPreparedRequests() + } + deinit { munmap(ringPtr, ringSize) munmap( diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 6421e0ca..306516be 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -13,8 +13,7 @@ final class IORingTests: XCTestCase { func testNop() throws { var ring = try IORing(queueDepth: 32) - ring.prepare(request: IORequest.nop()) - try ring.submitRequests() + try ring.submit(linkedRequests: IORequest.nop()) let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) } From 7f6e673750128d8b9a47934b83e0b0c523921b56 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 12 Feb 2025 23:43:29 +0000 Subject: [PATCH 042/172] Adjust registered resources API --- Sources/System/IORing.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index e238faba..0500be09 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -53,7 +53,7 @@ struct CQRing: ~Copyable { } public struct IOResource { - typealias Resource = T + public typealias Resource = T @usableFromInline let resource: T @usableFromInline let index: Int @@ -496,7 +496,7 @@ public struct IORing: ~Copyable { try handleRegistrationResult(result) } - public mutating func registerFileSlots(count: Int) -> some RandomAccessCollection { + public mutating func registerFileSlots(count: Int) -> RegisteredResources { precondition(_registeredFiles == nil) precondition(count < UInt32.max) let files = [UInt32](repeating: UInt32.max, count: count) @@ -519,11 +519,11 @@ public struct IORing: ~Copyable { fatalError("failed to unregister files") } - public var registeredFileSlots: some RandomAccessCollection { + public var registeredFileSlots: RegisteredResources { RegisteredResources(resources: _registeredFiles ?? []) } - public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) -> some RandomAccessCollection { + public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) -> RegisteredResources { precondition(buffers.count < UInt32.max) precondition(_registeredBuffers == nil) //TODO: check if io_uring has preconditions it needs for the buffers (e.g. alignment) @@ -542,20 +542,23 @@ public struct IORing: ~Copyable { return registeredBuffers } - struct RegisteredResources: RandomAccessCollection { + public struct RegisteredResources: RandomAccessCollection { let resources: [T] - var startIndex: Int { 0 } - var endIndex: Int { resources.endIndex } + public var startIndex: Int { 0 } + public var endIndex: Int { resources.endIndex } init(resources: [T]) { self.resources = resources } - subscript(position: Int) -> IOResource { + public subscript(position: Int) -> IOResource { IOResource(resource: resources[position], index: position) } + public subscript(position: Int16) -> IOResource { + IOResource(resource: resources[Int(position)], index: Int(position)) + } } - public var registeredBuffers: some RandomAccessCollection { + public var registeredBuffers: RegisteredResources { RegisteredResources(resources: _registeredBuffers ?? []) } From 0c6ef16ca91cd82ab78a52a5cac83b113ba8e67d Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 12 Feb 2025 23:45:18 +0000 Subject: [PATCH 043/172] Fix type --- Sources/System/IORing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 0500be09..fb0c20aa 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -553,7 +553,7 @@ public struct IORing: ~Copyable { public subscript(position: Int) -> IOResource { IOResource(resource: resources[position], index: position) } - public subscript(position: Int16) -> IOResource { + public subscript(position: UInt16) -> IOResource { IOResource(resource: resources[Int(position)], index: Int(position)) } } From a22e5f685866614a027ff4a394e2af71d4b412c8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 13 Feb 2025 22:09:42 +0000 Subject: [PATCH 044/172] Add a version of registerBuffers that isn't varargs --- Sources/System/IORing.swift | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index fb0c20aa..b7189917 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -496,16 +496,18 @@ public struct IORing: ~Copyable { try handleRegistrationResult(result) } - public mutating func registerFileSlots(count: Int) -> RegisteredResources { + public mutating func registerFileSlots(count: Int) -> RegisteredResources< + IORingFileSlot.Resource + > { precondition(_registeredFiles == nil) precondition(count < UInt32.max) - let files = [UInt32](repeating: UInt32.max, count: count) + let files = [UInt32](repeating: UInt32.max, count: count) let regResult = files.withUnsafeBufferPointer { bPtr in io_uring_register( self.ringDescriptor, IORING_REGISTER_FILES, - UnsafeMutableRawPointer(mutating:bPtr.baseAddress!), + UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: count) ) } @@ -523,7 +525,9 @@ public struct IORing: ~Copyable { RegisteredResources(resources: _registeredFiles ?? []) } - public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) -> RegisteredResources { + public mutating func registerBuffers(_ buffers: some Collection) + -> RegisteredResources + { precondition(buffers.count < UInt32.max) precondition(_registeredBuffers == nil) //TODO: check if io_uring has preconditions it needs for the buffers (e.g. alignment) @@ -542,6 +546,12 @@ public struct IORing: ~Copyable { return registeredBuffers } + public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) + -> RegisteredResources + { + registerBuffers(buffers) + } + public struct RegisteredResources: RandomAccessCollection { let resources: [T] @@ -588,7 +598,8 @@ public struct IORing: ~Copyable { raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } _writeRequest( - last.makeRawRequest(), ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + last.makeRawRequest(), ring: &submissionRing, + submissionQueueEntries: submissionQueueEntries) } //@inlinable //TODO: make sure the array allocation gets optimized out... From 6983196ad6f980fde158811e13a828d70b7a93f0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 13 Feb 2025 23:31:06 +0000 Subject: [PATCH 045/172] Add unlinkAt support --- Sources/System/IORequest.swift | 14 ++++++++++++++ Sources/System/RawIORequest.swift | 1 + 2 files changed, 15 insertions(+) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 4cdda768..8705f0d6 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -60,6 +60,10 @@ internal enum IORequestCore { ) case close(FileDescriptor) case closeSlot(IORingFileSlot) + case unlinkAt( + atDirectory: FileDescriptor, + path: UnsafePointer + ) } @inline(__always) @@ -235,6 +239,12 @@ extension IORequest { fatalError("Implement me") } + public static func unlinking(_ path: UnsafePointer, + in directory: FileDescriptor + ) -> IORequest { + IORequest(core: .unlinkAt(atDirectory: directory, path: path)) + } + @inline(__always) public consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() @@ -293,6 +303,10 @@ extension IORequest { case .closeSlot(let file): request.operation = .close request.rawValue.file_index = UInt32(file.index + 1) + case .unlinkAt(let atDirectory, let path): + request.operation = .unlinkAt + request.fileDescriptor = atDirectory + request.rawValue.addr = UInt64(UInt(bitPattern: path)) } return request } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 40b72366..8f1a2b1b 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -36,6 +36,7 @@ extension RawIORequest { // ... case openAt2 = 28 // ... + case unlinkAt = 36 } public struct Flags: OptionSet, Hashable, Codable { From 5ba137703c63791b5088ac44ef1ebd2662566d0a Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 13 Feb 2025 23:43:19 +0000 Subject: [PATCH 046/172] Dubious approach to this, but I want to try it out a bit --- Sources/System/RawIORequest.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 8f1a2b1b..78f4f6de 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -101,6 +101,7 @@ extension RawIORequest { // TODO: cleanup? rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) rawValue.len = UInt32(exactly: newValue.count)! + rawValue.user_data = rawValue.addr //TODO: this is kind of a hack, but I need to decide how best to get the buffer out on the other side } } From 006464963162f26a722f600b975a5ad5145e7150 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 14 Feb 2025 18:24:09 +0000 Subject: [PATCH 047/172] Turn on single issuer as an experiment --- Sources/System/IORing.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index b7189917..5dd53d37 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -214,6 +214,7 @@ public struct IORing: ~Copyable { public init(queueDepth: UInt32) throws { var params = io_uring_params() + params.flags = IORING_SETUP_SINGLE_ISSUER //TODO make this configurable ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { return io_uring_setup(queueDepth, $0) From 901f4c80c48dd983ae87b115736f0589d6963b73 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 14 Feb 2025 18:25:46 +0000 Subject: [PATCH 048/172] Revert "Turn on single issuer as an experiment" This reverts commit 006464963162f26a722f600b975a5ad5145e7150. --- Sources/System/IORing.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 5dd53d37..b7189917 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -214,7 +214,6 @@ public struct IORing: ~Copyable { public init(queueDepth: UInt32) throws { var params = io_uring_params() - params.flags = IORING_SETUP_SINGLE_ISSUER //TODO make this configurable ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { return io_uring_setup(queueDepth, $0) From 283e8d6b7b747f082ed2b7c9bd763229729d9d76 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 14 Feb 2025 18:26:31 +0000 Subject: [PATCH 049/172] Reapply "Turn on single issuer as an experiment" This reverts commit 901f4c80c48dd983ae87b115736f0589d6963b73. --- Sources/System/IORing.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index b7189917..5dd53d37 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -214,6 +214,7 @@ public struct IORing: ~Copyable { public init(queueDepth: UInt32) throws { var params = io_uring_params() + params.flags = IORING_SETUP_SINGLE_ISSUER //TODO make this configurable ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { return io_uring_setup(queueDepth, $0) From d895f2a828f6c6c884057f43f0b14aa7fac10aff Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 14 Feb 2025 18:27:17 +0000 Subject: [PATCH 050/172] Revert "Reapply "Turn on single issuer as an experiment"" This reverts commit 283e8d6b7b747f082ed2b7c9bd763229729d9d76. --- Sources/System/IORing.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 5dd53d37..b7189917 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -214,7 +214,6 @@ public struct IORing: ~Copyable { public init(queueDepth: UInt32) throws { var params = io_uring_params() - params.flags = IORING_SETUP_SINGLE_ISSUER //TODO make this configurable ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { return io_uring_setup(queueDepth, $0) From f18ed75481746807ed335731e1af46e62ecdf47f Mon Sep 17 00:00:00 2001 From: xuty Date: Tue, 17 Dec 2024 15:13:15 +0800 Subject: [PATCH 051/172] Use GetTempPathW for better compatibility --- Sources/System/FilePath/FilePathTempWindows.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/FilePath/FilePathTempWindows.swift b/Sources/System/FilePath/FilePathTempWindows.swift index 0d97edcb..d6e45f4f 100644 --- a/Sources/System/FilePath/FilePathTempWindows.swift +++ b/Sources/System/FilePath/FilePathTempWindows.swift @@ -17,7 +17,7 @@ internal func _getTemporaryDirectory() throws -> FilePath { capacity: Int(MAX_PATH) + 1) { buffer in - guard GetTempPath2W(DWORD(buffer.count), buffer.baseAddress) != 0 else { + guard GetTempPathW(DWORD(buffer.count), buffer.baseAddress) != 0 else { throw Errno(windowsError: GetLastError()) } From f3b8cc473dfb84705aaaa374d8385794ba4a5efe Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 28 Feb 2025 00:45:31 +0000 Subject: [PATCH 052/172] Actually consume events we waited for --- Sources/System/IORing.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index b7189917..505f3e02 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -376,9 +376,9 @@ public struct IORing: ~Copyable { "fatal error in receiving requests: " + Errno(rawValue: -res).debugDescription ) - while let completion = _tryConsumeCompletion(ring: completionRing) { - try consumer(completion, nil, false) - } + } + while let completion = _tryConsumeCompletion(ring: completionRing) { + try consumer(completion, nil, false) } try consumer(nil, nil, true) } From d338de147879b044c636992b6fe892a4b90ac8fc Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 28 Feb 2025 00:49:31 +0000 Subject: [PATCH 053/172] Only get one completion if we asked for one completion --- Sources/System/IORing.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 505f3e02..d459422a 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -336,6 +336,7 @@ public struct IORing: ~Copyable { private func _blockingConsumeCompletionGuts( minimumCount: UInt32, + maximumCount: UInt32, extraArgs: UnsafeMutablePointer? = nil, consumer: (IOCompletion?, IORingError?, Bool) throws -> Void ) rethrows { @@ -343,6 +344,10 @@ public struct IORing: ~Copyable { while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 try consumer(completion, nil, false) + if count == maximumCount { + try consumer(nil, nil, true) + return + } } if count < minimumCount { @@ -377,8 +382,13 @@ public struct IORing: ~Copyable { + Errno(rawValue: -res).debugDescription ) } + var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { + count += 1 try consumer(completion, nil, false) + if count == maximumCount { + break + } } try consumer(nil, nil, true) } @@ -388,7 +398,7 @@ public struct IORing: ~Copyable { extraArgs: UnsafeMutablePointer? = nil ) throws -> IOCompletion { var result: IOCompletion? = nil - try _blockingConsumeCompletionGuts(minimumCount: 1, extraArgs: extraArgs) { + try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { (completion, error, done) in if let error { throw error @@ -440,10 +450,10 @@ public struct IORing: ~Copyable { ts: UInt64(UInt(bitPattern: tsPtr)) ) try _blockingConsumeCompletionGuts( - minimumCount: minimumCount, extraArgs: &args, consumer: consumer) + minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, consumer: consumer) } } else { - try _blockingConsumeCompletionGuts(minimumCount: minimumCount, consumer: consumer) + try _blockingConsumeCompletionGuts(minimumCount: minimumCount, maximumCount: UInt32.max, consumer: consumer) } } From 5e2467325960b37a53b145fab6efc896ce9b1d6f Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 21:53:26 +0000 Subject: [PATCH 054/172] Switch from unsafe pointers to FilePaths, using hacks --- Sources/System/IORequest.swift | 97 +++++++++++++++----------- Sources/System/RawIORequest.swift | 3 +- Tests/SystemTests/IORequestTests.swift | 2 +- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 8705f0d6..e09bd895 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -5,14 +5,14 @@ internal enum IORequestCore { case nop // nothing here case openat( atDirectory: FileDescriptor, - path: UnsafePointer, + path: FilePath, FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil ) case openatSlot( atDirectory: FileDescriptor, - path: UnsafePointer, + path: FilePath, FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, @@ -60,9 +60,9 @@ internal enum IORequestCore { ) case close(FileDescriptor) case closeSlot(IORingFileSlot) - case unlinkAt( + case unlinkAt( atDirectory: FileDescriptor, - path: UnsafePointer + path: FilePath ) } @@ -135,56 +135,64 @@ extension IORequest { IORequest(core: .nop) } - public static func reading(_ file: IORingFileSlot, + public static func reading( + _ file: IORingFileSlot, into buffer: IORingBuffer, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset)) } - public static func reading(_ file: FileDescriptor, + public static func reading( + _ file: FileDescriptor, into buffer: IORingBuffer, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .read(file: file, buffer: buffer, offset: offset)) } - public static func reading(_ file: IORingFileSlot, + public static func reading( + _ file: IORingFileSlot, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .readUnregisteredSlot(file: file, buffer: buffer, offset: offset)) } - public static func reading(_ file: FileDescriptor, + public static func reading( + _ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .readUnregistered(file: file, buffer: buffer, offset: offset)) } - public static func writing(_ buffer: IORingBuffer, + public static func writing( + _ buffer: IORingBuffer, into file: IORingFileSlot, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset)) } - public static func writing(_ buffer: IORingBuffer, + public static func writing( + _ buffer: IORingBuffer, into file: FileDescriptor, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .write(file: file, buffer: buffer, offset: offset)) } - public static func writing(_ buffer: UnsafeMutableRawBufferPointer, + public static func writing( + _ buffer: UnsafeMutableRawBufferPointer, into file: IORingFileSlot, at offset: UInt64 = 0 ) -> IORequest { IORequest(core: .writeUnregisteredSlot(file: file, buffer: buffer, offset: offset)) } - public static func writing(_ buffer: UnsafeMutableRawBufferPointer, + public static func writing( + _ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, at offset: UInt64 = 0 ) -> IORequest { @@ -199,47 +207,35 @@ extension IORequest { IORequest(core: .closeSlot(file)) } - - public static func opening(_ path: UnsafePointer, - in directory: FileDescriptor, - into slot: IORingFileSlot, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil - ) -> IORequest { - IORequest(core :.openatSlot(atDirectory: directory, path: path, mode, options: options, permissions: permissions, intoSlot: slot)) - } - - public static func opening(_ path: UnsafePointer, - in directory: FileDescriptor, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil - ) -> IORequest { - IORequest(core: .openat(atDirectory: directory, path: path, mode, options: options, permissions: permissions)) - } - - - public static func opening(_ path: FilePath, + public static func opening( + _ path: FilePath, in directory: FileDescriptor, into slot: IORingFileSlot, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil ) -> IORequest { - fatalError("Implement me") + IORequest( + core: .openatSlot( + atDirectory: directory, path: path, mode, options: options, + permissions: permissions, intoSlot: slot)) } - public static func opening(_ path: FilePath, + public static func opening( + _ path: FilePath, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil ) -> IORequest { - fatalError("Implement me") + IORequest( + core: .openat( + atDirectory: directory, path: path, mode, options: options, permissions: permissions + )) } - public static func unlinking(_ path: UnsafePointer, + public static func unlinking( + _ path: FilePath, in directory: FileDescriptor ) -> IORequest { IORequest(core: .unlinkAt(atDirectory: directory, path: path)) @@ -251,20 +247,31 @@ extension IORequest { switch extractCore() { case .nop: request.operation = .nop - case .openatSlot(let atDirectory, let path, let mode, let options, let permissions, let fileSlot): + case .openatSlot( + let atDirectory, let path, let mode, let options, let permissions, let fileSlot): // TODO: use rawValue less request.operation = .openAt request.fileDescriptor = atDirectory - request.rawValue.addr = UInt64(UInt(bitPattern: path)) + request.rawValue.addr = UInt64( + UInt( + bitPattern: path.withPlatformString { ptr in + ptr //this is unsavory, but we keep it alive by storing path alongside it in the request + })) request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) request.rawValue.len = permissions?.rawValue ?? 0 request.rawValue.file_index = UInt32(fileSlot.index + 1) + request.path = path case .openat(let atDirectory, let path, let mode, let options, let permissions): request.operation = .openAt request.fileDescriptor = atDirectory - request.rawValue.addr = UInt64(UInt(bitPattern: path)) + request.rawValue.addr = UInt64( + UInt( + bitPattern: path.withPlatformString { ptr in + ptr //this is unsavory, but we keep it alive by storing path alongside it in the request + })) request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) request.rawValue.len = permissions?.rawValue ?? 0 + request.path = path case .write(let file, let buffer, let offset): request.operation = .writeFixed return makeRawRequest_readWrite_registered( @@ -306,7 +313,13 @@ extension IORequest { case .unlinkAt(let atDirectory, let path): request.operation = .unlinkAt request.fileDescriptor = atDirectory - request.rawValue.addr = UInt64(UInt(bitPattern: path)) + request.rawValue.addr = UInt64( + UInt( + bitPattern: path.withPlatformString { ptr in + ptr //this is unsavory, but we keep it alive by storing path alongside it in the request + }) + ) + request.path = path } return request } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 78f4f6de..980771cc 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -4,7 +4,8 @@ //TODO: make this internal public struct RawIORequest: ~Copyable { - var rawValue: io_uring_sqe + var rawValue: io_uring_sqe + var path: FilePath? //buffer owner for the path pointer that the sqe may have public init() { self.rawValue = io_uring_sqe() diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index f9f95803..4aaf7543 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -30,7 +30,7 @@ final class IORequestTests: XCTestCase { func testOpenatFixedFile() throws { let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! let fileSlot: IORingFileSlot = IORingFileSlot(resource: UInt32.max, index: 0) - let req = IORequest.opening(pathPtr, + let req = IORequest.opening(FilePath(platformString: pathPtr), in: FileDescriptor(rawValue: -100), into: fileSlot, mode: .readOnly, From 49dd7977ca34dc0ea40adcbd031586f4269819bc Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 22:07:28 +0000 Subject: [PATCH 055/172] Add a combined "submit and consume" operation --- Sources/System/IORing.swift | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index d459422a..1f223fa8 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -450,10 +450,12 @@ public struct IORing: ~Copyable { ts: UInt64(UInt(bitPattern: tsPtr)) ) try _blockingConsumeCompletionGuts( - minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, consumer: consumer) + minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, + consumer: consumer) } } else { - try _blockingConsumeCompletionGuts(minimumCount: minimumCount, maximumCount: UInt32.max, consumer: consumer) + try _blockingConsumeCompletionGuts( + minimumCount: minimumCount, maximumCount: UInt32.max, consumer: consumer) } } @@ -590,6 +592,20 @@ public struct IORing: ~Copyable { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } + public func submitPreparedRequestsAndConsumeCompletions( + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (IOCompletion?, IORingError?, Bool) throws -> Void + ) throws { + //TODO: optimize this to one uring_enter + try submitPreparedRequests() + try blockingConsumeCompletions( + minimumCount: minimumCount, + timeout: timeout, + consumer: consumer + ) + } + public mutating func prepare(request: __owned IORequest) -> Bool { var raw: RawIORequest? = request.makeRawRequest() return _writeRequest( From 91155fd52772b5cb1cf6980461f4429f3301f0e9 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 22:15:32 +0000 Subject: [PATCH 056/172] Plumb error handling through completion consumers --- Sources/System/IORing.swift | 12 ++++++++++-- Sources/System/IORingError.swift | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 1f223fa8..4525281a 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -343,7 +343,11 @@ public struct IORing: ~Copyable { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 - try consumer(completion, nil, false) + if completion.result < 0 { + try consumer(nil, IORingError(completionResult: completion.result), false) + } else { + try consumer(completion, nil, false) + } if count == maximumCount { try consumer(nil, nil, true) return @@ -385,8 +389,12 @@ public struct IORing: ~Copyable { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 + if completion.result < 0 { + try consumer(nil, IORingError(completionResult: completion.result), false) + } else { try consumer(completion, nil, false) - if count == maximumCount { + } + if count == maximumCount { break } } diff --git a/Sources/System/IORingError.swift b/Sources/System/IORingError.swift index fda58bcb..fbd70bce 100644 --- a/Sources/System/IORingError.swift +++ b/Sources/System/IORingError.swift @@ -2,5 +2,9 @@ public enum IORingError: Error, Equatable { case missingRequiredFeatures case operationCanceled - case unknown + case unknown(errorCode: Int) + + internal init(completionResult: Int32) { + self = .unknown(errorCode: Int(completionResult)) //TODO, flesh this out + } } From 9ad16c0aae9f005202eab849f1e6278179453238 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 22:37:51 +0000 Subject: [PATCH 057/172] Plumb through userData --- Sources/System/IORequest.swift | 175 +++++++++++++++++++----------- Sources/System/RawIORequest.swift | 1 - 2 files changed, 114 insertions(+), 62 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index e09bd895..7b602060 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -8,7 +8,8 @@ internal enum IORequestCore { path: FilePath, FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil + permissions: FilePermissions? = nil, + userData: UInt64 = 0 ) case openatSlot( atDirectory: FileDescriptor, @@ -16,53 +17,69 @@ internal enum IORequestCore { FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, - intoSlot: IORingFileSlot + intoSlot: IORingFileSlot, + userData: UInt64 = 0 ) case read( file: FileDescriptor, buffer: IORingBuffer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case readUnregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case readSlot( file: IORingFileSlot, buffer: IORingBuffer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case readUnregisteredSlot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case write( file: FileDescriptor, buffer: IORingBuffer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case writeUnregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case writeSlot( file: IORingFileSlot, buffer: IORingBuffer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 ) case writeUnregisteredSlot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, - offset: UInt64 = 0 + offset: UInt64 = 0, + userData: UInt64 = 0 + ) + case close( + FileDescriptor, + userData: UInt64 = 0 + ) + case closeSlot( + IORingFileSlot, + userData: UInt64 = 0 ) - case close(FileDescriptor) - case closeSlot(IORingFileSlot) case unlinkAt( atDirectory: FileDescriptor, - path: FilePath + path: FilePath, + userData: UInt64 = 0 ) } @@ -71,6 +88,7 @@ internal func makeRawRequest_readWrite_registered( file: FileDescriptor, buffer: IORingBuffer, offset: UInt64, + userData: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.fileDescriptor = file @@ -85,6 +103,7 @@ internal func makeRawRequest_readWrite_registered_slot( file: IORingFileSlot, buffer: IORingBuffer, offset: UInt64, + userData: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.rawValue.fd = Int32(exactly: file.index)! @@ -100,6 +119,7 @@ internal func makeRawRequest_readWrite_unregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, offset: UInt64, + userData: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.fileDescriptor = file @@ -113,6 +133,7 @@ internal func makeRawRequest_readWrite_unregistered_slot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, offset: UInt64, + userData: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.rawValue.fd = Int32(exactly: file.index)! @@ -131,80 +152,101 @@ public struct IORequest { } extension IORequest { - public static func nop() -> IORequest { + public static func nop(userData: UInt64 = 0) -> IORequest { IORequest(core: .nop) } public static func reading( _ file: IORingFileSlot, into buffer: IORingBuffer, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset)) + IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset, userData: userData)) } public static func reading( _ file: FileDescriptor, into buffer: IORingBuffer, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .read(file: file, buffer: buffer, offset: offset)) + IORequest(core: .read(file: file, buffer: buffer, offset: offset, userData: userData)) } public static func reading( _ file: IORingFileSlot, into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .readUnregisteredSlot(file: file, buffer: buffer, offset: offset)) + IORequest( + core: .readUnregisteredSlot( + file: file, buffer: buffer, offset: offset, userData: userData)) } public static func reading( _ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .readUnregistered(file: file, buffer: buffer, offset: offset)) + IORequest( + core: .readUnregistered(file: file, buffer: buffer, offset: offset, userData: userData)) } public static func writing( _ buffer: IORingBuffer, into file: IORingFileSlot, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset)) + IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset, userData: userData)) } public static func writing( _ buffer: IORingBuffer, into file: FileDescriptor, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .write(file: file, buffer: buffer, offset: offset)) + IORequest(core: .write(file: file, buffer: buffer, offset: offset, userData: userData)) } public static func writing( _ buffer: UnsafeMutableRawBufferPointer, into file: IORingFileSlot, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .writeUnregisteredSlot(file: file, buffer: buffer, offset: offset)) + IORequest( + core: .writeUnregisteredSlot( + file: file, buffer: buffer, offset: offset, userData: userData)) } public static func writing( _ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, - at offset: UInt64 = 0 + at offset: UInt64 = 0, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .writeUnregistered(file: file, buffer: buffer, offset: offset)) + IORequest( + core: .writeUnregistered(file: file, buffer: buffer, offset: offset, userData: userData) + ) } - public static func closing(_ file: FileDescriptor) -> IORequest { - IORequest(core: .close(file)) + public static func closing( + _ file: FileDescriptor, + userData: UInt64 = 0 + ) -> IORequest { + IORequest(core: .close(file, userData: userData)) } - public static func closing(_ file: IORingFileSlot) -> IORequest { - IORequest(core: .closeSlot(file)) + public static func closing( + _ file: IORingFileSlot, + userData: UInt64 = 0 + ) -> IORequest { + IORequest(core: .closeSlot(file, userData: userData)) } public static func opening( @@ -213,12 +255,13 @@ extension IORequest { into slot: IORingFileSlot, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil + permissions: FilePermissions? = nil, + userData: UInt64 = 0 ) -> IORequest { IORequest( core: .openatSlot( atDirectory: directory, path: path, mode, options: options, - permissions: permissions, intoSlot: slot)) + permissions: permissions, intoSlot: slot, userData: userData)) } public static func opening( @@ -226,19 +269,22 @@ extension IORequest { in directory: FileDescriptor, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil + permissions: FilePermissions? = nil, + userData: UInt64 = 0 ) -> IORequest { IORequest( core: .openat( - atDirectory: directory, path: path, mode, options: options, permissions: permissions + atDirectory: directory, path: path, mode, options: options, + permissions: permissions, userData: userData )) } public static func unlinking( _ path: FilePath, - in directory: FileDescriptor + in directory: FileDescriptor, + userData: UInt64 = 0 ) -> IORequest { - IORequest(core: .unlinkAt(atDirectory: directory, path: path)) + IORequest(core: .unlinkAt(atDirectory: directory, path: path, userData: userData)) } @inline(__always) @@ -248,7 +294,8 @@ extension IORequest { case .nop: request.operation = .nop case .openatSlot( - let atDirectory, let path, let mode, let options, let permissions, let fileSlot): + let atDirectory, let path, let mode, let options, let permissions, let fileSlot, + let userData): // TODO: use rawValue less request.operation = .openAt request.fileDescriptor = atDirectory @@ -261,7 +308,9 @@ extension IORequest { request.rawValue.len = permissions?.rawValue ?? 0 request.rawValue.file_index = UInt32(fileSlot.index + 1) request.path = path - case .openat(let atDirectory, let path, let mode, let options, let permissions): + request.rawValue.user_data = userData + case .openat( + let atDirectory, let path, let mode, let options, let permissions, let userData): request.operation = .openAt request.fileDescriptor = atDirectory request.rawValue.addr = UInt64( @@ -272,45 +321,48 @@ extension IORequest { request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) request.rawValue.len = permissions?.rawValue ?? 0 request.path = path - case .write(let file, let buffer, let offset): + request.rawValue.user_data = userData + case .write(let file, let buffer, let offset, let userData): request.operation = .writeFixed return makeRawRequest_readWrite_registered( - file: file, buffer: buffer, offset: offset, request: request) - case .writeSlot(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .writeSlot(let file, let buffer, let offset, let userData): request.operation = .writeFixed return makeRawRequest_readWrite_registered_slot( - file: file, buffer: buffer, offset: offset, request: request) - case .writeUnregistered(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .writeUnregistered(let file, let buffer, let offset, let userData): request.operation = .write return makeRawRequest_readWrite_unregistered( - file: file, buffer: buffer, offset: offset, request: request) - case .writeUnregisteredSlot(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .writeUnregisteredSlot(let file, let buffer, let offset, let userData): request.operation = .write return makeRawRequest_readWrite_unregistered_slot( - file: file, buffer: buffer, offset: offset, request: request) - case .read(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .read(let file, let buffer, let offset, let userData): request.operation = .readFixed return makeRawRequest_readWrite_registered( - file: file, buffer: buffer, offset: offset, request: request) - case .readSlot(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .readSlot(let file, let buffer, let offset, let userData): request.operation = .readFixed return makeRawRequest_readWrite_registered_slot( - file: file, buffer: buffer, offset: offset, request: request) - case .readUnregistered(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .readUnregistered(let file, let buffer, let offset, let userData): request.operation = .read return makeRawRequest_readWrite_unregistered( - file: file, buffer: buffer, offset: offset, request: request) - case .readUnregisteredSlot(let file, let buffer, let offset): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .readUnregisteredSlot(let file, let buffer, let offset, let userData): request.operation = .read return makeRawRequest_readWrite_unregistered_slot( - file: file, buffer: buffer, offset: offset, request: request) - case .close(let file): + file: file, buffer: buffer, offset: offset, userData: userData, request: request) + case .close(let file, let userData): request.operation = .close request.fileDescriptor = file - case .closeSlot(let file): + request.rawValue.user_data = userData + case .closeSlot(let file, let userData): request.operation = .close request.rawValue.file_index = UInt32(file.index + 1) - case .unlinkAt(let atDirectory, let path): + request.rawValue.user_data = userData + case .unlinkAt(let atDirectory, let path, let userData): request.operation = .unlinkAt request.fileDescriptor = atDirectory request.rawValue.addr = UInt64( @@ -320,6 +372,7 @@ extension IORequest { }) ) request.path = path + request.rawValue.user_data = userData } return request } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 980771cc..50c97c61 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -102,7 +102,6 @@ extension RawIORequest { // TODO: cleanup? rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) rawValue.len = UInt32(exactly: newValue.count)! - rawValue.user_data = rawValue.addr //TODO: this is kind of a hack, but I need to decide how best to get the buffer out on the other side } } From 880ec9086e0517cd8a90e1f58588cfcebdcec487 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 22:51:35 +0000 Subject: [PATCH 058/172] Add a pointer convenience for getting the user data --- Sources/System/IOCompletion.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 1702f9e8..9b41214e 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -23,19 +23,25 @@ extension IOCompletion { extension IOCompletion { public var userData: UInt64 { //TODO: naming? get { - return rawValue.user_data + rawValue.user_data + } + } + + public var userPointer: UnsafeRawPointer? { + get { + UnsafeRawPointer(bitPattern: UInt(rawValue.user_data)) } } public var result: Int32 { get { - return rawValue.res + rawValue.res } } public var flags: IOCompletion.Flags { get { - return Flags(rawValue: rawValue.flags & 0x0000FFFF) + Flags(rawValue: rawValue.flags & 0x0000FFFF) } } From 48455a913557acf7e7e5f0b7a209e7b6fa9a2c6a Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 23:04:52 +0000 Subject: [PATCH 059/172] Fix plumbing --- Sources/System/IORequest.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 7b602060..1eed6edf 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -95,6 +95,7 @@ internal func makeRawRequest_readWrite_registered( request.buffer = buffer.unsafeBuffer request.rawValue.buf_index = UInt16(exactly: buffer.index)! request.offset = offset + request.rawValue.user_data = userData return request } @@ -111,10 +112,11 @@ internal func makeRawRequest_readWrite_registered_slot( request.buffer = buffer.unsafeBuffer request.rawValue.buf_index = UInt16(exactly: buffer.index)! request.offset = offset + request.rawValue.user_data = userData return request } -@inlinable @inline(__always) +@inline(__always) internal func makeRawRequest_readWrite_unregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, @@ -125,6 +127,7 @@ internal func makeRawRequest_readWrite_unregistered( request.fileDescriptor = file request.buffer = buffer request.offset = offset + request.rawValue.user_data = userData return request } @@ -140,6 +143,7 @@ internal func makeRawRequest_readWrite_unregistered_slot( request.flags = .fixedFile request.buffer = buffer request.offset = offset + request.rawValue.user_data = userData return request } From 72c316ba86750e2cd3e223afd8ee698795e4efe0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 3 Mar 2025 23:59:36 +0000 Subject: [PATCH 060/172] Make completions noncopyable again --- Sources/System/IOCompletion.swift | 3 +-- Sources/System/IORing.swift | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 9b41214e..ee81797e 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -1,7 +1,6 @@ @_implementationOnly import CSystem -//TODO: should be ~Copyable, but requires UnsafeContinuation add ~Copyable support -public struct IOCompletion { +public struct IOCompletion: ~Copyable { let rawValue: io_uring_cqe } diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 4525281a..79840c1c 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -338,7 +338,7 @@ public struct IORing: ~Copyable { minimumCount: UInt32, maximumCount: UInt32, extraArgs: UnsafeMutablePointer? = nil, - consumer: (IOCompletion?, IORingError?, Bool) throws -> Void + consumer: (consuming IOCompletion?, IORingError?, Bool) throws -> Void ) rethrows { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { @@ -407,15 +407,15 @@ public struct IORing: ~Copyable { ) throws -> IOCompletion { var result: IOCompletion? = nil try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { - (completion, error, done) in + (completion: consuming IOCompletion?, error, done) in if let error { throw error } - if let completion { - result = completion + if let completion { + result = consume completion } } - return result.unsafelyUnwrapped + return result.take()! } public func blockingConsumeCompletion( @@ -443,7 +443,7 @@ public struct IORing: ~Copyable { public func blockingConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (IOCompletion?, IORingError?, Bool) throws -> Void + consumer: (consuming IOCompletion?, IORingError?, Bool) throws -> Void ) throws { if let timeout { var ts = __kernel_timespec( @@ -603,7 +603,7 @@ public struct IORing: ~Copyable { public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (IOCompletion?, IORingError?, Bool) throws -> Void + consumer: (consuming IOCompletion?, IORingError?, Bool) throws -> Void ) throws { //TODO: optimize this to one uring_enter try submitPreparedRequests() From d6b40a72846b5d1027728ad148bbcdea02315096 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Tue, 4 Mar 2025 23:22:34 -0800 Subject: [PATCH 061/172] [CI] Add support for GitHub Actions --- .github/workflows/pull_request.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..6936d99a --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,17 @@ +name: Pull request + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + tests: + name: Test + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + linux_exclude_swift_versions: '[{"swift_version": "5.8"}]' + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Swift.org" From 7fff872441b6acb03bb8922300c09ef5f2afae2b Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 6 Mar 2025 12:09:57 -0800 Subject: [PATCH 062/172] Add the draft proposal so I can link to it --- NNNN-swift-system-io-uring.md | 434 ++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 NNNN-swift-system-io-uring.md diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md new file mode 100644 index 00000000..9b0eb1dc --- /dev/null +++ b/NNNN-swift-system-io-uring.md @@ -0,0 +1,434 @@ +# IORing, a Swift System API for io_uring + +* Proposal: [SE-NNNN](NNNN-filename.md) +* Authors: [Lucy Satheesan](https://github.com/oxy), [David Smith](https://github.com/Catfish-Man/) +* Review Manager: TBD +* Status: **Awaiting implementation** +* Implementation: [apple/swift-system#208](https://github.com/apple/swift-system/pull/208) + +## Introduction + +`io_uring` is Linux's solution to asynchronous and batched syscalls, with a particular focus on IO. We propose a low-level Swift API for it in Swift System that could either be used directly by projects with unusual needs, or via intermediaries like Swift NIO, to address scalability and thread pool starvation issues. + +## Motivation + +Up until recently, the overwhelmingly dominant file IO syscalls on major Unix platforms have been synchronous, e.g. `read(2)`. This design is very simple and proved sufficient for many uses for decades, but is less than ideal for Swift's needs in a few major ways: + +1. Requiring an entire OS thread for each concurrent operation imposes significant memory overhead +2. Requiring a separate syscall for each operation imposes significant CPU/time overhead to switch into and out of kernel mode repeatedly. This has been exacerbated in recent years by mitigations for the Spectre family of security exploits increasing the cost of syscalls. +3. Swift's N:M coroutine-on-thread-pool concurrency model assumes that threads will not be blocked. Each thread waiting for a syscall means a CPU core being left idle. In practice systems like NIO that deal in highly concurrent IO have had to work around this by providing their own thread pools. + +Non-file IO (network, pipes, etc…) has been in a somewhat better place with `epoll` and `kqueue` for asynchronously waiting for readability, but syscall overhead remains a significant issue for highly scalable systems. + +With the introduction of `io_uring` in 2019, Linux now has the kernel level tools to address these three problems directly. However, `io_uring` is quite complex and maps poorly into Swift. We expect that by providing a Swift interface to it, we can enable Swift on Linux servers to scale better and be more efficient than it has been in the past. + +## Proposed solution + +`struct IORing: ~Copyable` provides facilities for + +* Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file descriptors that improves their efficiency +* Registering and unregistering eventfds, which allow asynchronous waiting for completions +* Enqueueing IO requests +* Dequeueing IO completions + +`class IOResource` represents, via its two typealiases `IORingFileSlot` and `IORingBuffer`, registered file descriptors and buffers. Ideally we'd express the lifetimes of these as being dependent on the lifetime of the ring, but so far that's proven intractable, so we use a reference type. We expect that the up-front overhead of this should be negligible for larger operations, and smaller or one-shot operations can use non-registered buffers and file descriptors. + +`struct IORequest: ~Copyable` represents an IO operation that can be enqueued for the kernel to execute. It supports a wide variety of operations matching traditional unix file and socket operations. + +IORequest operations are expressed as overloaded static methods on `IORequest`, e.g. `openat` is spelled + +```swift + public static func opening( + _ path: FilePath, + in directory: FileDescriptor, + into slot: IORingFileSlot, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> IORequest + + public static func opening( + _ path: FilePath, + in directory: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> IORequest +``` + +which allows clients to decide whether they want to open the file into a slot on the ring, or have it return a file descriptor via a completion. Similarly, read operations have overloads for "use a buffer from the ring" or "read into this `UnsafeMutableBufferPointer`" + +Multiple `IORequests` can be enqueued on a single `IORing` using the `prepare(…)` family of methods, and then submitted together using `submitPreparedRequests`, allowing for things like "open this file, read its contents, and then close it" to be a single syscall. Conveniences are provided for preparing and submitting requests in one call. + +Since IO operations can execute in parallel or out of order by default, linked chains of operations can be established with `prepare(linkedRequests:…)` and related methods. Separate chains can still execute in parallel, and if an operation early in the chain fails, all subsequent operations will deliver cancellation errors as their completion. + +Already-completed results can be retrieved from the ring using `tryConsumeCompletion`, which never waits but may return nil, or `blockingConsumeCompletion(timeout:)`, which synchronously waits (up to an optional timeout) until an operation completes. There's also a bulk version of `blockingConsumeCompletion`, which may reduce the number of syscalls issued. It takes a closure which will be called repeatedly as completions are available (see Future Directions for potential improvements to this API). + +Since neither polling nor synchronously waiting is optimal in many cases, `IORing` also exposes the ability to register an eventfd (see `man eventfd(2)`), which will become readable when completions are available on the ring. This can then be monitored asynchronously with `epoll`, `kqueue`, or for clients who are linking libdispatch, `DispatchSource`. + +`struct IOCompletion: ~Copyable` represents the result of an IO operation and provides + +* Flags indicating various operation-specific metadata about the now-completed syscall +* The context associated with the operation when it was enqueued, as an `UnsafeRawPointer` or a `UInt64` +* The result of the operation, as an `Int32` with operation-specific meaning +* The error, if one occurred + +Unfortunately the underlying kernel API makes it relatively difficult to determine which `IORequest` led to a given `IOCompletion`, so it's expected that users will need to create this association themselves via the context parameter. + +`IORingError` represents failure of an operation. + +`IORing.Features` describes the supported features of the underlying kernel `IORing` implementation, which can be used to provide graceful reduction in functionality when running on older systems. + +## Detailed design + +```swift +public class IOResource { } +public typealias IORingFileSlot = IOResource +public typealias IORingBuffer = IOResource + +extension IORingBuffer { + public var unsafeBuffer: UnsafeMutableRawBufferPointer +} + +// IORing is intentionally not Sendable, to avoid internal locking overhead +public struct IORing: ~Copyable { + + public init(queueDepth: UInt32) throws(IORingError) + + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(IORingError) + public mutating func unregisterEventFD(_ descriptor: FileDescriptor) throws(IORingError) + + // An IORing.RegisteredResources is a view into the buffers or files registered with the ring, if any + public struct RegisteredResources: RandomAccessCollection { + public subscript(position: Int) -> IOResource + public subscript(position: UInt16) -> IOResource // This is useful because io_uring likes to use UInt16s as indexes + } + + public mutating func registerFileSlots(count: Int) throws(IORingError) -> RegisteredResources + + public func unregisterFiles() + + public var registeredFileSlots: RegisteredResources + + public mutating func registerBuffers( + _ buffers: some Collection + ) throws(IORingError) -> RegisteredResources + + public mutating func registerBuffers( + _ buffers: UnsafeMutableRawBufferPointer... + ) throws(IORingError) -> RegisteredResources + + public func unregisterBuffers() + + public var registeredBuffers: RegisteredResources + + public func prepare(requests: IORequest...) + public func prepare(linkedRequests: IORequest...) + + public func submitPreparedRequests(timeout: Duration? = nil) throws(IORingError) + public func submit(requests: IORequest..., timeout: Duration? = nil) throws(IORingError) + public func submit(linkedRequests: IORequest..., timeout: Duration? = nil) throws(IORingError) + + public func submitPreparedRequests() throws(IORingError) + public func submitPreparedRequestsAndWait(timeout: Duration? = nil) throws(IORingError) + + public func submitPreparedRequestsAndConsumeCompletions( + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (consuming IOCompletion?, IORingError?, Bool) throws(E) -> Void + ) throws(E) + + public func blockingConsumeCompletion( + timeout: Duration? = nil + ) throws(IORingError) -> IOCompletion + + public func blockingConsumeCompletions( + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (consuming IOCompletion?, IORingError?, Bool) throws(E) -> Void + ) throws(E) + + public func tryConsumeCompletion() -> IOCompletion? + + public struct Features { + //IORING_FEAT_SINGLE_MMAP is handled internally + public var nonDroppingCompletions: Bool //IORING_FEAT_NODROP + public var stableSubmissions: Bool //IORING_FEAT_SUBMIT_STABLE + public var currentFilePosition: Bool //IORING_FEAT_RW_CUR_POS + public var assumingTaskCredentials: Bool //IORING_FEAT_CUR_PERSONALITY + public var fastPolling: Bool //IORING_FEAT_FAST_POLL + public var epoll32BitFlags: Bool //IORING_FEAT_POLL_32BITS + public var pollNonFixedFiles: Bool //IORING_FEAT_SQPOLL_NONFIXED + public var extendedArguments: Bool //IORING_FEAT_EXT_ARG + public var nativeWorkers: Bool //IORING_FEAT_NATIVE_WORKERS + public var resourceTags: Bool //IORING_FEAT_RSRC_TAGS + public var allowsSkippingSuccessfulCompletions: Bool //IORING_FEAT_CQE_SKIP + public var improvedLinkedFiles: Bool //IORING_FEAT_LINKED_FILE + public var registerRegisteredRings: Bool //IORING_FEAT_REG_REG_RING + public var minimumTimeout: Bool //IORING_FEAT_MIN_TIMEOUT + public var bundledSendReceive: Bool //IORING_FEAT_RECVSEND_BUNDLE + } + public static var supportedFeatures: Features +} + +public struct IORequest: ~Copyable { + public static func nop(context: UInt64 = 0) -> IORequest + + // overloads for each combination of registered vs unregistered buffer/descriptor + // Read + public static func reading( + _ file: IORingFileSlot, + into buffer: IORingBuffer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + public static func reading( + _ file: FileDescriptor, + into buffer: IORingBuffer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + public static func reading( + _ file: IORingFileSlot, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + public static func reading( + _ file: FileDescriptor, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + // Write + public static func writing( + _ buffer: IORingBuffer, + into file: IORingFileSlot, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + public static func writing( + _ buffer: IORingBuffer, + into file: FileDescriptor, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + public static func writing( + _ buffer: UnsafeMutableRawBufferPointer, + into file: IORingFileSlot, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + public static func writing( + _ buffer: UnsafeMutableRawBufferPointer, + into file: FileDescriptor, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> IORequest + + // Close + public static func closing( + _ file: FileDescriptor, + context: UInt64 = 0 + ) -> IORequest + + public static func closing( + _ file: IORingFileSlot, + context: UInt64 = 0 + ) -> IORequest + + // Open At + public static func opening( + _ path: FilePath, + in directory: FileDescriptor, + into slot: IORingFileSlot, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> IORequest + + public static func opening( + _ path: FilePath, + in directory: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> IORequest + + public static func unlinking( + _ path: FilePath, + in directory: FileDescriptor, + context: UInt64 = 0 + ) -> IORequest + + // Other operations follow in the same pattern +} + +public struct IOCompletion { + + public struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt32 + + public init(rawValue: UInt32) + + public static let moreCompletions: Flags + public static let socketNotEmpty: Flags + public static let isNotificationEvent: Flags + } + + //These are both the same value, but having both eliminates some ugly casts in client code + public var context: UInt64 + public var contextPointer: UnsafeRawPointer + + public var result: Int32 + + public var error: IORingError? // Convenience wrapper over `result` + + public var flags: Flags +} + +public struct IORingError: Error, Equatable { + static var missingRequiredFeatures: IORingError + static var operationCanceled: IORingError + static var timedOut: IORingError + static var resourceRegistrationFailed: IORingError + // Other error values to be filled out as the set of supported operations expands in the future + static var unknown: IORingError(errorCode: Int) +} + +``` + +## Usage Examples + +### Blocking + +```swift +let ring = try IORing(queueDepth: 2) + +//Make space on the ring for our file (this is optional, but improves performance with repeated use) +let file = ring.registerFiles(count: 1)[0] + +var statInfo = Glibc.stat() // System doesn't have an abstraction for stat() right now +// Build our requests to open the file and find out how big it is +ring.prepare(linkedRequests: + .opening(path, + in: parentDirectory, + into: file, + mode: mode, + options: openOptions, + permissions: nil + ), + .readingMetadataOf(file, + into: &statInfo + ) +) +//batch submit 2 syscalls in 1! +try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming IOCompletion?, error, done) in + if let error { + throw error //or other error handling as desired + } +} + +// We could register our buffer with the ring too, but we're only using it once +let buffer = UnsafeMutableRawBufferPointer.allocate(Int(statInfo.st_size)) + +// Build our requests to read the file and close it +ring.prepare(linkedRequests: + .reading(file, + into: buffer + ), + .closing(file) +) + +//batch submit 2 syscalls in 1! +try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming IOCompletion?, error, done) in + if let error { + throw error //or other error handling as desired + } +} + +processBuffer(buffer) +``` + +### Using libdispatch to wait for the read asynchronously + +```swift +//Initial setup as above up through creating buffer, omitted for brevity + +//Make the read request with a context so we can get the buffer out of it in the completion handler +… +.reading(file, into: buffer, context: UInt64(buffer.baseAddress!)) +… + +// Make an eventfd and register it with the ring +let eventfd = eventfd(0, 0) +ring.registerEventFD(eventfd) + +// Make a read source to monitor the eventfd for readability +let readabilityMonitor = DispatchSource.makeReadSource(fileDescriptor: eventfd) +readabilityMonitor.setEventHandler { + let completion = ring.blockingConsumeCompletion() + if let error = completion.error { + //handle failure to read the file + } + processBuffer(completion.contextPointer) +} +readabilityMonitor.activate() + +ring.submitPreparedRequests //note, not "AndConsumeCompletions" this time +``` + +## Source compatibility + +This is an all-new API in Swift System, so has no backwards compatibility implications. Of note, though, this API is only available on Linux. + +## ABI compatibility + +Swift on Linux does not have a stable ABI, and we will likely take advantage of this to evolve IORing as compiler support improves, as described in Future Directions. + +## Implications on adoption + +This feature is intrinsically linked to Linux kernel support, so constrains the deployment target of anything that adopts it to newer kernels. Exactly which features of the evolving io_uring syscall surface area we need is under consideration. + +## Future directions + +* While most Swift users on Darwin are not limited by IO scalability issues, the thread pool considerations still make introducing something similar to this appealing if and when the relevant OS support is available. We should attempt to the best of our ability to not design this in a way that's gratuitously incompatible with non-Linux OSs, although Swift System does not attempt to have an API that's identical on all platforms. +* The set of syscalls covered by `io_uring` has grown significantly and is still growing. We should leave room for supporting additional operations in the future. +* Once same-element requirements and pack counts as integer generic arguments are supported by the compiler, we should consider adding something along the lines of the following to allow preparing, submitting, and waiting for an entire set of operations at once: + +``` +func submitLinkedRequestsAndWait( + _ requests: repeat each Request +) where Request == IORequest + -> InlineArray<(repeat each Request).count, IOCompletion> +``` +* Once mutable borrows are supported, we should consider replacing the closure-taking bulk completion APIs (e.g. `blockingConsumeCompletions(…)`) with ones that return a sequence of completions instead +* We should consider making more types noncopyable as compiler support improves +* liburing has a "peek next completion" operation that doesn't consume it, and then a "mark consumed" operation. We may want to add something similar +* liburing has support for operations allocating their own buffers and returning them via the completion, we may want to support this +* We may want to provide API for asynchronously waiting, rather than just exposing the eventfd to let people roll their own async waits. Doing this really well has *considerable* implications for the concurrency runtime though. +* We should almost certainly expose API for more of the configuration options in `io_uring_setup` +* The API for feature probing is functional but not especially nice. Finding a better way to present that concept would be desirable. + +## Alternatives considered + +* We could use a NIO-style separate thread pool, but we believe `io_uring` is likely a better option for scalability. We may still want to provide a thread-pool backed version as an option, because many Linux systems currently disable `io_uring` due to security concerns. +* We could multiplex all IO onto a single actor as `AsyncBytes` currently does, but this has a number of downsides that make it entirely unsuitable to server usage. Most notably, it eliminates IO parallelism entirely. +* Using POSIX AIO instead of or as well as io_uring would greatly increase our ability to support older kernels and other Unix systems, but it has well-documented performance and usability issues that have prevented its adoption elsewhere, and apply just as much to Swift. +* Earlier versions of this proposal had higher level "managed" abstractions over IORing. These have been removed due to lack of interest from clients, but could be added back later if needed. +* I considered making any or all of `IORingError`, `IOCompletion`, and `IORequest` nested struct declarations inside `IORing`. The main reason I haven't done so is I was a little concerned about the ambiguity of having a type called `Error`. I'd be particularly interested in feedback on this choice. + +## Acknowledgments + +The NIO team, in particular Cory Benfield and Franz Busch, have provided invaluable feedback and direction on this project. From 2ac80d6f4e53d86c9924eacdba920e6c176932b1 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Sun, 9 Mar 2025 18:29:05 -0700 Subject: [PATCH 063/172] Disable a few checks --- .github/workflows/pull_request.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6936d99a..6ab954b5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,8 +10,16 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_exclude_swift_versions: '[{"swift_version": "5.8"}]' + # https://github.com/apple/swift-system/issues/223 + enable_windows_checks: false soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift.org" + # https://github.com/apple/swift-system/issues/224 + docs_check_enabled: false + unacceptable_language_check_enabled: false + license_header_check_enabled: false + format_check_enabled: false + python_lint_check_enabled: false From 009b07e140205b3ec1521dfa488ac7a8e8f015d7 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 13 Mar 2025 08:59:59 -0400 Subject: [PATCH 064/172] Add Android imports to tests --- Tests/SystemTests/FileOperationsTest.swift | 3 +++ Tests/SystemTests/FileTypesTest.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index ed05dcf4..18adde37 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -14,6 +14,9 @@ import XCTest #else @testable import System #endif +#if canImport(Android) +import Android +#endif @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) final class FileOperationsTest: XCTestCase { diff --git a/Tests/SystemTests/FileTypesTest.swift b/Tests/SystemTests/FileTypesTest.swift index 58ceac1b..5258709a 100644 --- a/Tests/SystemTests/FileTypesTest.swift +++ b/Tests/SystemTests/FileTypesTest.swift @@ -14,6 +14,9 @@ import SystemPackage #else import System #endif +#if canImport(Android) +import Android +#endif @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) final class FileDescriptorTest: XCTestCase { From 0e652c4d25adfda3e40e1001357261cfabbc93ef Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 4 Apr 2025 16:42:33 -0700 Subject: [PATCH 065/172] Naming updates, cancellation support, IOResources are structs now, other editorial updates --- NNNN-swift-system-io-uring.md | 117 ++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md index 9b0eb1dc..5c08588b 100644 --- a/NNNN-swift-system-io-uring.md +++ b/NNNN-swift-system-io-uring.md @@ -15,7 +15,7 @@ Up until recently, the overwhelmingly dominant file IO syscalls on major Unix platforms have been synchronous, e.g. `read(2)`. This design is very simple and proved sufficient for many uses for decades, but is less than ideal for Swift's needs in a few major ways: 1. Requiring an entire OS thread for each concurrent operation imposes significant memory overhead -2. Requiring a separate syscall for each operation imposes significant CPU/time overhead to switch into and out of kernel mode repeatedly. This has been exacerbated in recent years by mitigations for the Spectre family of security exploits increasing the cost of syscalls. +2. Requiring a separate syscall for each operation imposes significant CPU/time overhead to switch into and out of kernel mode repeatedly. This has been exacerbated in recent years by mitigations for the Meltdown family of security exploits increasing the cost of syscalls. 3. Swift's N:M coroutine-on-thread-pool concurrency model assumes that threads will not be blocked. Each thread waiting for a syscall means a CPU core being left idle. In practice systems like NIO that deal in highly concurrent IO have had to work around this by providing their own thread pools. Non-file IO (network, pipes, etc…) has been in a somewhat better place with `epoll` and `kqueue` for asynchronously waiting for readability, but syscall overhead remains a significant issue for highly scalable systems. @@ -24,6 +24,8 @@ With the introduction of `io_uring` in 2019, Linux now has the kernel level tool ## Proposed solution +We propose a *low level, unopinionated* Swift interface for io_uring on Linux (see Future Directions for discussion of possible more abstract interfaces). + `struct IORing: ~Copyable` provides facilities for * Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file descriptors that improves their efficiency @@ -31,14 +33,14 @@ With the introduction of `io_uring` in 2019, Linux now has the kernel level tool * Enqueueing IO requests * Dequeueing IO completions -`class IOResource` represents, via its two typealiases `IORingFileSlot` and `IORingBuffer`, registered file descriptors and buffers. Ideally we'd express the lifetimes of these as being dependent on the lifetime of the ring, but so far that's proven intractable, so we use a reference type. We expect that the up-front overhead of this should be negligible for larger operations, and smaller or one-shot operations can use non-registered buffers and file descriptors. +`struct IOResource` represents, via its two typealiases `IORingFileSlot` and `IORingBuffer`, registered file descriptors and buffers. `struct IORequest: ~Copyable` represents an IO operation that can be enqueued for the kernel to execute. It supports a wide variety of operations matching traditional unix file and socket operations. IORequest operations are expressed as overloaded static methods on `IORequest`, e.g. `openat` is spelled ```swift - public static func opening( + public static func open( _ path: FilePath, in directory: FileDescriptor, into slot: IORingFileSlot, @@ -48,7 +50,7 @@ IORequest operations are expressed as overloaded static methods on `IORequest`, context: UInt64 = 0 ) -> IORequest - public static func opening( + public static func open( _ path: FilePath, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, @@ -84,7 +86,7 @@ Unfortunately the underlying kernel API makes it relatively difficult to determi ## Detailed design ```swift -public class IOResource { } +public struct IOResource { } public typealias IORingFileSlot = IOResource public typealias IORingBuffer = IOResource @@ -152,23 +154,27 @@ public struct IORing: ~Copyable { public func tryConsumeCompletion() -> IOCompletion? - public struct Features { + public struct Features: OptionSet { + let rawValue: UInt32 + + public init(rawValue: UInt32) + //IORING_FEAT_SINGLE_MMAP is handled internally - public var nonDroppingCompletions: Bool //IORING_FEAT_NODROP - public var stableSubmissions: Bool //IORING_FEAT_SUBMIT_STABLE - public var currentFilePosition: Bool //IORING_FEAT_RW_CUR_POS - public var assumingTaskCredentials: Bool //IORING_FEAT_CUR_PERSONALITY - public var fastPolling: Bool //IORING_FEAT_FAST_POLL - public var epoll32BitFlags: Bool //IORING_FEAT_POLL_32BITS - public var pollNonFixedFiles: Bool //IORING_FEAT_SQPOLL_NONFIXED - public var extendedArguments: Bool //IORING_FEAT_EXT_ARG - public var nativeWorkers: Bool //IORING_FEAT_NATIVE_WORKERS - public var resourceTags: Bool //IORING_FEAT_RSRC_TAGS - public var allowsSkippingSuccessfulCompletions: Bool //IORING_FEAT_CQE_SKIP - public var improvedLinkedFiles: Bool //IORING_FEAT_LINKED_FILE - public var registerRegisteredRings: Bool //IORING_FEAT_REG_REG_RING - public var minimumTimeout: Bool //IORING_FEAT_MIN_TIMEOUT - public var bundledSendReceive: Bool //IORING_FEAT_RECVSEND_BUNDLE + public static let nonDroppingCompletions: Bool //IORING_FEAT_NODROP + public static let stableSubmissions: Bool //IORING_FEAT_SUBMIT_STABLE + public static let currentFilePosition: Bool //IORING_FEAT_RW_CUR_POS + public static let assumingTaskCredentials: Bool //IORING_FEAT_CUR_PERSONALITY + public static let fastPolling: Bool //IORING_FEAT_FAST_POLL + public static let epoll32BitFlags: Bool //IORING_FEAT_POLL_32BITS + public static let pollNonFixedFiles: Bool //IORING_FEAT_SQPOLL_NONFIXED + public static let extendedArguments: Bool //IORING_FEAT_EXT_ARG + public static let nativeWorkers: Bool //IORING_FEAT_NATIVE_WORKERS + public static let resourceTags: Bool //IORING_FEAT_RSRC_TAGS + public static let allowsSkippingSuccessfulCompletions: Bool //IORING_FEAT_CQE_SKIP + public static let improvedLinkedFiles: Bool //IORING_FEAT_LINKED_FILE + public static let registerRegisteredRings: Bool //IORING_FEAT_REG_REG_RING + public static let minimumTimeout: Bool //IORING_FEAT_MIN_TIMEOUT + public static let bundledSendReceive: Bool //IORING_FEAT_RECVSEND_BUNDLE } public static var supportedFeatures: Features } @@ -178,28 +184,28 @@ public struct IORequest: ~Copyable { // overloads for each combination of registered vs unregistered buffer/descriptor // Read - public static func reading( + public static func read( _ file: IORingFileSlot, into buffer: IORingBuffer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest - public static func reading( + public static func read( _ file: FileDescriptor, into buffer: IORingBuffer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest - public static func reading( + public static func read( _ file: IORingFileSlot, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest - public static func reading( + public static func read( _ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, @@ -207,28 +213,28 @@ public struct IORequest: ~Copyable { ) -> IORequest // Write - public static func writing( + public static func write( _ buffer: IORingBuffer, into file: IORingFileSlot, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest - public static func writing( + public static func write( _ buffer: IORingBuffer, into file: FileDescriptor, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest - public static func writing( + public static func write( _ buffer: UnsafeMutableRawBufferPointer, into file: IORingFileSlot, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest - public static func writing( + public static func write( _ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, at offset: UInt64 = 0, @@ -236,18 +242,18 @@ public struct IORequest: ~Copyable { ) -> IORequest // Close - public static func closing( + public static func close( _ file: FileDescriptor, context: UInt64 = 0 ) -> IORequest - public static func closing( + public static func close( _ file: IORingFileSlot, context: UInt64 = 0 ) -> IORequest // Open At - public static func opening( + public static func open( _ path: FilePath, in directory: FileDescriptor, into slot: IORingFileSlot, @@ -257,7 +263,7 @@ public struct IORequest: ~Copyable { context: UInt64 = 0 ) -> IORequest - public static func opening( + public static func open( _ path: FilePath, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, @@ -266,12 +272,42 @@ public struct IORequest: ~Copyable { context: UInt64 = 0 ) -> IORequest - public static func unlinking( + public static func unlink( _ path: FilePath, in directory: FileDescriptor, context: UInt64 = 0 ) -> IORequest + // Cancel + + public enum CancellationMatch { + case all + case first + } + + public static func cancel( + _ matchAll: CancellationMatch, + matchingContext: UInt64, + context: UInt64 + ) -> IORequest + + public static func cancel( + _ matchAll: CancellationMatch, + matchingFileDescriptor: FileDescriptor, + context: UInt64 + ) -> IORequest + + public static func cancel( + _ matchAll: CancellationMatch, + matchingRegisteredFileDescriptorAtIndex: Int, + context: UInt64 + ) -> IORequest + + public static func cancel( + _ matchAll: CancellationMatch, + context: UInt64 + ) -> IORequest + // Other operations follow in the same pattern } @@ -322,14 +358,14 @@ let file = ring.registerFiles(count: 1)[0] var statInfo = Glibc.stat() // System doesn't have an abstraction for stat() right now // Build our requests to open the file and find out how big it is ring.prepare(linkedRequests: - .opening(path, + .open(path, in: parentDirectory, into: file, mode: mode, options: openOptions, permissions: nil ), - .readingMetadataOf(file, + .stat(file, into: &statInfo ) ) @@ -345,10 +381,10 @@ let buffer = UnsafeMutableRawBufferPointer.allocate(Int(statInfo.st_size)) // Build our requests to read the file and close it ring.prepare(linkedRequests: - .reading(file, + .read(file, into: buffer ), - .closing(file) + .close(file) ) //batch submit 2 syscalls in 1! @@ -368,7 +404,7 @@ processBuffer(buffer) //Make the read request with a context so we can get the buffer out of it in the completion handler … -.reading(file, into: buffer, context: UInt64(buffer.baseAddress!)) +.read(file, into: buffer, context: UInt64(buffer.baseAddress!)) … // Make an eventfd and register it with the ring @@ -419,7 +455,7 @@ func submitLinkedRequestsAndWait( * liburing has support for operations allocating their own buffers and returning them via the completion, we may want to support this * We may want to provide API for asynchronously waiting, rather than just exposing the eventfd to let people roll their own async waits. Doing this really well has *considerable* implications for the concurrency runtime though. * We should almost certainly expose API for more of the configuration options in `io_uring_setup` -* The API for feature probing is functional but not especially nice. Finding a better way to present that concept would be desirable. +* Stronger safety guarantees around cancellation and resource lifetimes (e.g. as described in https://without.boats/blog/io-uring/) would be very welcome, but require an API that is much more strongly opinionated about how io_uring is used. A future higher level abstraction focused on the goal of being "an async IO API for Swift" rather than "a Swifty interface to io_uring" seems like a good place for that. ## Alternatives considered @@ -428,6 +464,7 @@ func submitLinkedRequestsAndWait( * Using POSIX AIO instead of or as well as io_uring would greatly increase our ability to support older kernels and other Unix systems, but it has well-documented performance and usability issues that have prevented its adoption elsewhere, and apply just as much to Swift. * Earlier versions of this proposal had higher level "managed" abstractions over IORing. These have been removed due to lack of interest from clients, but could be added back later if needed. * I considered making any or all of `IORingError`, `IOCompletion`, and `IORequest` nested struct declarations inside `IORing`. The main reason I haven't done so is I was a little concerned about the ambiguity of having a type called `Error`. I'd be particularly interested in feedback on this choice. +* IOResource was originally a class in an attempt to manage the lifetime of the resource via language features. Changing to the current model of it being a copyable struct didn't make the lifetime management any less safe (the IORing still owns the actual resource), and reduces overhead. In the future it would be neat if we could express IOResources as being borrowed from the IORing so they can't be used after its lifetime. ## Acknowledgments From b40b09440c5ce0bca9d2633b8d374a156d95e053 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 16 Apr 2025 23:37:50 +0000 Subject: [PATCH 066/172] Revamp error handling --- Sources/System/IORing.swift | 170 +++++++++++++++++++++---------- Sources/System/IORingError.swift | 10 -- 2 files changed, 117 insertions(+), 63 deletions(-) delete mode 100644 Sources/System/IORingError.swift diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 79840c1c..3ef92997 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -118,7 +118,7 @@ internal func _enter( numEvents: UInt32, minCompletions: UInt32, flags: UInt32 -) throws -> Int32 { +) throws(IORing.OperationError) -> Int32 { // Ring always needs enter right now; // TODO: support SQPOLL here while true { @@ -133,16 +133,14 @@ internal func _enter( //TODO: should we wait a bit on AGAIN? continue } else if ret < 0 { - fatalError( - "fatal error in submitting requests: " + Errno(rawValue: -ret).debugDescription - ) + throw(IORing.OperationError(result: -ret)) } else { return ret } } } -internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws { +internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws(IORing.OperationError) { let flushedEvents = _flushQueue(ring: ring) _ = try _enter( ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) @@ -193,7 +191,40 @@ internal func _getSubmissionEntry( return nil } +//TODO: make this not an enum. And maybe split it up? And move it into IORing + public struct IORing: ~Copyable { + + /// Errors in either submitting operations or receiving operation completions + public struct OperationError: Error, Hashable { + private var errorCode: Int + public static var operationCanceled: OperationError { .init(result: ECANCELED) } + + internal init(result: Int32) { + errorCode = Int(result) //TODO, flesh this out + } + } + + public struct SetupError: Error, Hashable { + private var errorCode: Int + + public static var missingRequiredFeatures: SetupError { + .init(setupResult: -1) /* TODO: numeric value */ + } + + internal init(setupResult: Int32) { + errorCode = Int(setupResult) //TODO, flesh this out + } + } + + public struct RegistrationError: Error, Hashable { + private var errorCode: Int + + internal init(registrationResult: Int32) { + errorCode = Int(registrationResult) //TODO, flesh this out + } + } + let ringFlags: UInt32 let ringDescriptor: Int32 @@ -212,7 +243,7 @@ public struct IORing: ~Copyable { var _registeredFiles: [UInt32]? var _registeredBuffers: [iovec]? - public init(queueDepth: UInt32) throws { + public init(queueDepth: UInt32) throws(SetupError) { var params = io_uring_params() ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { @@ -224,7 +255,7 @@ public struct IORing: ~Copyable { { close(ringDescriptor) // TODO: error handling - throw IORingError.missingRequiredFeatures + throw .missingRequiredFeatures } if ringDescriptor < 0 { @@ -334,17 +365,17 @@ public struct IORing: ~Copyable { self.ringFlags = params.flags } - private func _blockingConsumeCompletionGuts( + private func _blockingConsumeCompletionGuts( minimumCount: UInt32, maximumCount: UInt32, extraArgs: UnsafeMutablePointer? = nil, - consumer: (consuming IOCompletion?, IORingError?, Bool) throws -> Void - ) rethrows { + consumer: (consuming IOCompletion?, OperationError?, Bool) throws(Err) -> Void + ) throws(Err) { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 if completion.result < 0 { - try consumer(nil, IORingError(completionResult: completion.result), false) + try consumer(nil, OperationError(result: completion.result), false) } else { try consumer(completion, nil, false) } @@ -389,12 +420,12 @@ public struct IORing: ~Copyable { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 - if completion.result < 0 { - try consumer(nil, IORingError(completionResult: completion.result), false) - } else { - try consumer(completion, nil, false) - } - if count == maximumCount { + if completion.result < 0 { + try consumer(nil, OperationError(result: completion.result), false) + } else { + try consumer(completion, nil, false) + } + if count == maximumCount { break } } @@ -404,14 +435,14 @@ public struct IORing: ~Copyable { internal func _blockingConsumeOneCompletion( extraArgs: UnsafeMutablePointer? = nil - ) throws -> IOCompletion { + ) throws(OperationError) -> IOCompletion { var result: IOCompletion? = nil try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { (completion: consuming IOCompletion?, error, done) in if let error { throw error } - if let completion { + if let completion { result = consume completion } } @@ -420,46 +451,65 @@ public struct IORing: ~Copyable { public func blockingConsumeCompletion( timeout: Duration? = nil - ) throws -> IOCompletion { + ) throws(OperationError) -> IOCompletion { if let timeout { var ts = __kernel_timespec( tv_sec: timeout.components.seconds, tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) - return try withUnsafePointer(to: &ts) { tsPtr in + var err: OperationError? = nil + var result: IOCompletion? = nil + result = try withUnsafePointer(to: &ts) { tsPtr in var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, pad: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) - return try _blockingConsumeOneCompletion(extraArgs: &args) + do { + return try _blockingConsumeOneCompletion(extraArgs: &args) + } catch (let e) { + err = e as! OperationError + return nil + } + } + guard let result else { + throw(err!) } + return result } else { return try _blockingConsumeOneCompletion() } } - public func blockingConsumeCompletions( + public func blockingConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, IORingError?, Bool) throws -> Void - ) throws { + consumer: (consuming IOCompletion?, OperationError?, Bool) throws(Err) -> Void + ) throws(Err) { if let timeout { var ts = __kernel_timespec( tv_sec: timeout.components.seconds, tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) - return try withUnsafePointer(to: &ts) { tsPtr in + var err: Err? = nil + withUnsafePointer(to: &ts) { tsPtr in var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, pad: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) - try _blockingConsumeCompletionGuts( - minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, - consumer: consumer) + do { + try _blockingConsumeCompletionGuts( + minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, + consumer: consumer) + } catch (let e) { + err = e as! Err //TODO: why is `e` coming in as `any Error`? That seems wrong + } + } + if let err { + throw(err) } } else { try _blockingConsumeCompletionGuts( @@ -489,34 +539,35 @@ public struct IORing: ~Copyable { return nil } - internal func handleRegistrationResult(_ result: Int32) throws { - //TODO: error handling - } - - public mutating func registerEventFD(_ descriptor: FileDescriptor) throws { + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(RegistrationError) { var rawfd = descriptor.rawValue let result = withUnsafePointer(to: &rawfd) { fdptr in - return io_uring_register( + let result = io_uring_register( ringDescriptor, IORING_REGISTER_EVENTFD, UnsafeMutableRawPointer(mutating: fdptr), 1 ) + return result >= 0 ? nil : Errno.current + } + if let result { + throw(RegistrationError(registrationResult: result.rawValue)) } - try handleRegistrationResult(result) } - public mutating func unregisterEventFD() throws { + public mutating func unregisterEventFD() throws(RegistrationError) { let result = io_uring_register( ringDescriptor, IORING_UNREGISTER_EVENTFD, nil, 0 ) - try handleRegistrationResult(result) + if result < 0 { + throw(RegistrationError(registrationResult: result)) + } } - public mutating func registerFileSlots(count: Int) -> RegisteredResources< + public mutating func registerFileSlots(count: Int) throws(RegistrationError) -> RegisteredResources< IORingFileSlot.Resource > { precondition(_registeredFiles == nil) @@ -524,15 +575,19 @@ public struct IORing: ~Copyable { let files = [UInt32](repeating: UInt32.max, count: count) let regResult = files.withUnsafeBufferPointer { bPtr in - io_uring_register( + let result = io_uring_register( self.ringDescriptor, IORING_REGISTER_FILES, UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: count) ) + return result >= 0 ? nil : Errno.current + } + + guard regResult == nil else { + throw RegistrationError(registrationResult: regResult!.rawValue) } - // TODO: error handling _registeredFiles = files return registeredFileSlots } @@ -545,7 +600,7 @@ public struct IORing: ~Copyable { RegisteredResources(resources: _registeredFiles ?? []) } - public mutating func registerBuffers(_ buffers: some Collection) + public mutating func registerBuffers(_ buffers: some Collection) throws(RegistrationError) -> RegisteredResources { precondition(buffers.count < UInt32.max) @@ -553,12 +608,17 @@ public struct IORing: ~Copyable { //TODO: check if io_uring has preconditions it needs for the buffers (e.g. alignment) let iovecs = buffers.map { $0.to_iovec() } let regResult = iovecs.withUnsafeBufferPointer { bPtr in - io_uring_register( + let result = io_uring_register( self.ringDescriptor, IORING_REGISTER_BUFFERS, UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: buffers.count) ) + return result >= 0 ? nil : Errno.current + } + + guard regResult == nil else { + throw RegistrationError(registrationResult: regResult!.rawValue) } // TODO: error handling @@ -566,10 +626,10 @@ public struct IORing: ~Copyable { return registeredBuffers } - public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) + public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(RegistrationError) -> RegisteredResources { - registerBuffers(buffers) + try registerBuffers(buffers) } public struct RegisteredResources: RandomAccessCollection { @@ -596,20 +656,24 @@ public struct IORing: ~Copyable { fatalError("failed to unregister buffers: TODO") } - public func submitPreparedRequests() throws { + public func submitPreparedRequests() throws(OperationError) { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } - public func submitPreparedRequestsAndConsumeCompletions( + public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, IORingError?, Bool) throws -> Void - ) throws { + consumer: (consuming IOCompletion?, OperationError?, Bool) throws(Err) -> Void + ) throws(Err) { //TODO: optimize this to one uring_enter - try submitPreparedRequests() + do { + try submitPreparedRequests() + } catch (let e) { + try consumer(nil, e, true) + } try blockingConsumeCompletions( - minimumCount: minimumCount, - timeout: timeout, + minimumCount: minimumCount, + timeout: timeout, consumer: consumer ) } @@ -641,7 +705,7 @@ public struct IORing: ~Copyable { prepare(linkedRequests: linkedRequests) } - public mutating func submit(linkedRequests: IORequest...) throws { + public mutating func submit(linkedRequests: IORequest...) throws(OperationError) { prepare(linkedRequests: linkedRequests) try submitPreparedRequests() } diff --git a/Sources/System/IORingError.swift b/Sources/System/IORingError.swift deleted file mode 100644 index fbd70bce..00000000 --- a/Sources/System/IORingError.swift +++ /dev/null @@ -1,10 +0,0 @@ -//TODO: make this not an enum -public enum IORingError: Error, Equatable { - case missingRequiredFeatures - case operationCanceled - case unknown(errorCode: Int) - - internal init(completionResult: Int32) { - self = .unknown(errorCode: Int(completionResult)) //TODO, flesh this out - } -} From 2f91217c90d9efc02f4d1bd28a7e2188b774bf77 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 16 Apr 2025 23:44:21 +0000 Subject: [PATCH 067/172] Fix a few things I missed --- Sources/System/IORing.swift | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 3ef92997..e45d48af 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -437,14 +437,18 @@ public struct IORing: ~Copyable { extraArgs: UnsafeMutablePointer? = nil ) throws(OperationError) -> IOCompletion { var result: IOCompletion? = nil - try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { - (completion: consuming IOCompletion?, error, done) in - if let error { - throw error - } - if let completion { - result = consume completion + do { + try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { + (completion: consuming IOCompletion?, error, done) in + if let error { + throw error + } + if let completion { + result = consume completion + } } + } catch (let e) { + throw e as! OperationError //TODO: why is this needed? } return result.take()! } @@ -459,7 +463,7 @@ public struct IORing: ~Copyable { ) var err: OperationError? = nil var result: IOCompletion? = nil - result = try withUnsafePointer(to: &ts) { tsPtr in + result = withUnsafePointer(to: &ts) { tsPtr in var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, @@ -469,7 +473,7 @@ public struct IORing: ~Copyable { do { return try _blockingConsumeOneCompletion(extraArgs: &args) } catch (let e) { - err = e as! OperationError + err = (e as! OperationError) return nil } } @@ -505,7 +509,7 @@ public struct IORing: ~Copyable { minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, consumer: consumer) } catch (let e) { - err = e as! Err //TODO: why is `e` coming in as `any Error`? That seems wrong + err = (e as! Err) //TODO: why is `e` coming in as `any Error`? That seems wrong } } if let err { @@ -692,10 +696,10 @@ public struct IORing: ~Copyable { for req in linkedRequests.dropLast() { var raw = req.makeRawRequest() raw.linkToNextRequest() - _writeRequest( + _ = _writeRequest( raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } - _writeRequest( + _ = _writeRequest( last.makeRawRequest(), ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } From d4ca203c09c7e541d603436bf2f0d78d600f5309 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 17 Apr 2025 00:26:13 +0000 Subject: [PATCH 068/172] Most of feature querying and setup flags --- Sources/System/IORing.swift | 56 ++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index e45d48af..ef63eccb 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -243,8 +243,33 @@ public struct IORing: ~Copyable { var _registeredFiles: [UInt32]? var _registeredBuffers: [iovec]? - public init(queueDepth: UInt32) throws(SetupError) { + @frozen + public struct SetupFlags: OptionSet, RawRepresentable { + public var rawValue: UInt32 + + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + @inlinable public static var pollCompletions: SetupFlags { .init(rawValue: UInt32(1) << 0) } //IORING_SETUP_IOPOLL + @inlinable public static var pollSubmissions: SetupFlags { .init(rawValue: UInt32(1) << 1) } //IORING_SETUP_SQPOLL + //TODO: figure out how to expose IORING_SETUP_SQ_AFF, IORING_SETUP_CQSIZE, IORING_SETUP_ATTACH_WQ + @inlinable public static var clampMaxEntries: SetupFlags { .init(rawValue: UInt32(1) << 4) } //IORING_SETUP_CLAMP + @inlinable public static var startDisabled: SetupFlags { .init(rawValue: UInt32(1) << 6) } //IORING_SETUP_R_DISABLED + @inlinable public static var continueSubmittingOnError: SetupFlags { .init(rawValue: UInt32(1) << 7) } //IORING_SETUP_SUBMIT_ALL + //TODO: do we want to expose IORING_SETUP_COOP_TASKRUN and IORING_SETUP_TASKRUN_FLAG? + //public static var runTasksCooperatively: SetupFlags { .init(rawValue: UInt32(1) << 8) } //IORING_SETUP_COOP_TASKRUN + //TODO: can we even do different size sqe/cqe? It requires a kernel feature, but how do we convince swift to let the types be different sizes? + internal static var use128ByteSQEs: SetupFlags { .init(rawValue: UInt32(1) << 10) } //IORING_SETUP_SQE128 + internal static var use32ByteCQEs: SetupFlags { .init(rawValue: UInt32(1) << 11) } //IORING_SETUP_CQE32 + @inlinable public static var singleSubmissionThread: SetupFlags { .init(rawValue: UInt32(1) << 12) } //IORING_SETUP_SINGLE_ISSUER + @inlinable public static var deferRunningTasks: SetupFlags { .init(rawValue: UInt32(1) << 13) } //IORING_SETUP_DEFER_TASKRUN + //pretty sure we don't want to expose IORING_SETUP_NO_MMAP or IORING_SETUP_REGISTERED_FD_ONLY currently + //TODO: should IORING_SETUP_NO_SQARRAY be the default? do we need to adapt anything to it? + } + + public init(queueDepth: UInt32, flags: SetupFlags = []) throws(SetupError) { var params = io_uring_params() + params.flags = flags.rawValue ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { return io_uring_setup(queueDepth, $0) @@ -714,6 +739,35 @@ public struct IORing: ~Copyable { try submitPreparedRequests() } + @frozen + public struct Features: OptionSet, RawRepresentable { + public let rawValue: UInt32 + + @inlinable public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + //IORING_FEAT_SINGLE_MMAP is handled internally + @inlinable public static var nonDroppingCompletions: Features { .init(rawValue: UInt32(1) << 1) } //IORING_FEAT_NODROP + @inlinable public static var stableSubmissions: Features { .init(rawValue: UInt32(1) << 2) } //IORING_FEAT_SUBMIT_STABLE + @inlinable public static var currentFilePosition: Features { .init(rawValue: UInt32(1) << 3) } //IORING_FEAT_RW_CUR_POS + @inlinable public static var assumingTaskCredentials: Features { .init(rawValue: UInt32(1) << 4) } //IORING_FEAT_CUR_PERSONALITY + @inlinable public static var fastPolling: Features { .init(rawValue: UInt32(1) << 5) } //IORING_FEAT_FAST_POLL + @inlinable public static var epoll32BitFlags: Features { .init(rawValue: UInt32(1) << 6) } //IORING_FEAT_POLL_32BITS + @inlinable public static var pollNonFixedFiles: Features { .init(rawValue: UInt32(1) << 7) } //IORING_FEAT_SQPOLL_NONFIXED + @inlinable public static var extendedArguments: Features { .init(rawValue: UInt32(1) << 8) } //IORING_FEAT_EXT_ARG + @inlinable public static var nativeWorkers: Features { .init(rawValue: UInt32(1) << 9) } //IORING_FEAT_NATIVE_WORKERS + @inlinable public static var resourceTags: Features { .init(rawValue: UInt32(1) << 10) } //IORING_FEAT_RSRC_TAGS + @inlinable public static var allowsSkippingSuccessfulCompletions: Features { .init(rawValue: UInt32(1) << 11) } //IORING_FEAT_CQE_SKIP + @inlinable public static var improvedLinkedFiles: Features { .init(rawValue: UInt32(1) << 12) } //IORING_FEAT_LINKED_FILE + @inlinable public static var registerRegisteredRings: Features { .init(rawValue: UInt32(1) << 13) } //IORING_FEAT_REG_REG_RING + @inlinable public static var minimumTimeout: Features { .init(rawValue: UInt32(1) << 15) } //IORING_FEAT_MIN_TIMEOUT + @inlinable public static var bundledSendReceive: Features { .init(rawValue: UInt32(1) << 14) } //IORING_FEAT_RECVSEND_BUNDLE + } + public static var supportedFeatures: Features { + fatalError("Implement me") + } + deinit { munmap(ringPtr, ringSize) munmap( From 41f967690797dcb68220d7587215216824dac0fe Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 17 Apr 2025 23:31:01 +0000 Subject: [PATCH 069/172] WIP, crashes the compiler --- Sources/System/IORing.swift | 237 ++++++++++++++++++++---------------- 1 file changed, 130 insertions(+), 107 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index ef63eccb..e72c9e89 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -191,11 +191,113 @@ internal func _getSubmissionEntry( return nil } -//TODO: make this not an enum. And maybe split it up? And move it into IORing +private func setUpRing( + queueDepth: UInt32, flags: IORing.SetupFlags, submissionRing: inout SQRing +) throws(IORing.SetupError) -> + (params: io_uring_params, ringDescriptor: Int32, ringPtr: UnsafeMutableRawPointer, ringSize: Int, sqes: UnsafeMutableRawPointer) { + var params = io_uring_params() + params.flags = flags.rawValue + + var err: Errno? = nil + let ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { + let result = io_uring_setup(queueDepth, $0) + if result < 0 { + err = Errno.current + } + return result + } + + if let err { + throw IORing.SetupError(setupResult: err.rawValue) + } + + if params.features & IORING_FEAT_SINGLE_MMAP == 0 + || params.features & IORING_FEAT_NODROP == 0 + { + close(ringDescriptor) + // TODO: error handling + throw .missingRequiredFeatures + } + + let submitRingSize = + params.sq_off.array + + params.sq_entries * UInt32(MemoryLayout.size) + + let completionRingSize = + params.cq_off.cqes + + params.cq_entries * UInt32(MemoryLayout.size) + + let ringSize = Int(max(submitRingSize, completionRingSize)) + + let ringPtr: UnsafeMutableRawPointer! = mmap( + /* addr: */ nil, + /* len: */ ringSize, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_SQ_RING) + ) + + if ringPtr == MAP_FAILED { + perror("mmap") + close(ringDescriptor) + throw .mapFailed + } + + let submissionRing = SQRing( + kernelHead: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.head) + .assumingMemoryBound(to: Atomic.self) + ), + kernelTail: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.tail) + .assumingMemoryBound(to: Atomic.self) + ), + userTail: 0, // no requests yet + ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) + .assumingMemoryBound(to: UInt32.self).pointee, + flags: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.flags) + .assumingMemoryBound(to: Atomic.self) + ), + array: UnsafeMutableBufferPointer( + start: ringPtr.advanced(by: params.sq_off.array) + .assumingMemoryBound(to: UInt32.self), + count: Int( + ringPtr.advanced(by: params.sq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) + ) + ) + + // fill submission ring array with 1:1 map to underlying SQEs + for i in 0...size, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_SQES) + ) + + if sqes == MAP_FAILED { + perror("mmap") + munmap(ringPtr, ringSize) + close(ringDescriptor) + throw .mapFailed + } + + return (params: params, ringDescriptor: ringDescriptor, ringPtr: ringPtr!, ringSize: ringSize, sqes: sqes!) +} public struct IORing: ~Copyable { /// Errors in either submitting operations or receiving operation completions + @frozen public struct OperationError: Error, Hashable { private var errorCode: Int public static var operationCanceled: OperationError { .init(result: ECANCELED) } @@ -205,6 +307,7 @@ public struct IORing: ~Copyable { } } + @frozen public struct SetupError: Error, Hashable { private var errorCode: Int @@ -212,11 +315,14 @@ public struct IORing: ~Copyable { .init(setupResult: -1) /* TODO: numeric value */ } + public static var mapFailed: SetupError { .init(setupResult: -2) } + internal init(setupResult: Int32) { errorCode = Int(setupResult) //TODO, flesh this out } } + @frozen public struct RegistrationError: Error, Hashable { private var errorCode: Int @@ -228,7 +334,7 @@ public struct IORing: ~Copyable { let ringFlags: UInt32 let ringDescriptor: Int32 - @usableFromInline var submissionRing: SQRing + @usableFromInline var submissionRing: SQRing! // FEAT: set this eventually let submissionPolling: Bool = false @@ -240,8 +346,10 @@ public struct IORing: ~Copyable { let ringSize: Int let ringPtr: UnsafeMutableRawPointer - var _registeredFiles: [UInt32]? - var _registeredBuffers: [iovec]? + var _registeredFiles: [UInt32] = [] + var _registeredBuffers: [iovec] = [] + + var features = Features(rawValue: 0) @frozen public struct SetupFlags: OptionSet, RawRepresentable { @@ -267,103 +375,22 @@ public struct IORing: ~Copyable { //TODO: should IORING_SETUP_NO_SQARRAY be the default? do we need to adapt anything to it? } - public init(queueDepth: UInt32, flags: SetupFlags = []) throws(SetupError) { - var params = io_uring_params() - params.flags = flags.rawValue - - ringDescriptor = withUnsafeMutablePointer(to: ¶ms) { - return io_uring_setup(queueDepth, $0) - } - - if params.features & IORING_FEAT_SINGLE_MMAP == 0 - || params.features & IORING_FEAT_NODROP == 0 - { - close(ringDescriptor) - // TODO: error handling - throw .missingRequiredFeatures - } - - if ringDescriptor < 0 { - // TODO: error handling - } - - let submitRingSize = - params.sq_off.array - + params.sq_entries * UInt32(MemoryLayout.size) - - let completionRingSize = - params.cq_off.cqes - + params.cq_entries * UInt32(MemoryLayout.size) - - ringSize = Int(max(submitRingSize, completionRingSize)) - - ringPtr = mmap( - /* addr: */ nil, - /* len: */ ringSize, - /* prot: */ PROT_READ | PROT_WRITE, - /* flags: */ MAP_SHARED | MAP_POPULATE, - /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_SQ_RING) - ) - - if ringPtr == MAP_FAILED { - perror("mmap") - // TODO: error handling - fatalError("mmap failed in ring setup") - } - - let submissionRing = SQRing( - kernelHead: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.head) - .assumingMemoryBound(to: Atomic.self) - ), - kernelTail: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.tail) - .assumingMemoryBound(to: Atomic.self) - ), - userTail: 0, // no requests yet - ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) - .assumingMemoryBound(to: UInt32.self).pointee, - flags: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.flags) - .assumingMemoryBound(to: Atomic.self) - ), - array: UnsafeMutableBufferPointer( - start: ringPtr.advanced(by: params.sq_off.array) - .assumingMemoryBound(to: UInt32.self), - count: Int( - ringPtr.advanced(by: params.sq_off.ring_entries) - .assumingMemoryBound(to: UInt32.self).pointee) - ) - ) - - // fill submission ring array with 1:1 map to underlying SQEs - for i in 0...size, - /* prot: */ PROT_READ | PROT_WRITE, - /* flags: */ MAP_SHARED | MAP_POPULATE, - /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_SQES) - ) - - if sqes == MAP_FAILED { - perror("mmap") - // TODO: error handling - fatalError("sqe mmap failed in ring setup") - } + public init(queueDepth: UInt32, flags: SetupFlags) throws(SetupError) { + let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags, submissionRing: &submissionRing) + // All throws need to be before initializing ivars here to avoid + // "error: conditional initialization or destruction of noncopyable types is not supported; + // this variable must be consistently in an initialized or uninitialized state through every code path" + features = Features(rawValue: params.features) + ringDescriptor = tmpRingDescriptor + ringPtr = tmpRingPtr + ringSize = tmpRingSize submissionQueueEntries = UnsafeMutableBufferPointer( - start: sqes!.assumingMemoryBound(to: io_uring_sqe.self), + start: sqes.assumingMemoryBound(to: io_uring_sqe.self), count: Int(params.sq_entries) ) - let completionRing = CQRing( + completionRing = CQRing( kernelHead: UnsafePointer>( ringPtr.advanced(by: params.cq_off.head) .assumingMemoryBound(to: Atomic.self) @@ -383,10 +410,6 @@ public struct IORing: ~Copyable { .assumingMemoryBound(to: UInt32.self).pointee) ) ) - - self.submissionRing = submissionRing - self.completionRing = completionRing - self.ringFlags = params.flags } @@ -599,7 +622,7 @@ public struct IORing: ~Copyable { public mutating func registerFileSlots(count: Int) throws(RegistrationError) -> RegisteredResources< IORingFileSlot.Resource > { - precondition(_registeredFiles == nil) + precondition(_registeredFiles.isEmpty) precondition(count < UInt32.max) let files = [UInt32](repeating: UInt32.max, count: count) @@ -626,14 +649,14 @@ public struct IORing: ~Copyable { } public var registeredFileSlots: RegisteredResources { - RegisteredResources(resources: _registeredFiles ?? []) + RegisteredResources(resources: _registeredFiles) } public mutating func registerBuffers(_ buffers: some Collection) throws(RegistrationError) -> RegisteredResources { precondition(buffers.count < UInt32.max) - precondition(_registeredBuffers == nil) + precondition(_registeredBuffers.isEmpty) //TODO: check if io_uring has preconditions it needs for the buffers (e.g. alignment) let iovecs = buffers.map { $0.to_iovec() } let regResult = iovecs.withUnsafeBufferPointer { bPtr in @@ -678,7 +701,7 @@ public struct IORing: ~Copyable { } public var registeredBuffers: RegisteredResources { - RegisteredResources(resources: _registeredBuffers ?? []) + RegisteredResources(resources: _registeredBuffers) } public func unregisterBuffers() { @@ -740,7 +763,7 @@ public struct IORing: ~Copyable { } @frozen - public struct Features: OptionSet, RawRepresentable { + public struct Features: OptionSet, RawRepresentable, Hashable { public let rawValue: UInt32 @inlinable public init(rawValue: UInt32) { @@ -764,8 +787,8 @@ public struct IORing: ~Copyable { @inlinable public static var minimumTimeout: Features { .init(rawValue: UInt32(1) << 15) } //IORING_FEAT_MIN_TIMEOUT @inlinable public static var bundledSendReceive: Features { .init(rawValue: UInt32(1) << 14) } //IORING_FEAT_RECVSEND_BUNDLE } - public static var supportedFeatures: Features { - fatalError("Implement me") + public var supportedFeatures: Features { + return features } deinit { From 014f8b78eb3b2acf07f98589661acd8fa2e42bd7 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 17 Apr 2025 23:36:51 +0000 Subject: [PATCH 070/172] Rename userData to context --- Sources/System/IOCompletion.swift | 2 +- Sources/System/IORequest.swift | 148 +++++++++++++++--------------- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index ee81797e..b15f0d45 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -20,7 +20,7 @@ extension IOCompletion { } extension IOCompletion { - public var userData: UInt64 { //TODO: naming? + public var context: UInt64 { get { rawValue.user_data } diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 1eed6edf..c836fd32 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -9,7 +9,7 @@ internal enum IORequestCore { FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, - userData: UInt64 = 0 + context: UInt64 = 0 ) case openatSlot( atDirectory: FileDescriptor, @@ -18,68 +18,68 @@ internal enum IORequestCore { options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, intoSlot: IORingFileSlot, - userData: UInt64 = 0 + context: UInt64 = 0 ) case read( file: FileDescriptor, buffer: IORingBuffer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case readUnregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case readSlot( file: IORingFileSlot, buffer: IORingBuffer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case readUnregisteredSlot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case write( file: FileDescriptor, buffer: IORingBuffer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case writeUnregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case writeSlot( file: IORingFileSlot, buffer: IORingBuffer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case writeUnregisteredSlot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) case close( FileDescriptor, - userData: UInt64 = 0 + context: UInt64 = 0 ) case closeSlot( IORingFileSlot, - userData: UInt64 = 0 + context: UInt64 = 0 ) case unlinkAt( atDirectory: FileDescriptor, path: FilePath, - userData: UInt64 = 0 + context: UInt64 = 0 ) } @@ -88,14 +88,14 @@ internal func makeRawRequest_readWrite_registered( file: FileDescriptor, buffer: IORingBuffer, offset: UInt64, - userData: UInt64 = 0, + context: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.fileDescriptor = file request.buffer = buffer.unsafeBuffer request.rawValue.buf_index = UInt16(exactly: buffer.index)! request.offset = offset - request.rawValue.user_data = userData + request.rawValue.user_data = context return request } @@ -104,7 +104,7 @@ internal func makeRawRequest_readWrite_registered_slot( file: IORingFileSlot, buffer: IORingBuffer, offset: UInt64, - userData: UInt64 = 0, + context: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.rawValue.fd = Int32(exactly: file.index)! @@ -112,7 +112,7 @@ internal func makeRawRequest_readWrite_registered_slot( request.buffer = buffer.unsafeBuffer request.rawValue.buf_index = UInt16(exactly: buffer.index)! request.offset = offset - request.rawValue.user_data = userData + request.rawValue.user_data = context return request } @@ -121,13 +121,13 @@ internal func makeRawRequest_readWrite_unregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, offset: UInt64, - userData: UInt64 = 0, + context: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.fileDescriptor = file request.buffer = buffer request.offset = offset - request.rawValue.user_data = userData + request.rawValue.user_data = context return request } @@ -136,14 +136,14 @@ internal func makeRawRequest_readWrite_unregistered_slot( file: IORingFileSlot, buffer: UnsafeMutableRawBufferPointer, offset: UInt64, - userData: UInt64 = 0, + context: UInt64 = 0, request: consuming RawIORequest ) -> RawIORequest { request.rawValue.fd = Int32(exactly: file.index)! request.flags = .fixedFile request.buffer = buffer request.offset = offset - request.rawValue.user_data = userData + request.rawValue.user_data = context return request } @@ -156,7 +156,7 @@ public struct IORequest { } extension IORequest { - public static func nop(userData: UInt64 = 0) -> IORequest { + public static func nop(context: UInt64 = 0) -> IORequest { IORequest(core: .nop) } @@ -164,93 +164,93 @@ extension IORequest { _ file: IORingFileSlot, into buffer: IORingBuffer, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset, userData: userData)) + IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset, context: context)) } public static func reading( _ file: FileDescriptor, into buffer: IORingBuffer, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .read(file: file, buffer: buffer, offset: offset, userData: userData)) + IORequest(core: .read(file: file, buffer: buffer, offset: offset, context: context)) } public static func reading( _ file: IORingFileSlot, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { IORequest( core: .readUnregisteredSlot( - file: file, buffer: buffer, offset: offset, userData: userData)) + file: file, buffer: buffer, offset: offset, context: context)) } public static func reading( _ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { IORequest( - core: .readUnregistered(file: file, buffer: buffer, offset: offset, userData: userData)) + core: .readUnregistered(file: file, buffer: buffer, offset: offset, context: context)) } public static func writing( _ buffer: IORingBuffer, into file: IORingFileSlot, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset, userData: userData)) + IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset, context: context)) } public static func writing( _ buffer: IORingBuffer, into file: FileDescriptor, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .write(file: file, buffer: buffer, offset: offset, userData: userData)) + IORequest(core: .write(file: file, buffer: buffer, offset: offset, context: context)) } public static func writing( _ buffer: UnsafeMutableRawBufferPointer, into file: IORingFileSlot, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { IORequest( core: .writeUnregisteredSlot( - file: file, buffer: buffer, offset: offset, userData: userData)) + file: file, buffer: buffer, offset: offset, context: context)) } public static func writing( _ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, at offset: UInt64 = 0, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { IORequest( - core: .writeUnregistered(file: file, buffer: buffer, offset: offset, userData: userData) + core: .writeUnregistered(file: file, buffer: buffer, offset: offset, context: context) ) } public static func closing( _ file: FileDescriptor, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .close(file, userData: userData)) + IORequest(core: .close(file, context: context)) } public static func closing( _ file: IORingFileSlot, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .closeSlot(file, userData: userData)) + IORequest(core: .closeSlot(file, context: context)) } public static func opening( @@ -260,12 +260,12 @@ extension IORequest { mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { IORequest( core: .openatSlot( atDirectory: directory, path: path, mode, options: options, - permissions: permissions, intoSlot: slot, userData: userData)) + permissions: permissions, intoSlot: slot, context: context)) } public static func opening( @@ -274,21 +274,21 @@ extension IORequest { mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { IORequest( core: .openat( atDirectory: directory, path: path, mode, options: options, - permissions: permissions, userData: userData + permissions: permissions, context: context )) } public static func unlinking( _ path: FilePath, in directory: FileDescriptor, - userData: UInt64 = 0 + context: UInt64 = 0 ) -> IORequest { - IORequest(core: .unlinkAt(atDirectory: directory, path: path, userData: userData)) + IORequest(core: .unlinkAt(atDirectory: directory, path: path, context: context)) } @inline(__always) @@ -299,7 +299,7 @@ extension IORequest { request.operation = .nop case .openatSlot( let atDirectory, let path, let mode, let options, let permissions, let fileSlot, - let userData): + let context): // TODO: use rawValue less request.operation = .openAt request.fileDescriptor = atDirectory @@ -312,9 +312,9 @@ extension IORequest { request.rawValue.len = permissions?.rawValue ?? 0 request.rawValue.file_index = UInt32(fileSlot.index + 1) request.path = path - request.rawValue.user_data = userData + request.rawValue.user_data = context case .openat( - let atDirectory, let path, let mode, let options, let permissions, let userData): + let atDirectory, let path, let mode, let options, let permissions, let context): request.operation = .openAt request.fileDescriptor = atDirectory request.rawValue.addr = UInt64( @@ -325,48 +325,48 @@ extension IORequest { request.rawValue.open_flags = UInt32(bitPattern: options.rawValue | mode.rawValue) request.rawValue.len = permissions?.rawValue ?? 0 request.path = path - request.rawValue.user_data = userData - case .write(let file, let buffer, let offset, let userData): + request.rawValue.user_data = context + case .write(let file, let buffer, let offset, let context): request.operation = .writeFixed return makeRawRequest_readWrite_registered( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .writeSlot(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .writeSlot(let file, let buffer, let offset, let context): request.operation = .writeFixed return makeRawRequest_readWrite_registered_slot( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .writeUnregistered(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .writeUnregistered(let file, let buffer, let offset, let context): request.operation = .write return makeRawRequest_readWrite_unregistered( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .writeUnregisteredSlot(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .writeUnregisteredSlot(let file, let buffer, let offset, let context): request.operation = .write return makeRawRequest_readWrite_unregistered_slot( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .read(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .read(let file, let buffer, let offset, let context): request.operation = .readFixed return makeRawRequest_readWrite_registered( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .readSlot(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .readSlot(let file, let buffer, let offset, let context): request.operation = .readFixed return makeRawRequest_readWrite_registered_slot( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .readUnregistered(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .readUnregistered(let file, let buffer, let offset, let context): request.operation = .read return makeRawRequest_readWrite_unregistered( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .readUnregisteredSlot(let file, let buffer, let offset, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .readUnregisteredSlot(let file, let buffer, let offset, let context): request.operation = .read return makeRawRequest_readWrite_unregistered_slot( - file: file, buffer: buffer, offset: offset, userData: userData, request: request) - case .close(let file, let userData): + file: file, buffer: buffer, offset: offset, context: context, request: request) + case .close(let file, let context): request.operation = .close request.fileDescriptor = file - request.rawValue.user_data = userData - case .closeSlot(let file, let userData): + request.rawValue.user_data = context + case .closeSlot(let file, let context): request.operation = .close request.rawValue.file_index = UInt32(file.index + 1) - request.rawValue.user_data = userData - case .unlinkAt(let atDirectory, let path, let userData): + request.rawValue.user_data = context + case .unlinkAt(let atDirectory, let path, let context): request.operation = .unlinkAt request.fileDescriptor = atDirectory request.rawValue.addr = UInt64( @@ -376,7 +376,7 @@ extension IORequest { }) ) request.path = path - request.rawValue.user_data = userData + request.rawValue.user_data = context } return request } From 261ca250fe3d097247b903e141396dc28ee76c37 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 18 Apr 2025 22:20:09 +0000 Subject: [PATCH 071/172] Centralize on Errno for errors, and work around lifetime compiler crash. There's a second compiler crash still to work around though. --- Sources/System/IORing.swift | 159 ++++++++++++------------------------ 1 file changed, 52 insertions(+), 107 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index e72c9e89..6c994b94 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -118,7 +118,7 @@ internal func _enter( numEvents: UInt32, minCompletions: UInt32, flags: UInt32 -) throws(IORing.OperationError) -> Int32 { +) throws(Errno) -> Int32 { // Ring always needs enter right now; // TODO: support SQPOLL here while true { @@ -133,14 +133,14 @@ internal func _enter( //TODO: should we wait a bit on AGAIN? continue } else if ret < 0 { - throw(IORing.OperationError(result: -ret)) + throw(Errno(rawValue: -ret)) } else { return ret } } } -internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws(IORing.OperationError) { +internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws(Errno) { let flushedEvents = _flushQueue(ring: ring) _ = try _enter( ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) @@ -193,7 +193,7 @@ internal func _getSubmissionEntry( private func setUpRing( queueDepth: UInt32, flags: IORing.SetupFlags, submissionRing: inout SQRing -) throws(IORing.SetupError) -> +) throws(Errno) -> (params: io_uring_params, ringDescriptor: Int32, ringPtr: UnsafeMutableRawPointer, ringSize: Int, sqes: UnsafeMutableRawPointer) { var params = io_uring_params() params.flags = flags.rawValue @@ -208,7 +208,7 @@ private func setUpRing( } if let err { - throw IORing.SetupError(setupResult: err.rawValue) + throw err } if params.features & IORING_FEAT_SINGLE_MMAP == 0 @@ -216,7 +216,7 @@ private func setUpRing( { close(ringDescriptor) // TODO: error handling - throw .missingRequiredFeatures + throw Errno.invalidArgument } let submitRingSize = @@ -239,9 +239,10 @@ private func setUpRing( ) if ringPtr == MAP_FAILED { + let errno = Errno.current perror("mmap") close(ringDescriptor) - throw .mapFailed + throw errno } let submissionRing = SQRing( @@ -285,52 +286,17 @@ private func setUpRing( ) if sqes == MAP_FAILED { + let errno = Errno.current perror("mmap") munmap(ringPtr, ringSize) close(ringDescriptor) - throw .mapFailed + throw errno } return (params: params, ringDescriptor: ringDescriptor, ringPtr: ringPtr!, ringSize: ringSize, sqes: sqes!) } public struct IORing: ~Copyable { - - /// Errors in either submitting operations or receiving operation completions - @frozen - public struct OperationError: Error, Hashable { - private var errorCode: Int - public static var operationCanceled: OperationError { .init(result: ECANCELED) } - - internal init(result: Int32) { - errorCode = Int(result) //TODO, flesh this out - } - } - - @frozen - public struct SetupError: Error, Hashable { - private var errorCode: Int - - public static var missingRequiredFeatures: SetupError { - .init(setupResult: -1) /* TODO: numeric value */ - } - - public static var mapFailed: SetupError { .init(setupResult: -2) } - - internal init(setupResult: Int32) { - errorCode = Int(setupResult) //TODO, flesh this out - } - } - - @frozen - public struct RegistrationError: Error, Hashable { - private var errorCode: Int - - internal init(registrationResult: Int32) { - errorCode = Int(registrationResult) //TODO, flesh this out - } - } - let ringFlags: UInt32 let ringDescriptor: Int32 @@ -346,8 +312,8 @@ public struct IORing: ~Copyable { let ringSize: Int let ringPtr: UnsafeMutableRawPointer - var _registeredFiles: [UInt32] = [] - var _registeredBuffers: [iovec] = [] + var _registeredFiles: [UInt32] + var _registeredBuffers: [iovec] var features = Features(rawValue: 0) @@ -375,7 +341,7 @@ public struct IORing: ~Copyable { //TODO: should IORING_SETUP_NO_SQARRAY be the default? do we need to adapt anything to it? } - public init(queueDepth: UInt32, flags: SetupFlags) throws(SetupError) { + public init(queueDepth: UInt32, flags: SetupFlags) throws(Errno) { let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags, submissionRing: &submissionRing) // All throws need to be before initializing ivars here to avoid // "error: conditional initialization or destruction of noncopyable types is not supported; @@ -384,6 +350,8 @@ public struct IORing: ~Copyable { ringDescriptor = tmpRingDescriptor ringPtr = tmpRingPtr ringSize = tmpRingSize + _registeredFiles = [] + _registeredBuffers = [] submissionQueueEntries = UnsafeMutableBufferPointer( start: sqes.assumingMemoryBound(to: io_uring_sqe.self), @@ -417,13 +385,13 @@ public struct IORing: ~Copyable { minimumCount: UInt32, maximumCount: UInt32, extraArgs: UnsafeMutablePointer? = nil, - consumer: (consuming IOCompletion?, OperationError?, Bool) throws(Err) -> Void + consumer: (consuming IOCompletion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 if completion.result < 0 { - try consumer(nil, OperationError(result: completion.result), false) + try consumer(nil, Errno(rawValue: -completion.result), false) } else { try consumer(completion, nil, false) } @@ -469,7 +437,7 @@ public struct IORing: ~Copyable { while let completion = _tryConsumeCompletion(ring: completionRing) { count += 1 if completion.result < 0 { - try consumer(nil, OperationError(result: completion.result), false) + try consumer(nil, Errno(rawValue: -completion.result), false) } else { try consumer(completion, nil, false) } @@ -483,52 +451,37 @@ public struct IORing: ~Copyable { internal func _blockingConsumeOneCompletion( extraArgs: UnsafeMutablePointer? = nil - ) throws(OperationError) -> IOCompletion { + ) throws(Errno) -> IOCompletion { var result: IOCompletion? = nil - do { - try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { - (completion: consuming IOCompletion?, error, done) in - if let error { - throw error - } - if let completion { - result = consume completion - } + try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { + (completion: consuming IOCompletion?, error, done) throws(Errno) in + if let error { + throw error + } + if let completion { + result = consume completion } - } catch (let e) { - throw e as! OperationError //TODO: why is this needed? } return result.take()! } public func blockingConsumeCompletion( timeout: Duration? = nil - ) throws(OperationError) -> IOCompletion { + ) throws(Errno) -> IOCompletion { if let timeout { var ts = __kernel_timespec( tv_sec: timeout.components.seconds, tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) - var err: OperationError? = nil - var result: IOCompletion? = nil - result = withUnsafePointer(to: &ts) { tsPtr in + return try withUnsafePointer(to: &ts) { (tsPtr) throws(Errno) -> IOCompletion in var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, pad: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) - do { - return try _blockingConsumeOneCompletion(extraArgs: &args) - } catch (let e) { - err = (e as! OperationError) - return nil - } - } - guard let result else { - throw(err!) + return try _blockingConsumeOneCompletion(extraArgs: &args) } - return result } else { return try _blockingConsumeOneCompletion() } @@ -537,31 +490,23 @@ public struct IORing: ~Copyable { public func blockingConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, OperationError?, Bool) throws(Err) -> Void + consumer: (consuming IOCompletion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { if let timeout { var ts = __kernel_timespec( tv_sec: timeout.components.seconds, tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) - var err: Err? = nil - withUnsafePointer(to: &ts) { tsPtr in + try withUnsafePointer(to: &ts) { (tsPtr) throws(Err) in var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, pad: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) - do { - try _blockingConsumeCompletionGuts( - minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, - consumer: consumer) - } catch (let e) { - err = (e as! Err) //TODO: why is `e` coming in as `any Error`? That seems wrong - } - } - if let err { - throw(err) + try _blockingConsumeCompletionGuts( + minimumCount: minimumCount, maximumCount: UInt32.max, extraArgs: &args, + consumer: consumer) } } else { try _blockingConsumeCompletionGuts( @@ -591,7 +536,7 @@ public struct IORing: ~Copyable { return nil } - public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(RegistrationError) { + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) { var rawfd = descriptor.rawValue let result = withUnsafePointer(to: &rawfd) { fdptr in let result = io_uring_register( @@ -600,14 +545,14 @@ public struct IORing: ~Copyable { UnsafeMutableRawPointer(mutating: fdptr), 1 ) - return result >= 0 ? nil : Errno.current + return result >= 0 ? nil : Errno(rawValue: -result) } if let result { - throw(RegistrationError(registrationResult: result.rawValue)) + throw result } } - public mutating func unregisterEventFD() throws(RegistrationError) { + public mutating func unregisterEventFD() throws(Errno) { let result = io_uring_register( ringDescriptor, IORING_UNREGISTER_EVENTFD, @@ -615,11 +560,11 @@ public struct IORing: ~Copyable { 0 ) if result < 0 { - throw(RegistrationError(registrationResult: result)) + throw Errno(rawValue: -result) } } - public mutating func registerFileSlots(count: Int) throws(RegistrationError) -> RegisteredResources< + public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources< IORingFileSlot.Resource > { precondition(_registeredFiles.isEmpty) @@ -633,11 +578,11 @@ public struct IORing: ~Copyable { UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: count) ) - return result >= 0 ? nil : Errno.current + return result >= 0 ? nil : Errno(rawValue: -result) } - - guard regResult == nil else { - throw RegistrationError(registrationResult: regResult!.rawValue) + + if let regResult { + throw regResult } _registeredFiles = files @@ -652,7 +597,7 @@ public struct IORing: ~Copyable { RegisteredResources(resources: _registeredFiles) } - public mutating func registerBuffers(_ buffers: some Collection) throws(RegistrationError) + public mutating func registerBuffers(_ buffers: some Collection) throws(Errno) -> RegisteredResources { precondition(buffers.count < UInt32.max) @@ -666,11 +611,11 @@ public struct IORing: ~Copyable { UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: buffers.count) ) - return result >= 0 ? nil : Errno.current + return result >= 0 ? nil : Errno(rawValue: -result) } - guard regResult == nil else { - throw RegistrationError(registrationResult: regResult!.rawValue) + if let regResult { + throw regResult } // TODO: error handling @@ -678,7 +623,7 @@ public struct IORing: ~Copyable { return registeredBuffers } - public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(RegistrationError) + public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(Errno) -> RegisteredResources { try registerBuffers(buffers) @@ -708,14 +653,14 @@ public struct IORing: ~Copyable { fatalError("failed to unregister buffers: TODO") } - public func submitPreparedRequests() throws(OperationError) { + public func submitPreparedRequests() throws(Errno) { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, OperationError?, Bool) throws(Err) -> Void + consumer: (consuming IOCompletion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { //TODO: optimize this to one uring_enter do { @@ -757,7 +702,7 @@ public struct IORing: ~Copyable { prepare(linkedRequests: linkedRequests) } - public mutating func submit(linkedRequests: IORequest...) throws(OperationError) { + public mutating func submit(linkedRequests: IORequest...) throws(Errno) { prepare(linkedRequests: linkedRequests) try submitPreparedRequests() } From c029c1bfbd0da1f8672229dc80f39626617c1abb Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 18 Apr 2025 15:22:17 -0700 Subject: [PATCH 072/172] Proposal updates and a minor change to setup flags --- NNNN-swift-system-io-uring.md | 59 ++++++++++++++++++----------------- Sources/System/IORing.swift | 2 +- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md index 5c08588b..0d8cf8ec 100644 --- a/NNNN-swift-system-io-uring.md +++ b/NNNN-swift-system-io-uring.md @@ -79,8 +79,6 @@ Since neither polling nor synchronously waiting is optimal in many cases, `IORin Unfortunately the underlying kernel API makes it relatively difficult to determine which `IORequest` led to a given `IOCompletion`, so it's expected that users will need to create this association themselves via the context parameter. -`IORingError` represents failure of an operation. - `IORing.Features` describes the supported features of the underlying kernel `IORing` implementation, which can be used to provide graceful reduction in functionality when running on older systems. ## Detailed design @@ -97,10 +95,22 @@ extension IORingBuffer { // IORing is intentionally not Sendable, to avoid internal locking overhead public struct IORing: ~Copyable { - public init(queueDepth: UInt32) throws(IORingError) + public init(queueDepth: UInt32, flags: IORing.SetupFlags) throws(Errno) + + public struct SetupFlags: OptionSet, RawRepresentable, Hashable { + public var rawValue: UInt32 + public init(rawValue: UInt32) + public static var pollCompletions: SetupFlags //IORING_SETUP_IOPOLL + public static var pollSubmissions: SetupFlags //IORING_SETUP_SQPOLL + public static var clampMaxEntries: SetupFlags //IORING_SETUP_CLAMP + public static var startDisabled: SetupFlags //IORING_SETUP_R_DISABLED + public static var continueSubmittingOnError: SetupFlags //IORING_SETUP_SUBMIT_ALL + public static var singleSubmissionThread: SetupFlags //IORING_SETUP_SINGLE_ISSUER + public static var deferRunningTasks: SetupFlags //IORING_SETUP_DEFER_TASKRUN + } - public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(IORingError) - public mutating func unregisterEventFD(_ descriptor: FileDescriptor) throws(IORingError) + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) + public mutating func unregisterEventFD(_ descriptor: FileDescriptor) throws(Errno) // An IORing.RegisteredResources is a view into the buffers or files registered with the ring, if any public struct RegisteredResources: RandomAccessCollection { @@ -108,7 +118,7 @@ public struct IORing: ~Copyable { public subscript(position: UInt16) -> IOResource // This is useful because io_uring likes to use UInt16s as indexes } - public mutating func registerFileSlots(count: Int) throws(IORingError) -> RegisteredResources + public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources public func unregisterFiles() @@ -116,11 +126,11 @@ public struct IORing: ~Copyable { public mutating func registerBuffers( _ buffers: some Collection - ) throws(IORingError) -> RegisteredResources + ) throws(Errno) -> RegisteredResources public mutating func registerBuffers( _ buffers: UnsafeMutableRawBufferPointer... - ) throws(IORingError) -> RegisteredResources + ) throws(Errno) -> RegisteredResources public func unregisterBuffers() @@ -129,32 +139,32 @@ public struct IORing: ~Copyable { public func prepare(requests: IORequest...) public func prepare(linkedRequests: IORequest...) - public func submitPreparedRequests(timeout: Duration? = nil) throws(IORingError) - public func submit(requests: IORequest..., timeout: Duration? = nil) throws(IORingError) - public func submit(linkedRequests: IORequest..., timeout: Duration? = nil) throws(IORingError) + public func submitPreparedRequests(timeout: Duration? = nil) throws(Errno) + public func submit(requests: IORequest..., timeout: Duration? = nil) throws(Errno) + public func submit(linkedRequests: IORequest..., timeout: Duration? = nil) throws(Errno) - public func submitPreparedRequests() throws(IORingError) - public func submitPreparedRequestsAndWait(timeout: Duration? = nil) throws(IORingError) + public func submitPreparedRequests() throws(Errno) + public func submitPreparedRequestsAndWait(timeout: Duration? = nil) throws(Errno) public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, IORingError?, Bool) throws(E) -> Void + consumer: (consuming IOCompletion?, Errno?, Bool) throws(E) -> Void ) throws(E) public func blockingConsumeCompletion( timeout: Duration? = nil - ) throws(IORingError) -> IOCompletion + ) throws(Errno) -> IOCompletion public func blockingConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, IORingError?, Bool) throws(E) -> Void + consumer: (consuming IOCompletion?, Errno?, Bool) throws(E) -> Void ) throws(E) public func tryConsumeCompletion() -> IOCompletion? - public struct Features: OptionSet { + public struct Features: OptionSet, RawRepresentable, Hashable { let rawValue: UInt32 public init(rawValue: UInt32) @@ -176,7 +186,7 @@ public struct IORing: ~Copyable { public static let minimumTimeout: Bool //IORING_FEAT_MIN_TIMEOUT public static let bundledSendReceive: Bool //IORING_FEAT_RECVSEND_BUNDLE } - public static var supportedFeatures: Features + public var supportedFeatures: Features } public struct IORequest: ~Copyable { @@ -329,19 +339,10 @@ public struct IOCompletion { public var result: Int32 - public var error: IORingError? // Convenience wrapper over `result` + public var error: Errno? // Convenience wrapper over `result` public var flags: Flags } - -public struct IORingError: Error, Equatable { - static var missingRequiredFeatures: IORingError - static var operationCanceled: IORingError - static var timedOut: IORingError - static var resourceRegistrationFailed: IORingError - // Other error values to be filled out as the set of supported operations expands in the future - static var unknown: IORingError(errorCode: Int) -} ``` @@ -463,7 +464,7 @@ func submitLinkedRequestsAndWait( * We could multiplex all IO onto a single actor as `AsyncBytes` currently does, but this has a number of downsides that make it entirely unsuitable to server usage. Most notably, it eliminates IO parallelism entirely. * Using POSIX AIO instead of or as well as io_uring would greatly increase our ability to support older kernels and other Unix systems, but it has well-documented performance and usability issues that have prevented its adoption elsewhere, and apply just as much to Swift. * Earlier versions of this proposal had higher level "managed" abstractions over IORing. These have been removed due to lack of interest from clients, but could be added back later if needed. -* I considered making any or all of `IORingError`, `IOCompletion`, and `IORequest` nested struct declarations inside `IORing`. The main reason I haven't done so is I was a little concerned about the ambiguity of having a type called `Error`. I'd be particularly interested in feedback on this choice. +* I considered having dedicated error types for IORing, but eventually decided throwing Errno was more consistent with other platform APIs * IOResource was originally a class in an attempt to manage the lifetime of the resource via language features. Changing to the current model of it being a copyable struct didn't make the lifetime management any less safe (the IORing still owns the actual resource), and reduces overhead. In the future it would be neat if we could express IOResources as being borrowed from the IORing so they can't be used after its lifetime. ## Acknowledgments diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 6c994b94..81efe128 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -318,7 +318,7 @@ public struct IORing: ~Copyable { var features = Features(rawValue: 0) @frozen - public struct SetupFlags: OptionSet, RawRepresentable { + public struct SetupFlags: OptionSet, RawRepresentable, Hashable { public var rawValue: UInt32 @inlinable public init(rawValue: UInt32) { From 05eac87c08b651d93c644fade578fea4bf13f1cd Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 18 Apr 2025 23:37:50 +0000 Subject: [PATCH 073/172] It compiles again! --- Sources/CSystem/include/io_uring.h | 8 ++++---- Sources/System/IORing.swift | 7 ++++++- Tests/SystemTests/IORingTests.swift | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index 5cab757c..75a07e57 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -34,25 +34,25 @@ # endif #endif -int io_uring_register(int fd, unsigned int opcode, void *arg, +static inline int io_uring_register(int fd, unsigned int opcode, void *arg, unsigned int nr_args) { return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args); } -int io_uring_setup(unsigned int entries, struct io_uring_params *p) +static inline int io_uring_setup(unsigned int entries, struct io_uring_params *p) { return syscall(__NR_io_uring_setup, entries, p); } -int io_uring_enter2(int fd, unsigned int to_submit, unsigned int min_complete, +static inline int io_uring_enter2(int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, void *args, size_t sz) { return syscall(__NR_io_uring_enter, fd, to_submit, min_complete, flags, args, _NSIG / 8); } -int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, +static inline int io_uring_enter(int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig) { return io_uring_enter2(fd, to_submit, min_complete, flags, sig, _NSIG / 8); diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 81efe128..9b45f564 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -654,7 +654,12 @@ public struct IORing: ~Copyable { } public func submitPreparedRequests() throws(Errno) { - try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) + switch submissionRing { + case .some(let submissionRing): + try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) + case .none: + fatalError() + } } public func submitPreparedRequestsAndConsumeCompletions( diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 306516be..57437e4d 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -8,11 +8,11 @@ import System final class IORingTests: XCTestCase { func testInit() throws { - _ = try IORing(queueDepth: 32) + _ = try IORing(queueDepth: 32, flags: []) } func testNop() throws { - var ring = try IORing(queueDepth: 32) + var ring = try IORing(queueDepth: 32, flags: []) try ring.submit(linkedRequests: IORequest.nop()) let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) From 28ce8353633af7ba7b3e78f9405693bb79bdec4d Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Fri, 18 Apr 2025 21:18:59 -0700 Subject: [PATCH 074/172] Enable Windows CI FileOperationsTestWindows.testPermissions fails on Windows in a container environment with Hyper-V isolated containers; see https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/persistent-storage#permissions for some potentially-relevant information. Skip it if we detect we're in a container. Closes #223 --- .github/workflows/pull_request.yml | 2 -- Tests/SystemTests/FileOperationsTestWindows.swift | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6ab954b5..d543be29 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,8 +10,6 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_exclude_swift_versions: '[{"swift_version": "5.8"}]' - # https://github.com/apple/swift-system/issues/223 - enable_windows_checks: false soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main diff --git a/Tests/SystemTests/FileOperationsTestWindows.swift b/Tests/SystemTests/FileOperationsTestWindows.swift index 82030516..7b87b354 100644 --- a/Tests/SystemTests/FileOperationsTestWindows.swift +++ b/Tests/SystemTests/FileOperationsTestWindows.swift @@ -174,6 +174,9 @@ final class FileOperationsTestWindows: XCTestCase { /// Test that the umask works properly func testUmask() throws { + // See https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/persistent-storage#permissions + try XCTSkipIf(NSUserName() == "ContainerAdministrator", "containers use a different permission model") + // Default mask should be 0o022 XCTAssertEqual(FilePermissions.creationMask, [.groupWrite, .otherWrite]) @@ -205,6 +208,9 @@ final class FileOperationsTestWindows: XCTestCase { /// Test that setting permissions on a file works as expected func testPermissions() throws { + // See https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/persistent-storage#permissions + try XCTSkipIf(NSUserName() == "ContainerAdministrator", "containers use a different permission model") + try FilePermissions.withCreationMask([]) { try withTemporaryFilePath(basename: "testPermissions") { path in let tests = [ From 7798f0c9577319dab7fe5080c9ed4bc4e856c785 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 23:18:33 +0000 Subject: [PATCH 075/172] Add cancellation support and drop gerunds --- Sources/System/IORequest.swift | 116 ++++++++++++++++++++++--- Sources/System/RawIORequest.swift | 11 +++ Tests/SystemTests/IORequestTests.swift | 2 +- 3 files changed, 115 insertions(+), 14 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index c836fd32..df505210 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -81,6 +81,18 @@ internal enum IORequestCore { path: FilePath, context: UInt64 = 0 ) + case cancel( + flags: UInt32, + targetContext: UInt64 + ) + case cancelFD( + flags: UInt32, + targetFD: FileDescriptor + ) + case cancelFDSlot( + flags: UInt32, + target: IORingFileSlot + ) } @inline(__always) @@ -160,7 +172,7 @@ extension IORequest { IORequest(core: .nop) } - public static func reading( + public static func read( _ file: IORingFileSlot, into buffer: IORingBuffer, at offset: UInt64 = 0, @@ -169,7 +181,7 @@ extension IORequest { IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset, context: context)) } - public static func reading( + public static func read( _ file: FileDescriptor, into buffer: IORingBuffer, at offset: UInt64 = 0, @@ -178,7 +190,7 @@ extension IORequest { IORequest(core: .read(file: file, buffer: buffer, offset: offset, context: context)) } - public static func reading( + public static func read( _ file: IORingFileSlot, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, @@ -189,7 +201,7 @@ extension IORequest { file: file, buffer: buffer, offset: offset, context: context)) } - public static func reading( + public static func read( _ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, @@ -199,7 +211,7 @@ extension IORequest { core: .readUnregistered(file: file, buffer: buffer, offset: offset, context: context)) } - public static func writing( + public static func write( _ buffer: IORingBuffer, into file: IORingFileSlot, at offset: UInt64 = 0, @@ -208,7 +220,7 @@ extension IORequest { IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset, context: context)) } - public static func writing( + public static func write( _ buffer: IORingBuffer, into file: FileDescriptor, at offset: UInt64 = 0, @@ -217,7 +229,7 @@ extension IORequest { IORequest(core: .write(file: file, buffer: buffer, offset: offset, context: context)) } - public static func writing( + public static func write( _ buffer: UnsafeMutableRawBufferPointer, into file: IORingFileSlot, at offset: UInt64 = 0, @@ -228,7 +240,7 @@ extension IORequest { file: file, buffer: buffer, offset: offset, context: context)) } - public static func writing( + public static func write( _ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, at offset: UInt64 = 0, @@ -239,21 +251,21 @@ extension IORequest { ) } - public static func closing( + public static func close( _ file: FileDescriptor, context: UInt64 = 0 ) -> IORequest { IORequest(core: .close(file, context: context)) } - public static func closing( + public static func close( _ file: IORingFileSlot, context: UInt64 = 0 ) -> IORequest { IORequest(core: .closeSlot(file, context: context)) } - public static func opening( + public static func open( _ path: FilePath, in directory: FileDescriptor, into slot: IORingFileSlot, @@ -268,7 +280,7 @@ extension IORequest { permissions: permissions, intoSlot: slot, context: context)) } - public static func opening( + public static func open( _ path: FilePath, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, @@ -283,7 +295,7 @@ extension IORequest { )) } - public static func unlinking( + public static func unlink( _ path: FilePath, in directory: FileDescriptor, context: UInt64 = 0 @@ -291,6 +303,71 @@ extension IORequest { IORequest(core: .unlinkAt(atDirectory: directory, path: path, context: context)) } + // Cancel + + /* + * ASYNC_CANCEL flags. + * + * IORING_ASYNC_CANCEL_ALL Cancel all requests that match the given key + * IORING_ASYNC_CANCEL_FD Key off 'fd' for cancelation 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 + */ + //TODO: why aren't these showing up from the header import? + private static var IORING_ASYNC_CANCEL_ALL: UInt32 { (1 as UInt32) << 0 } + private static var IORING_ASYNC_CANCEL_FD: UInt32 { (1 as UInt32) << 1 } + private static var IORING_ASYNC_CANCEL_ANY: UInt32 { (1 as UInt32) << 2 } + private static var IORING_ASYNC_CANCEL_FD_FIXED: UInt32 { (1 as UInt32) << 3 } + private static var IORING_ASYNC_CANCEL_USERDATA: UInt32 { (1 as UInt32) << 4 } + private static var IORING_ASYNC_CANCEL_OP: UInt32 { (1 as UInt32) << 5 } + + + public enum CancellationMatch { + case all + case first + } + + public static func cancel( + _ matchAll: CancellationMatch, + matchingContext: UInt64, + ) -> IORequest { + switch matchAll { + case .all: + IORequest(core: .cancel(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + case .first: + IORequest(core: .cancel(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + } + } + + public static func cancel( + _ matchAll: CancellationMatch, + matchingFileDescriptor: FileDescriptor, + ) -> IORequest { + switch matchAll { + case .all: + IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD, targetFD: matchingFileDescriptor)) + case .first: + IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD, targetFD: matchingFileDescriptor)) + } + } + + public static func cancel( + _ matchAll: CancellationMatch, + matchingFile: IORingFileSlot, + ) -> IORequest { + switch matchAll { + case .all: + IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD_FIXED, target: matchingFile)) + case .first: + IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD_FIXED, target: matchingFile)) + } + } + + //TODO: add support for CANCEL_OP + @inline(__always) public consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() @@ -377,7 +454,20 @@ extension IORequest { ) request.path = path request.rawValue.user_data = context + case .cancel(let flags, let targetContext): + request.operation = .asyncCancel + request.cancel_flags = flags + request.addr = targetContext + case .cancelFD(let flags, let targetFD): + request.operation = .asyncCancel + request.cancel_flags = flags + request.fileDescriptor = targetFD + case .cancelFDSlot(let flags, let target): + request.operation = .asyncCancel + request.cancel_flags = flags + request.rawValue.fd = Int32(target.index) } + return request } } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 50c97c61..6190b0da 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -26,6 +26,7 @@ extension RawIORequest { case sendMessage = 9 case receiveMessage = 10 // ... + case asyncCancel = 14 case link_timeout = 15 // ... case openAt = 18 @@ -61,6 +62,16 @@ extension RawIORequest { set { rawValue.opcode = newValue.rawValue } } + var cancel_flags: UInt32 { + get { rawValue.cancel_flags } + set { rawValue.cancel_flags = newValue } + } + + var addr: UInt64 { + get { rawValue.addr } + set { rawValue.addr = newValue } + } + public var flags: Flags { get { Flags(rawValue: rawValue.flags) } set { rawValue.flags = newValue.rawValue } diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 4aaf7543..6642293e 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -30,7 +30,7 @@ final class IORequestTests: XCTestCase { func testOpenatFixedFile() throws { let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! let fileSlot: IORingFileSlot = IORingFileSlot(resource: UInt32.max, index: 0) - let req = IORequest.opening(FilePath(platformString: pathPtr), + let req = IORequest.open(FilePath(platformString: pathPtr), in: FileDescriptor(rawValue: -100), into: fileSlot, mode: .readOnly, From 9d325b29ceb88760b55a5900d6cb796e5cbc1a20 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 23:23:26 +0000 Subject: [PATCH 076/172] Change IORingFileSlot and IORingBuffer to be nested types in IORing --- Sources/System/IORequest.swift | 52 +++++++++++++------------- Sources/System/IORing.swift | 38 +++++++++---------- Tests/SystemTests/IORequestTests.swift | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index df505210..f89facdb 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -17,12 +17,12 @@ internal enum IORequestCore { FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, - intoSlot: IORingFileSlot, + intoSlot: IORing.RegisteredFile, context: UInt64 = 0 ) case read( file: FileDescriptor, - buffer: IORingBuffer, + buffer: IORing.RegisteredBuffer, offset: UInt64 = 0, context: UInt64 = 0 ) @@ -33,20 +33,20 @@ internal enum IORequestCore { context: UInt64 = 0 ) case readSlot( - file: IORingFileSlot, - buffer: IORingBuffer, + file: IORing.RegisteredFile, + buffer: IORing.RegisteredBuffer, offset: UInt64 = 0, context: UInt64 = 0 ) case readUnregisteredSlot( - file: IORingFileSlot, + file: IORing.RegisteredFile, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0, context: UInt64 = 0 ) case write( file: FileDescriptor, - buffer: IORingBuffer, + buffer: IORing.RegisteredBuffer, offset: UInt64 = 0, context: UInt64 = 0 ) @@ -57,13 +57,13 @@ internal enum IORequestCore { context: UInt64 = 0 ) case writeSlot( - file: IORingFileSlot, - buffer: IORingBuffer, + file: IORing.RegisteredFile, + buffer: IORing.RegisteredBuffer, offset: UInt64 = 0, context: UInt64 = 0 ) case writeUnregisteredSlot( - file: IORingFileSlot, + file: IORing.RegisteredFile, buffer: UnsafeMutableRawBufferPointer, offset: UInt64 = 0, context: UInt64 = 0 @@ -73,7 +73,7 @@ internal enum IORequestCore { context: UInt64 = 0 ) case closeSlot( - IORingFileSlot, + IORing.RegisteredFile, context: UInt64 = 0 ) case unlinkAt( @@ -91,14 +91,14 @@ internal enum IORequestCore { ) case cancelFDSlot( flags: UInt32, - target: IORingFileSlot + target: IORing.RegisteredFile ) } @inline(__always) internal func makeRawRequest_readWrite_registered( file: FileDescriptor, - buffer: IORingBuffer, + buffer: IORing.RegisteredBuffer, offset: UInt64, context: UInt64 = 0, request: consuming RawIORequest @@ -113,8 +113,8 @@ internal func makeRawRequest_readWrite_registered( @inline(__always) internal func makeRawRequest_readWrite_registered_slot( - file: IORingFileSlot, - buffer: IORingBuffer, + file: IORing.RegisteredFile, + buffer: IORing.RegisteredBuffer, offset: UInt64, context: UInt64 = 0, request: consuming RawIORequest @@ -145,7 +145,7 @@ internal func makeRawRequest_readWrite_unregistered( @inline(__always) internal func makeRawRequest_readWrite_unregistered_slot( - file: IORingFileSlot, + file: IORing.RegisteredFile, buffer: UnsafeMutableRawBufferPointer, offset: UInt64, context: UInt64 = 0, @@ -173,8 +173,8 @@ extension IORequest { } public static func read( - _ file: IORingFileSlot, - into buffer: IORingBuffer, + _ file: IORing.RegisteredFile, + into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest { @@ -183,7 +183,7 @@ extension IORequest { public static func read( _ file: FileDescriptor, - into buffer: IORingBuffer, + into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest { @@ -191,7 +191,7 @@ extension IORequest { } public static func read( - _ file: IORingFileSlot, + _ file: IORing.RegisteredFile, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, context: UInt64 = 0 @@ -212,8 +212,8 @@ extension IORequest { } public static func write( - _ buffer: IORingBuffer, - into file: IORingFileSlot, + _ buffer: IORing.RegisteredBuffer, + into file: IORing.RegisteredFile, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest { @@ -221,7 +221,7 @@ extension IORequest { } public static func write( - _ buffer: IORingBuffer, + _ buffer: IORing.RegisteredBuffer, into file: FileDescriptor, at offset: UInt64 = 0, context: UInt64 = 0 @@ -231,7 +231,7 @@ extension IORequest { public static func write( _ buffer: UnsafeMutableRawBufferPointer, - into file: IORingFileSlot, + into file: IORing.RegisteredFile, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest { @@ -259,7 +259,7 @@ extension IORequest { } public static func close( - _ file: IORingFileSlot, + _ file: IORing.RegisteredFile, context: UInt64 = 0 ) -> IORequest { IORequest(core: .closeSlot(file, context: context)) @@ -268,7 +268,7 @@ extension IORequest { public static func open( _ path: FilePath, in directory: FileDescriptor, - into slot: IORingFileSlot, + into slot: IORing.RegisteredFile, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, @@ -356,7 +356,7 @@ extension IORequest { public static func cancel( _ matchAll: CancellationMatch, - matchingFile: IORingFileSlot, + matchingFile: IORing.RegisteredFile, ) -> IORequest { switch matchAll { case .all: diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 9b45f564..89391b02 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -66,20 +66,6 @@ public struct IOResource { } } -public typealias IORingFileSlot = IOResource -public typealias IORingBuffer = IOResource - -extension IORingFileSlot { - public var unsafeFileSlot: Int { - return index - } -} -extension IORingBuffer { - public var unsafeBuffer: UnsafeMutableRawBufferPointer { - return .init(start: resource.iov_base, count: resource.iov_len) - } -} - @inline(__always) internal func _writeRequest( _ request: __owned RawIORequest, ring: inout SQRing, @@ -317,6 +303,9 @@ public struct IORing: ~Copyable { var features = Features(rawValue: 0) + public typealias RegisteredFile = IOResource + public typealias RegisteredBuffer = IOResource + @frozen public struct SetupFlags: OptionSet, RawRepresentable, Hashable { public var rawValue: UInt32 @@ -565,7 +554,7 @@ public struct IORing: ~Copyable { } public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources< - IORingFileSlot.Resource + IORing.RegisteredFile.Resource > { precondition(_registeredFiles.isEmpty) precondition(count < UInt32.max) @@ -593,12 +582,12 @@ public struct IORing: ~Copyable { fatalError("failed to unregister files") } - public var registeredFileSlots: RegisteredResources { + public var registeredFileSlots: RegisteredResources { RegisteredResources(resources: _registeredFiles) } public mutating func registerBuffers(_ buffers: some Collection) throws(Errno) - -> RegisteredResources + -> RegisteredResources { precondition(buffers.count < UInt32.max) precondition(_registeredBuffers.isEmpty) @@ -624,7 +613,7 @@ public struct IORing: ~Copyable { } public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(Errno) - -> RegisteredResources + -> RegisteredResources { try registerBuffers(buffers) } @@ -645,7 +634,7 @@ public struct IORing: ~Copyable { } } - public var registeredBuffers: RegisteredResources { + public var registeredBuffers: RegisteredResources { RegisteredResources(resources: _registeredBuffers) } @@ -750,3 +739,14 @@ public struct IORing: ~Copyable { close(ringDescriptor) } } + +extension IORing.RegisteredFile { + public var unsafeFileSlot: Int { + return index + } +} +extension IORing.RegisteredBuffer { + public var unsafeBuffer: UnsafeMutableRawBufferPointer { + return .init(start: resource.iov_base, count: resource.iov_len) + } +} \ No newline at end of file diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 6642293e..9705026f 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -29,7 +29,7 @@ final class IORequestTests: XCTestCase { func testOpenatFixedFile() throws { let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! - let fileSlot: IORingFileSlot = IORingFileSlot(resource: UInt32.max, index: 0) + let fileSlot = IORing.RegisteredFile(resource: UInt32.max, index: 0) let req = IORequest.open(FilePath(platformString: pathPtr), in: FileDescriptor(rawValue: -100), into: fileSlot, From ea77ef8a21e45e28c267c68a6f318fe578acae69 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 23:24:09 +0000 Subject: [PATCH 077/172] Minor cleanup --- Sources/System/IORing.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 89391b02..54e3b104 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -553,9 +553,7 @@ public struct IORing: ~Copyable { } } - public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources< - IORing.RegisteredFile.Resource - > { + public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources { precondition(_registeredFiles.isEmpty) precondition(count < UInt32.max) let files = [UInt32](repeating: UInt32.max, count: count) From baa3d625693df48a7882c689350431fa7794e361 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 23:26:14 +0000 Subject: [PATCH 078/172] API tweak --- Sources/System/IORequest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index f89facdb..4be37d32 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -344,7 +344,7 @@ extension IORequest { public static func cancel( _ matchAll: CancellationMatch, - matchingFileDescriptor: FileDescriptor, + matching: FileDescriptor, ) -> IORequest { switch matchAll { case .all: @@ -356,7 +356,7 @@ extension IORequest { public static func cancel( _ matchAll: CancellationMatch, - matchingFile: IORing.RegisteredFile, + matching: IORing.RegisteredFile, ) -> IORequest { switch matchAll { case .all: From 3760dc265ac2ebce5dfa87d030148edc2e7b0d30 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 16:35:08 -0700 Subject: [PATCH 079/172] Nest IOResource, and update proposal to match --- NNNN-swift-system-io-uring.md | 70 ++++++++++++++++------------------- Sources/System/IORing.swift | 34 ++++++++--------- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md index 0d8cf8ec..2bd981d8 100644 --- a/NNNN-swift-system-io-uring.md +++ b/NNNN-swift-system-io-uring.md @@ -33,7 +33,7 @@ We propose a *low level, unopinionated* Swift interface for io_uring on Linux (s * Enqueueing IO requests * Dequeueing IO completions -`struct IOResource` represents, via its two typealiases `IORingFileSlot` and `IORingBuffer`, registered file descriptors and buffers. +`struct RegisteredResource` represents, via its two typealiases `IORing.RegisteredFile` and `IORing.RegisteredBuffer`, registered file descriptors and buffers. `struct IORequest: ~Copyable` represents an IO operation that can be enqueued for the kernel to execute. It supports a wide variety of operations matching traditional unix file and socket operations. @@ -43,7 +43,7 @@ IORequest operations are expressed as overloaded static methods on `IORequest`, public static func open( _ path: FilePath, in directory: FileDescriptor, - into slot: IORingFileSlot, + into slot: IORing.RegisteredFile, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, @@ -84,14 +84,6 @@ Unfortunately the underlying kernel API makes it relatively difficult to determi ## Detailed design ```swift -public struct IOResource { } -public typealias IORingFileSlot = IOResource -public typealias IORingBuffer = IOResource - -extension IORingBuffer { - public var unsafeBuffer: UnsafeMutableRawBufferPointer -} - // IORing is intentionally not Sendable, to avoid internal locking overhead public struct IORing: ~Copyable { @@ -111,6 +103,10 @@ public struct IORing: ~Copyable { public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) public mutating func unregisterEventFD(_ descriptor: FileDescriptor) throws(Errno) + + public struct RegisteredResource { } + public typealias RegisteredFile = RegisteredResource + public typealias RegisteredBuffer = RegisteredResource // An IORing.RegisteredResources is a view into the buffers or files registered with the ring, if any public struct RegisteredResources: RandomAccessCollection { @@ -118,23 +114,23 @@ public struct IORing: ~Copyable { public subscript(position: UInt16) -> IOResource // This is useful because io_uring likes to use UInt16s as indexes } - public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources + public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources public func unregisterFiles() - public var registeredFileSlots: RegisteredResources + public var registeredFileSlots: RegisteredResources public mutating func registerBuffers( _ buffers: some Collection - ) throws(Errno) -> RegisteredResources + ) throws(Errno) -> RegisteredResources public mutating func registerBuffers( _ buffers: UnsafeMutableRawBufferPointer... - ) throws(Errno) -> RegisteredResources + ) throws(Errno) -> RegisteredResources public func unregisterBuffers() - public var registeredBuffers: RegisteredResources + public var registeredBuffers: RegisteredResources public func prepare(requests: IORequest...) public func prepare(linkedRequests: IORequest...) @@ -189,27 +185,31 @@ public struct IORing: ~Copyable { public var supportedFeatures: Features } +extension IORing.RegisteredBuffer { + public var unsafeBuffer: UnsafeMutableRawBufferPointer +} + public struct IORequest: ~Copyable { public static func nop(context: UInt64 = 0) -> IORequest // overloads for each combination of registered vs unregistered buffer/descriptor // Read public static func read( - _ file: IORingFileSlot, - into buffer: IORingBuffer, + _ file: IORing.RegisteredFile, + into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest public static func read( _ file: FileDescriptor, - into buffer: IORingBuffer, + into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest public static func read( - _ file: IORingFileSlot, + _ file: IORing.RegisteredFile, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, context: UInt64 = 0 @@ -224,14 +224,14 @@ public struct IORequest: ~Copyable { // Write public static func write( - _ buffer: IORingBuffer, - into file: IORingFileSlot, + _ buffer: IORing.RegisteredBuffer, + into file: IORing.RegisteredFile, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest public static func write( - _ buffer: IORingBuffer, + _ buffer: IORing.RegisteredBuffer, into file: FileDescriptor, at offset: UInt64 = 0, context: UInt64 = 0 @@ -239,7 +239,7 @@ public struct IORequest: ~Copyable { public static func write( _ buffer: UnsafeMutableRawBufferPointer, - into file: IORingFileSlot, + into file: IORing.RegisteredFile, at offset: UInt64 = 0, context: UInt64 = 0 ) -> IORequest @@ -258,7 +258,7 @@ public struct IORequest: ~Copyable { ) -> IORequest public static func close( - _ file: IORingFileSlot, + _ file: IORing.RegisteredFile, context: UInt64 = 0 ) -> IORequest @@ -266,7 +266,7 @@ public struct IORequest: ~Copyable { public static func open( _ path: FilePath, in directory: FileDescriptor, - into slot: IORingFileSlot, + into slot: IORing.RegisteredFile, mode: FileDescriptor.AccessMode, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, @@ -296,26 +296,18 @@ public struct IORequest: ~Copyable { } public static func cancel( - _ matchAll: CancellationMatch, - matchingContext: UInt64, - context: UInt64 - ) -> IORequest - - public static func cancel( - _ matchAll: CancellationMatch, - matchingFileDescriptor: FileDescriptor, - context: UInt64 + _ matchAll: CancellationMatch, + matchingContext: UInt64, ) -> IORequest public static func cancel( - _ matchAll: CancellationMatch, - matchingRegisteredFileDescriptorAtIndex: Int, - context: UInt64 + _ matchAll: CancellationMatch, + matching: FileDescriptor, ) -> IORequest public static func cancel( - _ matchAll: CancellationMatch, - context: UInt64 + _ matchAll: CancellationMatch, + matching: IORing.RegisteredFile, ) -> IORequest // Other operations follow in the same pattern diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 54e3b104..7d69aa35 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -52,20 +52,6 @@ struct CQRing: ~Copyable { let cqes: UnsafeBufferPointer } -public struct IOResource { - public typealias Resource = T - @usableFromInline let resource: T - @usableFromInline let index: Int - - internal init( - resource: T, - index: Int - ) { - self.resource = resource - self.index = index - } -} - @inline(__always) internal func _writeRequest( _ request: __owned RawIORequest, ring: inout SQRing, @@ -302,9 +288,23 @@ public struct IORing: ~Copyable { var _registeredBuffers: [iovec] var features = Features(rawValue: 0) + + public struct RegisteredResource { + public typealias Resource = T + @usableFromInline let resource: T + @usableFromInline let index: Int + + internal init( + resource: T, + index: Int + ) { + self.resource = resource + self.index = index + } + } - public typealias RegisteredFile = IOResource - public typealias RegisteredBuffer = IOResource + public typealias RegisteredFile = RegisteredResource + public typealias RegisteredBuffer = RegisteredResource @frozen public struct SetupFlags: OptionSet, RawRepresentable, Hashable { @@ -747,4 +747,4 @@ extension IORing.RegisteredBuffer { public var unsafeBuffer: UnsafeMutableRawBufferPointer { return .init(start: resource.iov_base, count: resource.iov_len) } -} \ No newline at end of file +} From a98cd3f2dae5cdd397f71e4cc6d549229a570bf7 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 23:47:00 +0000 Subject: [PATCH 080/172] Build fix and rename IOResource --- Sources/System/IORequest.swift | 8 ++++---- Sources/System/IORing.swift | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 4be37d32..b5d48af3 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -348,9 +348,9 @@ extension IORequest { ) -> IORequest { switch matchAll { case .all: - IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD, targetFD: matchingFileDescriptor)) + IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD, targetFD: matching)) case .first: - IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD, targetFD: matchingFileDescriptor)) + IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD, targetFD: matching)) } } @@ -360,9 +360,9 @@ extension IORequest { ) -> IORequest { switch matchAll { case .all: - IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD_FIXED, target: matchingFile)) + IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) case .first: - IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD_FIXED, target: matchingFile)) + IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) } } diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 7d69aa35..76f68182 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -624,11 +624,11 @@ public struct IORing: ~Copyable { init(resources: [T]) { self.resources = resources } - public subscript(position: Int) -> IOResource { - IOResource(resource: resources[position], index: position) + public subscript(position: Int) -> RegisteredResource { + RegisteredResource(resource: resources[position], index: position) } - public subscript(position: UInt16) -> IOResource { - IOResource(resource: resources[Int(position)], index: Int(position)) + public subscript(position: UInt16) -> RegisteredResource { + RegisteredResource(resource: resources[Int(position)], index: Int(position)) } } From 344b3b85f8bb0d8014efb27927e639fb2dae5d62 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Apr 2025 16:59:39 -0700 Subject: [PATCH 081/172] Proposal updates --- NNNN-swift-system-io-uring.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md index 2bd981d8..978b87bd 100644 --- a/NNNN-swift-system-io-uring.md +++ b/NNNN-swift-system-io-uring.md @@ -108,7 +108,7 @@ public struct IORing: ~Copyable { public typealias RegisteredFile = RegisteredResource public typealias RegisteredBuffer = RegisteredResource - // An IORing.RegisteredResources is a view into the buffers or files registered with the ring, if any + // A `RegisteredResources` is a view into the buffers or files registered with the ring, if any public struct RegisteredResources: RandomAccessCollection { public subscript(position: Int) -> IOResource public subscript(position: UInt16) -> IOResource // This is useful because io_uring likes to use UInt16s as indexes @@ -355,7 +355,7 @@ ring.prepare(linkedRequests: in: parentDirectory, into: file, mode: mode, - options: openOptions, + options: openOptions, permissions: nil ), .stat(file, From c47779fac00f63042684364a7011aefc541a187d Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 22 Apr 2025 23:06:48 +0000 Subject: [PATCH 082/172] Try making requests and completions nested types --- Sources/System/IOCompletion.swift | 22 +++--- Sources/System/IORequest.swift | 92 +++++++++++++------------- Sources/System/IORing.swift | 30 ++++----- Tests/SystemTests/IORequestTests.swift | 4 +- Tests/SystemTests/IORingTests.swift | 2 +- 5 files changed, 76 insertions(+), 74 deletions(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index b15f0d45..5fa29325 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -1,11 +1,13 @@ @_implementationOnly import CSystem -public struct IOCompletion: ~Copyable { - let rawValue: io_uring_cqe +public extension IORing { + struct Completion: ~Copyable { + let rawValue: io_uring_cqe + } } -extension IOCompletion { - public struct Flags: OptionSet, Hashable, Codable { +public extension IORing.Completion { + struct Flags: OptionSet, Hashable, Codable { public let rawValue: UInt32 public init(rawValue: UInt32) { @@ -19,32 +21,32 @@ extension IOCompletion { } } -extension IOCompletion { - public var context: UInt64 { +public extension IORing.Completion { + var context: UInt64 { get { rawValue.user_data } } - public var userPointer: UnsafeRawPointer? { + var userPointer: UnsafeRawPointer? { get { UnsafeRawPointer(bitPattern: UInt(rawValue.user_data)) } } - public var result: Int32 { + var result: Int32 { get { rawValue.res } } - public var flags: IOCompletion.Flags { + var flags: IORing.Completion.Flags { get { Flags(rawValue: rawValue.flags & 0x0000FFFF) } } - public var bufferIndex: UInt16? { + var bufferIndex: UInt16? { get { if self.flags.contains(.allocatedBuffer) { return UInt16(rawValue.flags >> 16) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index b5d48af3..3c4d2a27 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -159,17 +159,21 @@ internal func makeRawRequest_readWrite_unregistered_slot( return request } -public struct IORequest { - @usableFromInline var core: IORequestCore +extension IORing { + public struct Request { + @usableFromInline var core: IORequestCore - @inlinable internal consuming func extractCore() -> IORequestCore { - return core + @inlinable internal consuming func extractCore() -> IORequestCore { + return core + } } } -extension IORequest { - public static func nop(context: UInt64 = 0) -> IORequest { - IORequest(core: .nop) + + +extension IORing.Request { + public static func nop(context: UInt64 = 0) -> IORing.Request { + .init(core: .nop) } public static func read( @@ -177,8 +181,8 @@ extension IORequest { into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .readSlot(file: file, buffer: buffer, offset: offset, context: context)) + ) -> IORing.Request { + .init(core: .readSlot(file: file, buffer: buffer, offset: offset, context: context)) } public static func read( @@ -186,8 +190,8 @@ extension IORequest { into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .read(file: file, buffer: buffer, offset: offset, context: context)) + ) -> IORing.Request { + .init(core: .read(file: file, buffer: buffer, offset: offset, context: context)) } public static func read( @@ -195,10 +199,8 @@ extension IORequest { into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest( - core: .readUnregisteredSlot( - file: file, buffer: buffer, offset: offset, context: context)) + ) -> IORing.Request { + .init(core: .readUnregisteredSlot(file: file, buffer: buffer, offset: offset, context: context)) } public static func read( @@ -206,9 +208,8 @@ extension IORequest { into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest( - core: .readUnregistered(file: file, buffer: buffer, offset: offset, context: context)) + ) -> IORing.Request { + .init(core: .readUnregistered(file: file, buffer: buffer, offset: offset, context: context)) } public static func write( @@ -216,8 +217,8 @@ extension IORequest { into file: IORing.RegisteredFile, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .writeSlot(file: file, buffer: buffer, offset: offset, context: context)) + ) -> IORing.Request { + .init(core: .writeSlot(file: file, buffer: buffer, offset: offset, context: context)) } public static func write( @@ -225,8 +226,8 @@ extension IORequest { into file: FileDescriptor, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .write(file: file, buffer: buffer, offset: offset, context: context)) + ) -> IORing.Request { + .init(core: .write(file: file, buffer: buffer, offset: offset, context: context)) } public static func write( @@ -234,9 +235,8 @@ extension IORequest { into file: IORing.RegisteredFile, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest( - core: .writeUnregisteredSlot( + ) -> IORing.Request { + .init(core: .writeUnregisteredSlot( file: file, buffer: buffer, offset: offset, context: context)) } @@ -245,8 +245,8 @@ extension IORequest { into file: FileDescriptor, at offset: UInt64 = 0, context: UInt64 = 0 - ) -> IORequest { - IORequest( + ) -> IORing.Request { + .init( core: .writeUnregistered(file: file, buffer: buffer, offset: offset, context: context) ) } @@ -254,15 +254,15 @@ extension IORequest { public static func close( _ file: FileDescriptor, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .close(file, context: context)) + ) -> IORing.Request { + .init(core: .close(file, context: context)) } public static func close( _ file: IORing.RegisteredFile, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .closeSlot(file, context: context)) + ) -> IORing.Request { + .init(core: .closeSlot(file, context: context)) } public static func open( @@ -273,8 +273,8 @@ extension IORequest { options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, context: UInt64 = 0 - ) -> IORequest { - IORequest( + ) -> IORing.Request { + .init( core: .openatSlot( atDirectory: directory, path: path, mode, options: options, permissions: permissions, intoSlot: slot, context: context)) @@ -287,8 +287,8 @@ extension IORequest { options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, context: UInt64 = 0 - ) -> IORequest { - IORequest( + ) -> IORing.Request { + .init( core: .openat( atDirectory: directory, path: path, mode, options: options, permissions: permissions, context: context @@ -299,8 +299,8 @@ extension IORequest { _ path: FilePath, in directory: FileDescriptor, context: UInt64 = 0 - ) -> IORequest { - IORequest(core: .unlinkAt(atDirectory: directory, path: path, context: context)) + ) -> IORing.Request { + .init(core: .unlinkAt(atDirectory: directory, path: path, context: context)) } // Cancel @@ -333,36 +333,36 @@ extension IORequest { public static func cancel( _ matchAll: CancellationMatch, matchingContext: UInt64, - ) -> IORequest { + ) -> IORing.Request { switch matchAll { case .all: - IORequest(core: .cancel(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + .init(core: .cancel(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) case .first: - IORequest(core: .cancel(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + .init(core: .cancel(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) } } public static func cancel( _ matchAll: CancellationMatch, matching: FileDescriptor, - ) -> IORequest { + ) -> IORing.Request { switch matchAll { case .all: - IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD, targetFD: matching)) + .init(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD, targetFD: matching)) case .first: - IORequest(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD, targetFD: matching)) + .init(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD, targetFD: matching)) } } public static func cancel( _ matchAll: CancellationMatch, matching: IORing.RegisteredFile, - ) -> IORequest { + ) -> IORing.Request { switch matchAll { case .all: - IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + .init(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) case .first: - IORequest(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + .init(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) } } diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 76f68182..dafedce2 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -374,7 +374,7 @@ public struct IORing: ~Copyable { minimumCount: UInt32, maximumCount: UInt32, extraArgs: UnsafeMutablePointer? = nil, - consumer: (consuming IOCompletion?, Errno?, Bool) throws(Err) -> Void + consumer: (consuming IORing.Completion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { var count = 0 while let completion = _tryConsumeCompletion(ring: completionRing) { @@ -440,10 +440,10 @@ public struct IORing: ~Copyable { internal func _blockingConsumeOneCompletion( extraArgs: UnsafeMutablePointer? = nil - ) throws(Errno) -> IOCompletion { - var result: IOCompletion? = nil + ) throws(Errno) -> Completion { + var result: Completion? = nil try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { - (completion: consuming IOCompletion?, error, done) throws(Errno) in + (completion: consuming Completion?, error, done) throws(Errno) in if let error { throw error } @@ -456,13 +456,13 @@ public struct IORing: ~Copyable { public func blockingConsumeCompletion( timeout: Duration? = nil - ) throws(Errno) -> IOCompletion { + ) throws(Errno) -> Completion { if let timeout { var ts = __kernel_timespec( tv_sec: timeout.components.seconds, tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) - return try withUnsafePointer(to: &ts) { (tsPtr) throws(Errno) -> IOCompletion in + return try withUnsafePointer(to: &ts) { (tsPtr) throws(Errno) -> Completion in var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, @@ -479,7 +479,7 @@ public struct IORing: ~Copyable { public func blockingConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, Errno?, Bool) throws(Err) -> Void + consumer: (consuming Completion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { if let timeout { var ts = __kernel_timespec( @@ -507,11 +507,11 @@ public struct IORing: ~Copyable { // } - public func tryConsumeCompletion() -> IOCompletion? { + public func tryConsumeCompletion() -> Completion? { return _tryConsumeCompletion(ring: completionRing) } - func _tryConsumeCompletion(ring: borrowing CQRing) -> IOCompletion? { + func _tryConsumeCompletion(ring: borrowing CQRing) -> Completion? { let tail = ring.kernelTail.pointee.load(ordering: .acquiring) let head = ring.kernelHead.pointee.load(ordering: .acquiring) @@ -519,7 +519,7 @@ public struct IORing: ~Copyable { // 32 byte copy - oh well let res = ring.cqes[Int(head & ring.ringMask)] ring.kernelHead.pointee.store(head &+ 1, ordering: .releasing) - return IOCompletion(rawValue: res) + return Completion(rawValue: res) } return nil @@ -652,7 +652,7 @@ public struct IORing: ~Copyable { public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, - consumer: (consuming IOCompletion?, Errno?, Bool) throws(Err) -> Void + consumer: (consuming Completion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { //TODO: optimize this to one uring_enter do { @@ -667,13 +667,13 @@ public struct IORing: ~Copyable { ) } - public mutating func prepare(request: __owned IORequest) -> Bool { + public mutating func prepare(request: __owned Request) -> Bool { var raw: RawIORequest? = request.makeRawRequest() return _writeRequest( raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } - mutating func prepare(linkedRequests: some BidirectionalCollection) { + mutating func prepare(linkedRequests: some BidirectionalCollection) { guard linkedRequests.count > 0 else { return } @@ -690,11 +690,11 @@ public struct IORing: ~Copyable { } //@inlinable //TODO: make sure the array allocation gets optimized out... - public mutating func prepare(linkedRequests: IORequest...) { + public mutating func prepare(linkedRequests: Request...) { prepare(linkedRequests: linkedRequests) } - public mutating func submit(linkedRequests: IORequest...) throws(Errno) { + public mutating func submit(linkedRequests: Request...) throws(Errno) { prepare(linkedRequests: linkedRequests) try submitPreparedRequests() } diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 9705026f..3d1f72de 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -19,7 +19,7 @@ func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { // which are known to work correctly. final class IORequestTests: XCTestCase { func testNop() { - let req = IORequest.nop().makeRawRequest() + let req = IORing.Request.nop().makeRawRequest() let sourceBytes = requestBytes(req) // convenient property of nop: it's all zeros! // for some unknown reason, liburing sets the fd field to -1. @@ -30,7 +30,7 @@ final class IORequestTests: XCTestCase { func testOpenatFixedFile() throws { let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! let fileSlot = IORing.RegisteredFile(resource: UInt32.max, index: 0) - let req = IORequest.open(FilePath(platformString: pathPtr), + let req = IORing.Request.open(FilePath(platformString: pathPtr), in: FileDescriptor(rawValue: -100), into: fileSlot, mode: .readOnly, diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 57437e4d..6c086da6 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -13,7 +13,7 @@ final class IORingTests: XCTestCase { func testNop() throws { var ring = try IORing(queueDepth: 32, flags: []) - try ring.submit(linkedRequests: IORequest.nop()) + try ring.submit(linkedRequests: .nop()) let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) } From a49ba8a8b3194043d839d691a3229d8cb20a8309 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 22 Apr 2025 23:20:37 +0000 Subject: [PATCH 083/172] Give flags a default value --- Sources/System/IORing.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index dafedce2..94c9504a 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -330,7 +330,7 @@ public struct IORing: ~Copyable { //TODO: should IORING_SETUP_NO_SQARRAY be the default? do we need to adapt anything to it? } - public init(queueDepth: UInt32, flags: SetupFlags) throws(Errno) { + public init(queueDepth: UInt32, flags: SetupFlags = []) throws(Errno) { let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags, submissionRing: &submissionRing) // All throws need to be before initializing ivars here to avoid // "error: conditional initialization or destruction of noncopyable types is not supported; From f38d72f310a133490e85f1fa50a64c49bd59b8a8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 22 Apr 2025 16:30:50 -0700 Subject: [PATCH 084/172] Nested types, some renames, misc other updates --- NNNN-swift-system-io-uring.md | 333 +++++++++++++++++----------------- 1 file changed, 166 insertions(+), 167 deletions(-) diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md index 978b87bd..37f0c5c9 100644 --- a/NNNN-swift-system-io-uring.md +++ b/NNNN-swift-system-io-uring.md @@ -28,16 +28,16 @@ We propose a *low level, unopinionated* Swift interface for io_uring on Linux (s `struct IORing: ~Copyable` provides facilities for -* Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file descriptors that improves their efficiency +* Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file IOdescriptors that improves their efficiency * Registering and unregistering eventfds, which allow asynchronous waiting for completions * Enqueueing IO requests * Dequeueing IO completions -`struct RegisteredResource` represents, via its two typealiases `IORing.RegisteredFile` and `IORing.RegisteredBuffer`, registered file descriptors and buffers. +`struct IORing.RegisteredResource` represents, via its two typealiases `IORing.RegisteredFile` and `IORing.RegisteredBuffer`, registered file descriptors and buffers. -`struct IORequest: ~Copyable` represents an IO operation that can be enqueued for the kernel to execute. It supports a wide variety of operations matching traditional unix file and socket operations. +`struct IORing.Request: ~Copyable` represents an IO operation that can be enqueued for the kernel to execute. It supports a wide variety of operations matching traditional unix file and socket operations. -IORequest operations are expressed as overloaded static methods on `IORequest`, e.g. `openat` is spelled +Request operations are expressed as overloaded static methods on `Request`, e.g. `openat` is spelled ```swift public static func open( @@ -48,7 +48,7 @@ IORequest operations are expressed as overloaded static methods on `IORequest`, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, context: UInt64 = 0 - ) -> IORequest + ) -> Request public static func open( _ path: FilePath, @@ -57,12 +57,12 @@ IORequest operations are expressed as overloaded static methods on `IORequest`, options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), permissions: FilePermissions? = nil, context: UInt64 = 0 - ) -> IORequest + ) -> Request ``` which allows clients to decide whether they want to open the file into a slot on the ring, or have it return a file descriptor via a completion. Similarly, read operations have overloads for "use a buffer from the ring" or "read into this `UnsafeMutableBufferPointer`" -Multiple `IORequests` can be enqueued on a single `IORing` using the `prepare(…)` family of methods, and then submitted together using `submitPreparedRequests`, allowing for things like "open this file, read its contents, and then close it" to be a single syscall. Conveniences are provided for preparing and submitting requests in one call. +Multiple `Requests` can be enqueued on a single `IORing` using the `prepare(…)` family of methods, and then submitted together using `submitPreparedRequests`, allowing for things like "open this file, read its contents, and then close it" to be a single syscall. Conveniences are provided for preparing and submitting requests in one call. Since IO operations can execute in parallel or out of order by default, linked chains of operations can be established with `prepare(linkedRequests:…)` and related methods. Separate chains can still execute in parallel, and if an operation early in the chain fails, all subsequent operations will deliver cancellation errors as their completion. @@ -70,14 +70,14 @@ Already-completed results can be retrieved from the ring using `tryConsumeComple Since neither polling nor synchronously waiting is optimal in many cases, `IORing` also exposes the ability to register an eventfd (see `man eventfd(2)`), which will become readable when completions are available on the ring. This can then be monitored asynchronously with `epoll`, `kqueue`, or for clients who are linking libdispatch, `DispatchSource`. -`struct IOCompletion: ~Copyable` represents the result of an IO operation and provides +`struct Completion: ~Copyable` represents the result of an IO operation and provides * Flags indicating various operation-specific metadata about the now-completed syscall * The context associated with the operation when it was enqueued, as an `UnsafeRawPointer` or a `UInt64` * The result of the operation, as an `Int32` with operation-specific meaning * The error, if one occurred -Unfortunately the underlying kernel API makes it relatively difficult to determine which `IORequest` led to a given `IOCompletion`, so it's expected that users will need to create this association themselves via the context parameter. +Unfortunately the underlying kernel API makes it relatively difficult to determine which `Request` led to a given `Completion`, so it's expected that users will need to create this association themselves via the context parameter. `IORing.Features` describes the supported features of the underlying kernel `IORing` implementation, which can be used to provide graceful reduction in functionality when running on older systems. @@ -87,37 +87,35 @@ Unfortunately the underlying kernel API makes it relatively difficult to determi // IORing is intentionally not Sendable, to avoid internal locking overhead public struct IORing: ~Copyable { - public init(queueDepth: UInt32, flags: IORing.SetupFlags) throws(Errno) - - public struct SetupFlags: OptionSet, RawRepresentable, Hashable { - public var rawValue: UInt32 - public init(rawValue: UInt32) - public static var pollCompletions: SetupFlags //IORING_SETUP_IOPOLL - public static var pollSubmissions: SetupFlags //IORING_SETUP_SQPOLL - public static var clampMaxEntries: SetupFlags //IORING_SETUP_CLAMP - public static var startDisabled: SetupFlags //IORING_SETUP_R_DISABLED - public static var continueSubmittingOnError: SetupFlags //IORING_SETUP_SUBMIT_ALL - public static var singleSubmissionThread: SetupFlags //IORING_SETUP_SINGLE_ISSUER - public static var deferRunningTasks: SetupFlags //IORING_SETUP_DEFER_TASKRUN - } - - public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) - public mutating func unregisterEventFD(_ descriptor: FileDescriptor) throws(Errno) + public init(queueDepth: UInt32, flags: IORing.SetupFlags = []) throws(Errno) + + public struct SetupFlags: OptionSet, RawRepresentable, Hashable { + public var rawValue: UInt32 + public init(rawValue: UInt32) + public static var pollCompletions: SetupFlags //IORING_SETUP_IOPOLL + public static var pollSubmissions: SetupFlags //IORING_SETUP_SQPOLL + public static var clampMaxEntries: SetupFlags //IORING_SETUP_CLAMP + public static var startDisabled: SetupFlags //IORING_SETUP_R_DISABLED + public static var continueSubmittingOnError: SetupFlags //IORING_SETUP_SUBMIT_ALL + public static var singleSubmissionThread: SetupFlags //IORING_SETUP_SINGLE_ISSUER + public static var deferRunningTasks: SetupFlags //IORING_SETUP_DEFER_TASKRUN + } + + public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) + public mutating func unregisterEventFD() throws(Errno) public struct RegisteredResource { } public typealias RegisteredFile = RegisteredResource public typealias RegisteredBuffer = RegisteredResource - - // A `RegisteredResources` is a view into the buffers or files registered with the ring, if any - public struct RegisteredResources: RandomAccessCollection { - public subscript(position: Int) -> IOResource - public subscript(position: UInt16) -> IOResource // This is useful because io_uring likes to use UInt16s as indexes - } + + // A `RegisteredResources` is a view into the buffers or files registered with the ring, if any + public struct RegisteredResources: RandomAccessCollection { + public subscript(position: Int) -> RegisteredResource + public subscript(position: UInt16) -> RegisteredResource // This is useful because io_uring likes to use UInt16s as indexes + } public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources - public func unregisterFiles() - public var registeredFileSlots: RegisteredResources public mutating func registerBuffers( @@ -132,33 +130,33 @@ public struct IORing: ~Copyable { public var registeredBuffers: RegisteredResources - public func prepare(requests: IORequest...) - public func prepare(linkedRequests: IORequest...) + public func prepare(requests: Request...) + public func prepare(linkedRequests: Request...) public func submitPreparedRequests(timeout: Duration? = nil) throws(Errno) - public func submit(requests: IORequest..., timeout: Duration? = nil) throws(Errno) - public func submit(linkedRequests: IORequest..., timeout: Duration? = nil) throws(Errno) + public func submit(requests: Request..., timeout: Duration? = nil) throws(Errno) + public func submit(linkedRequests: Request..., timeout: Duration? = nil) throws(Errno) public func submitPreparedRequests() throws(Errno) public func submitPreparedRequestsAndWait(timeout: Duration? = nil) throws(Errno) public func submitPreparedRequestsAndConsumeCompletions( - minimumCount: UInt32 = 1, - timeout: Duration? = nil, - consumer: (consuming IOCompletion?, Errno?, Bool) throws(E) -> Void - ) throws(E) + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (consuming Completion?, Errno?, Bool) throws(E) -> Void + ) throws(E) public func blockingConsumeCompletion( - timeout: Duration? = nil - ) throws(Errno) -> IOCompletion + timeout: Duration? = nil + ) throws(Errno) -> Completion public func blockingConsumeCompletions( - minimumCount: UInt32 = 1, - timeout: Duration? = nil, - consumer: (consuming IOCompletion?, Errno?, Bool) throws(E) -> Void + minimumCount: UInt32 = 1, + timeout: Duration? = nil, + consumer: (consuming Completion?, Errno?, Bool) throws(E) -> Void ) throws(E) - public func tryConsumeCompletion() -> IOCompletion? + public func tryConsumeCompletion() -> Completion? public struct Features: OptionSet, RawRepresentable, Hashable { let rawValue: UInt32 @@ -185,155 +183,156 @@ public struct IORing: ~Copyable { public var supportedFeatures: Features } -extension IORing.RegisteredBuffer { - public var unsafeBuffer: UnsafeMutableRawBufferPointer +public extension IORing.RegisteredBuffer { + var unsafeBuffer: UnsafeMutableRawBufferPointer } -public struct IORequest: ~Copyable { - public static func nop(context: UInt64 = 0) -> IORequest - - // overloads for each combination of registered vs unregistered buffer/descriptor - // Read +public extension IORing { + struct Request: ~Copyable { + public static func nop(context: UInt64 = 0) -> Request + + // overloads for each combination of registered vs unregistered buffer/descriptor + // Read public static func read( - _ file: IORing.RegisteredFile, - into buffer: IORing.RegisteredBuffer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ file: IORing.RegisteredFile, + into buffer: IORing.RegisteredBuffer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + public static func read( - _ file: FileDescriptor, - into buffer: IORing.RegisteredBuffer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ file: FileDescriptor, + into buffer: IORing.RegisteredBuffer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + public static func read( - _ file: IORing.RegisteredFile, - into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ file: IORing.RegisteredFile, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + public static func read( - _ file: FileDescriptor, - into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ file: FileDescriptor, + into buffer: UnsafeMutableRawBufferPointer, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + // Write public static func write( - _ buffer: IORing.RegisteredBuffer, - into file: IORing.RegisteredFile, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ buffer: IORing.RegisteredBuffer, + into file: IORing.RegisteredFile, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + public static func write( - _ buffer: IORing.RegisteredBuffer, - into file: FileDescriptor, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ buffer: IORing.RegisteredBuffer, + into file: FileDescriptor, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + public static func write( - _ buffer: UnsafeMutableRawBufferPointer, - into file: IORing.RegisteredFile, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ buffer: UnsafeMutableRawBufferPointer, + into file: IORing.RegisteredFile, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + public static func write( - _ buffer: UnsafeMutableRawBufferPointer, - into file: FileDescriptor, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> IORequest - + _ buffer: UnsafeMutableRawBufferPointer, + into file: FileDescriptor, + at offset: UInt64 = 0, + context: UInt64 = 0 + ) -> Request + // Close public static func close( - _ file: FileDescriptor, - context: UInt64 = 0 - ) -> IORequest - + _ file: FileDescriptor, + context: UInt64 = 0 + ) -> Request + public static func close( - _ file: IORing.RegisteredFile, - context: UInt64 = 0 - ) -> IORequest - + _ file: IORing.RegisteredFile, + context: UInt64 = 0 + ) -> Request + // Open At public static func open( - _ path: FilePath, - in directory: FileDescriptor, - into slot: IORing.RegisteredFile, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - context: UInt64 = 0 - ) -> IORequest - + _ path: FilePath, + in directory: FileDescriptor, + into slot: IORing.RegisteredFile, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> Request + public static func open( - _ path: FilePath, - in directory: FileDescriptor, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - context: UInt64 = 0 - ) -> IORequest - + _ path: FilePath, + in directory: FileDescriptor, + mode: FileDescriptor.AccessMode, + options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), + permissions: FilePermissions? = nil, + context: UInt64 = 0 + ) -> Request + public static func unlink( - _ path: FilePath, - in directory: FileDescriptor, - context: UInt64 = 0 - ) -> IORequest - + _ path: FilePath, + in directory: FileDescriptor, + context: UInt64 = 0 + ) -> Request + // Cancel - + public enum CancellationMatch { - case all - case first + case all + case first } - + public static func cancel( _ matchAll: CancellationMatch, matchingContext: UInt64, - ) -> IORequest - + ) -> Request + public static func cancel( _ matchAll: CancellationMatch, matching: FileDescriptor, - ) -> IORequest - + ) -> Request + public static func cancel( _ matchAll: CancellationMatch, matching: IORing.RegisteredFile, - ) -> IORequest - - // Other operations follow in the same pattern -} + ) -> Request -public struct IOCompletion { + // Other operations follow in the same pattern + } + + struct Completion { + public struct Flags: OptionSet, Hashable, Codable { + public let rawValue: UInt32 - public struct Flags: OptionSet, Hashable, Codable { - public let rawValue: UInt32 + public init(rawValue: UInt32) - public init(rawValue: UInt32) - - public static let moreCompletions: Flags - public static let socketNotEmpty: Flags - public static let isNotificationEvent: Flags - } + public static let moreCompletions: Flags + public static let socketNotEmpty: Flags + public static let isNotificationEvent: Flags + } - //These are both the same value, but having both eliminates some ugly casts in client code - public var context: UInt64 - public var contextPointer: UnsafeRawPointer - - public var result: Int32 - - public var error: Errno? // Convenience wrapper over `result` - - public var flags: Flags + //These are both the same value, but having both eliminates some ugly casts in client code + public var context: UInt64 + public var contextPointer: UnsafeRawPointer + + public var result: Int32 + + public var error: Errno? // Convenience wrapper over `result` + + public var flags: Flags + } } ``` @@ -363,7 +362,7 @@ ring.prepare(linkedRequests: ) ) //batch submit 2 syscalls in 1! -try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming IOCompletion?, error, done) in +try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming Completion?, error, done) in if let error { throw error //or other error handling as desired } @@ -381,7 +380,7 @@ ring.prepare(linkedRequests: ) //batch submit 2 syscalls in 1! -try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming IOCompletion?, error, done) in +try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming Completion?, error, done) in if let error { throw error //or other error handling as desired } @@ -439,8 +438,8 @@ This feature is intrinsically linked to Linux kernel support, so constrains the ``` func submitLinkedRequestsAndWait( _ requests: repeat each Request -) where Request == IORequest - -> InlineArray<(repeat each Request).count, IOCompletion> +) where Request == IORing.Request + -> InlineArray<(repeat each Request).count, IORing.Completion> ``` * Once mutable borrows are supported, we should consider replacing the closure-taking bulk completion APIs (e.g. `blockingConsumeCompletions(…)`) with ones that return a sequence of completions instead * We should consider making more types noncopyable as compiler support improves @@ -457,7 +456,7 @@ func submitLinkedRequestsAndWait( * Using POSIX AIO instead of or as well as io_uring would greatly increase our ability to support older kernels and other Unix systems, but it has well-documented performance and usability issues that have prevented its adoption elsewhere, and apply just as much to Swift. * Earlier versions of this proposal had higher level "managed" abstractions over IORing. These have been removed due to lack of interest from clients, but could be added back later if needed. * I considered having dedicated error types for IORing, but eventually decided throwing Errno was more consistent with other platform APIs -* IOResource was originally a class in an attempt to manage the lifetime of the resource via language features. Changing to the current model of it being a copyable struct didn't make the lifetime management any less safe (the IORing still owns the actual resource), and reduces overhead. In the future it would be neat if we could express IOResources as being borrowed from the IORing so they can't be used after its lifetime. +* RegisteredResource was originally a class in an attempt to manage the lifetime of the resource via language features. Changing to the current model of it being a copyable struct didn't make the lifetime management any less safe (the IORing still owns the actual resource), and reduces overhead. In the future it would be neat if we could express RegisteredResources as being borrowed from the IORing so they can't be used after its lifetime. ## Acknowledgments From 5695a8831bcee664428fae077296ec6981d1e0f5 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Fri, 18 Apr 2025 14:30:21 -0700 Subject: [PATCH 085/172] Transparently add the \\?\ prefix to Win32 calls for extended length path handling On Windows, there is a built-in maximum path limitation of 260 characters under most conditions. This can be extended to 32767 characters under either of the following two conditions: - Adding the longPathAware attribute to the executable's manifest AND enabling the LongPathsEnabled system-wide registry key or group policy. - Ensuring fully qualified paths passed to Win32 APIs are prefixed with \\?\ Unfortunately, the former is not realistic for the Swift ecosystem, since it requires developers to have awareness of this specific Windows limitation, AND set longPathAware in their apps' manifest AND expect end users of those apps to change their system configuration. Instead, this patch transparently prefixes all eligible paths in calls to Win32 APIs with the \\?\ prefix to allow them to work with paths longer than 260 characters without requiring the caller of System to manually prefix the paths. See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more info. --- .../System/FilePath/FilePathTempWindows.swift | 8 +- Sources/System/FilePath/FilePathWindows.swift | 75 +++++++++++++++++++ .../Internals/WindowsSyscallAdapters.swift | 48 ++++++------ 3 files changed, 103 insertions(+), 28 deletions(-) diff --git a/Sources/System/FilePath/FilePathTempWindows.swift b/Sources/System/FilePath/FilePathTempWindows.swift index d6e45f4f..259aaafe 100644 --- a/Sources/System/FilePath/FilePathTempWindows.swift +++ b/Sources/System/FilePath/FilePathTempWindows.swift @@ -40,7 +40,7 @@ fileprivate func forEachFile( try searchPath.withPlatformString { szPath in var findData = WIN32_FIND_DATAW() - let hFind = FindFirstFileW(szPath, &findData) + let hFind = try szPath.withCanonicalPathRepresentation({ szPath in FindFirstFileW(szPath, &findData) }) if hFind == INVALID_HANDLE_VALUE { throw Errno(windowsError: GetLastError()) } @@ -95,8 +95,8 @@ internal func _recursiveRemove( let subpath = path.appending(component) if (findData.dwFileAttributes & DWORD(FILE_ATTRIBUTE_DIRECTORY)) == 0 { - try subpath.withPlatformString { - if !DeleteFileW($0) { + try subpath.withPlatformString { subpath in + if try !subpath.withCanonicalPathRepresentation({ DeleteFileW($0) }) { throw Errno(windowsError: GetLastError()) } } @@ -105,7 +105,7 @@ internal func _recursiveRemove( // Finally, delete the parent try path.withPlatformString { - if !RemoveDirectoryW($0) { + if try !$0.withCanonicalPathRepresentation({ RemoveDirectoryW($0) }) { throw Errno(windowsError: GetLastError()) } } diff --git a/Sources/System/FilePath/FilePathWindows.swift b/Sources/System/FilePath/FilePathWindows.swift index b725dd17..2226816e 100644 --- a/Sources/System/FilePath/FilePathWindows.swift +++ b/Sources/System/FilePath/FilePathWindows.swift @@ -461,3 +461,78 @@ extension SystemString { return lexer.current } } + +#if os(Windows) +import WinSDK + +// FIXME: Rather than canonicalizing the path at every call site to a Win32 API, +// we should consider always storing absolute paths with the \\?\ prefix applied, +// for better performance. +extension UnsafePointer where Pointee == CInterop.PlatformChar { + /// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee, + /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. + /// + /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + internal func withCanonicalPathRepresentation(_ body: (Self) throws -> Result) throws -> Result { + // 1. Normalize the path first. + // Contrary to the documentation, this works on long paths independently + // of the registry or process setting to enable long paths (but it will also + // not add the \\?\ prefix required by other functions under these conditions). + let dwLength: DWORD = GetFullPathNameW(self, 0, nil, nil) + return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in + guard (1.. DWORD { + DWORD(hr) & 0xffff +} + +@inline(__always) +fileprivate func HRESULT_FACILITY(_ hr: HRESULT) -> DWORD { + DWORD(hr << 16) & 0x1fff +} + +@inline(__always) +fileprivate func SUCCEEDED(_ hr: HRESULT) -> Bool { + hr >= 0 +} + +// This is a non-standard extension to the Windows SDK that allows us to convert +// an HRESULT to a Win32 error code. +@inline(__always) +fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD { + if SUCCEEDED(hr) { return DWORD(ERROR_SUCCESS) } + if HRESULT_FACILITY(hr) == FACILITY_WIN32 { + return HRESULT_CODE(hr) + } + return DWORD(hr) +} +#endif diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index 706881ec..6437d16a 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -35,17 +35,17 @@ internal func open( bInheritHandle: decodedFlags.bInheritHandle ) - let hFile = CreateFileW(path, - decodedFlags.dwDesiredAccess, - DWORD(FILE_SHARE_DELETE - | FILE_SHARE_READ - | FILE_SHARE_WRITE), - &saAttrs, - decodedFlags.dwCreationDisposition, - decodedFlags.dwFlagsAndAttributes, - nil) - - if hFile == INVALID_HANDLE_VALUE { + guard let hFile = try? path.withCanonicalPathRepresentation({ path in + CreateFileW(path, + decodedFlags.dwDesiredAccess, + DWORD(FILE_SHARE_DELETE + | FILE_SHARE_READ + | FILE_SHARE_WRITE), + &saAttrs, + decodedFlags.dwCreationDisposition, + decodedFlags.dwFlagsAndAttributes, + nil) + }), hFile != INVALID_HANDLE_VALUE else { ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) return -1 } @@ -77,17 +77,17 @@ internal func open( bInheritHandle: decodedFlags.bInheritHandle ) - let hFile = CreateFileW(path, - decodedFlags.dwDesiredAccess, - DWORD(FILE_SHARE_DELETE - | FILE_SHARE_READ - | FILE_SHARE_WRITE), - &saAttrs, - decodedFlags.dwCreationDisposition, - decodedFlags.dwFlagsAndAttributes, - nil) - - if hFile == INVALID_HANDLE_VALUE { + guard let hFile = try? path.withCanonicalPathRepresentation({ path in + CreateFileW(path, + decodedFlags.dwDesiredAccess, + DWORD(FILE_SHARE_DELETE + | FILE_SHARE_READ + | FILE_SHARE_WRITE), + &saAttrs, + decodedFlags.dwCreationDisposition, + decodedFlags.dwFlagsAndAttributes, + nil) + }), hFile != INVALID_HANDLE_VALUE else { ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) return -1 } @@ -242,7 +242,7 @@ internal func mkdir( bInheritHandle: false ) - if !CreateDirectoryW(path, &saAttrs) { + guard (try? path.withCanonicalPathRepresentation({ path in CreateDirectoryW(path, &saAttrs) })) == true else { ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) return -1 } @@ -254,7 +254,7 @@ internal func mkdir( internal func rmdir( _ path: UnsafePointer ) -> CInt { - if !RemoveDirectoryW(path) { + guard (try? path.withCanonicalPathRepresentation({ path in RemoveDirectoryW(path) })) == true else { ucrt._set_errno(_mapWindowsErrorToErrno(GetLastError())) return -1 } From 3a649e32a67625a06e183bbe68094056b28c690b Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 24 Apr 2025 16:39:41 -0700 Subject: [PATCH 086/172] Fix type of features, and make it clear that Completion is a nested type --- NNNN-swift-system-io-uring.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md index 37f0c5c9..d103578e 100644 --- a/NNNN-swift-system-io-uring.md +++ b/NNNN-swift-system-io-uring.md @@ -70,7 +70,7 @@ Already-completed results can be retrieved from the ring using `tryConsumeComple Since neither polling nor synchronously waiting is optimal in many cases, `IORing` also exposes the ability to register an eventfd (see `man eventfd(2)`), which will become readable when completions are available on the ring. This can then be monitored asynchronously with `epoll`, `kqueue`, or for clients who are linking libdispatch, `DispatchSource`. -`struct Completion: ~Copyable` represents the result of an IO operation and provides +`struct IORing.Completion: ~Copyable` represents the result of an IO operation and provides * Flags indicating various operation-specific metadata about the now-completed syscall * The context associated with the operation when it was enqueued, as an `UnsafeRawPointer` or a `UInt64` @@ -87,7 +87,7 @@ Unfortunately the underlying kernel API makes it relatively difficult to determi // IORing is intentionally not Sendable, to avoid internal locking overhead public struct IORing: ~Copyable { - public init(queueDepth: UInt32, flags: IORing.SetupFlags = []) throws(Errno) + public init(queueDepth: UInt32, flags: IORing.SetupFlags = []) throws(Errno) public struct SetupFlags: OptionSet, RawRepresentable, Hashable { public var rawValue: UInt32 @@ -164,21 +164,21 @@ public struct IORing: ~Copyable { public init(rawValue: UInt32) //IORING_FEAT_SINGLE_MMAP is handled internally - public static let nonDroppingCompletions: Bool //IORING_FEAT_NODROP - public static let stableSubmissions: Bool //IORING_FEAT_SUBMIT_STABLE - public static let currentFilePosition: Bool //IORING_FEAT_RW_CUR_POS - public static let assumingTaskCredentials: Bool //IORING_FEAT_CUR_PERSONALITY - public static let fastPolling: Bool //IORING_FEAT_FAST_POLL - public static let epoll32BitFlags: Bool //IORING_FEAT_POLL_32BITS - public static let pollNonFixedFiles: Bool //IORING_FEAT_SQPOLL_NONFIXED - public static let extendedArguments: Bool //IORING_FEAT_EXT_ARG - public static let nativeWorkers: Bool //IORING_FEAT_NATIVE_WORKERS - public static let resourceTags: Bool //IORING_FEAT_RSRC_TAGS - public static let allowsSkippingSuccessfulCompletions: Bool //IORING_FEAT_CQE_SKIP - public static let improvedLinkedFiles: Bool //IORING_FEAT_LINKED_FILE - public static let registerRegisteredRings: Bool //IORING_FEAT_REG_REG_RING - public static let minimumTimeout: Bool //IORING_FEAT_MIN_TIMEOUT - public static let bundledSendReceive: Bool //IORING_FEAT_RECVSEND_BUNDLE + public static let nonDroppingCompletions: Features //IORING_FEAT_NODROP + public static let stableSubmissions: Features //IORING_FEAT_SUBMIT_STABLE + public static let currentFilePosition: Features //IORING_FEAT_RW_CUR_POS + public static let assumingTaskCredentials: Features //IORING_FEAT_CUR_PERSONALITY + public static let fastPolling: Features //IORING_FEAT_FAST_POLL + public static let epoll32BitFlags: Features //IORING_FEAT_POLL_32BITS + public static let pollNonFixedFiles: Features //IORING_FEAT_SQPOLL_NONFIXED + public static let extendedArguments: Features //IORING_FEAT_EXT_ARG + public static let nativeWorkers: Features //IORING_FEAT_NATIVE_WORKERS + public static let resourceTags: Features //IORING_FEAT_RSRC_TAGS + public static let allowsSkippingSuccessfulCompletions: Features //IORING_FEAT_CQE_SKIP + public static let improvedLinkedFiles: Features //IORING_FEAT_LINKED_FILE + public static let registerRegisteredRings: Features //IORING_FEAT_REG_REG_RING + public static let minimumTimeout: Features //IORING_FEAT_MIN_TIMEOUT + public static let bundledSendReceive: Features //IORING_FEAT_RECVSEND_BUNDLE } public var supportedFeatures: Features } From 7e2f797fb497f820cbbf7a60ec4376fe20a215f5 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Apr 2025 22:30:00 +0000 Subject: [PATCH 087/172] Fix some errors on newer linux systems --- Sources/System/IORing.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 94c9504a..b4e633e0 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -466,7 +466,7 @@ public struct IORing: ~Copyable { var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, - pad: 0, + min_wait_usec: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) return try _blockingConsumeOneCompletion(extraArgs: &args) @@ -490,7 +490,7 @@ public struct IORing: ~Copyable { var args = io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, - pad: 0, + min_wait_usec: 0, ts: UInt64(UInt(bitPattern: tsPtr)) ) try _blockingConsumeCompletionGuts( @@ -530,7 +530,7 @@ public struct IORing: ~Copyable { let result = withUnsafePointer(to: &rawfd) { fdptr in let result = io_uring_register( ringDescriptor, - IORING_REGISTER_EVENTFD, + IORING_REGISTER_EVENTFD.rawValue, UnsafeMutableRawPointer(mutating: fdptr), 1 ) @@ -544,7 +544,7 @@ public struct IORing: ~Copyable { public mutating func unregisterEventFD() throws(Errno) { let result = io_uring_register( ringDescriptor, - IORING_UNREGISTER_EVENTFD, + IORING_UNREGISTER_EVENTFD.rawValue, nil, 0 ) @@ -561,7 +561,7 @@ public struct IORing: ~Copyable { let regResult = files.withUnsafeBufferPointer { bPtr in let result = io_uring_register( self.ringDescriptor, - IORING_REGISTER_FILES, + IORING_REGISTER_FILES.rawValue, UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: count) ) @@ -594,7 +594,7 @@ public struct IORing: ~Copyable { let regResult = iovecs.withUnsafeBufferPointer { bPtr in let result = io_uring_register( self.ringDescriptor, - IORING_REGISTER_BUFFERS, + IORING_REGISTER_BUFFERS.rawValue, UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: buffers.count) ) From 3d6cba22b69e3adac60528e69f759b2ff76dc166 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Apr 2025 23:03:26 +0000 Subject: [PATCH 088/172] Work around compiler bugs harder --- Sources/System/IORing.swift | 72 +++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index b4e633e0..5f54af91 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -164,7 +164,7 @@ internal func _getSubmissionEntry( } private func setUpRing( - queueDepth: UInt32, flags: IORing.SetupFlags, submissionRing: inout SQRing + queueDepth: UInt32, flags: IORing.SetupFlags ) throws(Errno) -> (params: io_uring_params, ringDescriptor: Int32, ringPtr: UnsafeMutableRawPointer, ringSize: Int, sqes: UnsafeMutableRawPointer) { var params = io_uring_params() @@ -217,36 +217,6 @@ private func setUpRing( throw errno } - let submissionRing = SQRing( - kernelHead: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.head) - .assumingMemoryBound(to: Atomic.self) - ), - kernelTail: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.tail) - .assumingMemoryBound(to: Atomic.self) - ), - userTail: 0, // no requests yet - ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) - .assumingMemoryBound(to: UInt32.self).pointee, - flags: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.flags) - .assumingMemoryBound(to: Atomic.self) - ), - array: UnsafeMutableBufferPointer( - start: ringPtr.advanced(by: params.sq_off.array) - .assumingMemoryBound(to: UInt32.self), - count: Int( - ringPtr.advanced(by: params.sq_off.ring_entries) - .assumingMemoryBound(to: UInt32.self).pointee) - ) - ) - - // fill submission ring array with 1:1 map to underlying SQEs - for i in 0..>( + ringPtr.advanced(by: params.sq_off.head) + .assumingMemoryBound(to: Atomic.self) + ), + kernelTail: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.tail) + .assumingMemoryBound(to: Atomic.self) + ), + userTail: 0, // no requests yet + ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) + .assumingMemoryBound(to: UInt32.self).pointee, + flags: UnsafePointer>( + ringPtr.advanced(by: params.sq_off.flags) + .assumingMemoryBound(to: Atomic.self) + ), + array: UnsafeMutableBufferPointer( + start: ringPtr.advanced(by: params.sq_off.array) + .assumingMemoryBound(to: UInt32.self), + count: Int( + ringPtr.advanced(by: params.sq_off.ring_entries) + .assumingMemoryBound(to: UInt32.self).pointee) + ) + ) + + // fill submission ring array with 1:1 map to underlying SQEs + for i in 0 ..< submissionRing.array.count { + submissionRing.array[i] = UInt32(i) + } submissionQueueEntries = UnsafeMutableBufferPointer( start: sqes.assumingMemoryBound(to: io_uring_sqe.self), @@ -641,12 +640,7 @@ public struct IORing: ~Copyable { } public func submitPreparedRequests() throws(Errno) { - switch submissionRing { - case .some(let submissionRing): - try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) - case .none: - fatalError() - } + try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } public func submitPreparedRequestsAndConsumeCompletions( From 595c862c19722fe1f544163c3ea9448f0e4be094 Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Wed, 21 May 2025 21:12:58 -0700 Subject: [PATCH 089/172] Fix Windows path parsing when the package is built in release configuration forceWindowsPaths can be used to force Windows path parsing both on and off. Instead of defaulting it to false in release builds, we should not express any preference --- Sources/System/Internals/Mocking.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/Internals/Mocking.swift b/Sources/System/Internals/Mocking.swift index 2945651c..ffdaaa92 100644 --- a/Sources/System/Internals/Mocking.swift +++ b/Sources/System/Internals/Mocking.swift @@ -130,7 +130,7 @@ internal var mockingEnabled: Bool { @inline(__always) internal var forceWindowsPaths: Bool? { #if !ENABLE_MOCKING - return false + return nil #else return MockingDriver.forceWindowsPaths #endif From f9c2a2b8e78c458c207b7e353ec0e43606d15162 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 18 Jun 2025 23:08:45 +0000 Subject: [PATCH 090/172] Work around io_uring.h in ways that will work on both versions --- Sources/CSystem/include/io_uring.h | 33 ++++++++++++++++++++++++++++++ Sources/System/IORing.swift | 12 +++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index 75a07e57..df1b36a8 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -34,6 +34,39 @@ # endif #endif +/* +struct io_uring_getevents_arg { + __u64 sigmask; + __u32 sigmask_sz; + __u32 min_wait_usec; //used to be called `pad`. This compatibility wrapper avoids dealing with that. + __u64 ts; +}; +*/ +struct swift_io_uring_getevents_arg { + __u64 sigmask; + __u32 sigmask_sz; + __u32 min_wait_usec; + __u64 ts; +}; + +//This was #defines in older headers, so we redeclare it to get a consistent import +typedef enum : __u32 { + SWIFT_IORING_REGISTER_BUFFERS = 0, + SWIFT_IORING_UNREGISTER_BUFFERS = 1, + SWIFT_IORING_REGISTER_FILES = 2, + SWIFT_IORING_UNREGISTER_FILES = 3, + SWIFT_IORING_REGISTER_EVENTFD = 4, + SWIFT_IORING_UNREGISTER_EVENTFD = 5, + SWIFT_IORING_REGISTER_FILES_UPDATE = 6, + SWIFT_IORING_REGISTER_EVENTFD_ASYNC = 7, + SWIFT_IORING_REGISTER_PROBE = 8, + SWIFT_IORING_REGISTER_PERSONALITY = 9, + SWIFT_IORING_UNREGISTER_PERSONALITY = 10, + + /* this goes last */ + SWIFT_IORING_REGISTER_LAST +} SWIFT_IORING_REGISTER_OPS; + static inline int io_uring_register(int fd, unsigned int opcode, void *arg, unsigned int nr_args) { diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 5f54af91..ce72aa7c 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -372,7 +372,7 @@ public struct IORing: ~Copyable { private func _blockingConsumeCompletionGuts( minimumCount: UInt32, maximumCount: UInt32, - extraArgs: UnsafeMutablePointer? = nil, + extraArgs: UnsafeMutablePointer? = nil, consumer: (consuming IORing.Completion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { var count = 0 @@ -393,7 +393,7 @@ public struct IORing: ~Copyable { while count < minimumCount { var sz = 0 if extraArgs != nil { - sz = MemoryLayout.size + sz = MemoryLayout.size } let res = io_uring_enter2( ringDescriptor, @@ -438,7 +438,7 @@ public struct IORing: ~Copyable { } internal func _blockingConsumeOneCompletion( - extraArgs: UnsafeMutablePointer? = nil + extraArgs: UnsafeMutablePointer? = nil ) throws(Errno) -> Completion { var result: Completion? = nil try _blockingConsumeCompletionGuts(minimumCount: 1, maximumCount: 1, extraArgs: extraArgs) { @@ -462,7 +462,7 @@ public struct IORing: ~Copyable { tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) return try withUnsafePointer(to: &ts) { (tsPtr) throws(Errno) -> Completion in - var args = io_uring_getevents_arg( + var args = swift_io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, min_wait_usec: 0, @@ -486,7 +486,7 @@ public struct IORing: ~Copyable { tv_nsec: timeout.components.attoseconds / 1_000_000_000 ) try withUnsafePointer(to: &ts) { (tsPtr) throws(Err) in - var args = io_uring_getevents_arg( + var args = swift_io_uring_getevents_arg( sigmask: 0, sigmask_sz: 0, min_wait_usec: 0, @@ -529,7 +529,7 @@ public struct IORing: ~Copyable { let result = withUnsafePointer(to: &rawfd) { fdptr in let result = io_uring_register( ringDescriptor, - IORING_REGISTER_EVENTFD.rawValue, + SWIFT_IORING_REGISTER_EVENTFD.rawValue, UnsafeMutableRawPointer(mutating: fdptr), 1 ) From f91cb6b8de316075a086504e688b81af6aa157dc Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 27 Jun 2025 00:39:35 +0000 Subject: [PATCH 091/172] Improve tests. Not quite working but close --- Tests/SystemTests/IORequestTests.swift | 57 +++++++++++--------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 3d1f72de..04100e01 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -1,9 +1,9 @@ import XCTest #if SYSTEM_PACKAGE -@testable import SystemPackage + @testable import SystemPackage #else -import System + import System #endif func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { @@ -27,37 +27,30 @@ final class IORequestTests: XCTestCase { XCTAssertEqual(sourceBytes, .init(repeating: 0, count: 64)) } - func testOpenatFixedFile() throws { - let pathPtr = UnsafePointer(bitPattern: 0x414141410badf00d)! - let fileSlot = IORing.RegisteredFile(resource: UInt32.max, index: 0) - let req = IORing.Request.open(FilePath(platformString: pathPtr), - in: FileDescriptor(rawValue: -100), - into: fileSlot, - mode: .readOnly, - options: [], - permissions: nil + func testOpenAndReadFixedFile() throws { + mkdir("/tmp/IORingTests/", 0o777) + let path: FilePath = "/tmp/IORingTests/test.txt" + let fd = try FileDescriptor.open( + path, .readWrite, options: .create, permissions: .ownerReadWrite) + try fd.writeAll("Hello, World!".utf8) + try fd.close() + var ring = try IORing(queueDepth: 3) + let parent = try FileDescriptor.open("/tmp/IORingTests/", .readOnly) + let fileSlot = try ring.registerFileSlots(count: 1)[0] + let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 64, alignment: 16) + let buffer = try ring.registerBuffers([rawBuffer])[0] + try ring.submit(linkedRequests: + .open(path, in: parent, into: fileSlot, mode: .readOnly), + .read(fileSlot, into: buffer), + .close(fileSlot) ) + _ = try ring.blockingConsumeCompletion() //open + _ = try ring.blockingConsumeCompletion() //read + _ = try ring.blockingConsumeCompletion() //close - let expectedRequest: [UInt8] = { - var bin = [UInt8].init(repeating: 0, count: 64) - bin[0] = 0x12 // opcode for the request - // bin[1] = 0 - no request flags - // bin[2...3] = 0 - padding - bin[4...7] = [0x9c, 0xff, 0xff, 0xff] // -100 in UInt32 - dirfd - // bin[8...15] = 0 - zeroes - withUnsafeBytes(of: pathPtr) { - // path pointer - bin[16...23] = ArraySlice($0) - } - // bin[24...43] = 0 - zeroes - withUnsafeBytes(of: UInt32(fileSlot.index + 1)) { - // file index + 1 - yes, unfortunately - bin[44...47] = ArraySlice($0) - } - return bin - }() - - let actualRequest = requestBytes(req.makeRawRequest()) - XCTAssertEqual(expectedRequest, actualRequest) + let result = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) + XCTAssertEqual(result, "Hello, World!") + + rmdir("/tmp/IORingTests/") } } From c0273af7e5543305327fe2d434072f35f9dcd272 Mon Sep 17 00:00:00 2001 From: Alex Azarov Date: Sat, 28 Jun 2025 17:46:44 +0200 Subject: [PATCH 092/172] Fix API documentation links in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fa266b5..c18ba496 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ try fd.closeAfter { } ``` -[API documentation](https://swiftpackageindex.com/apple/swift-system/main/documentation/SystemPackage) +[API documentation](https://swiftpackageindex.com/apple/swift-system/documentation/SystemPackage) ## Adding `SystemPackage` as a Dependency From 131326c8aedf6e32d9084075cc67cbed0271c538 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 1 Jul 2025 01:02:40 +0000 Subject: [PATCH 093/172] Fix tests --- Tests/SystemTests/IORequestTests.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 04100e01..d22b527a 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -7,7 +7,7 @@ import XCTest #endif func requestBytes(_ request: consuming RawIORequest) -> [UInt8] { - return withUnsafePointer(to: request) { + return withUnsafePointer(to: request.rawValue) { let requestBuf = UnsafeBufferPointer(start: $0, count: 1) let rawBytes = UnsafeRawBufferPointer(requestBuf) return .init(rawBytes) @@ -31,26 +31,32 @@ final class IORequestTests: XCTestCase { mkdir("/tmp/IORingTests/", 0o777) let path: FilePath = "/tmp/IORingTests/test.txt" let fd = try FileDescriptor.open( - path, .readWrite, options: .create, permissions: .ownerReadWrite) + path, + .readWrite, + options: .create, + permissions: .ownerReadWrite + ) try fd.writeAll("Hello, World!".utf8) try fd.close() - var ring = try IORing(queueDepth: 3) + var ring = try IORing(queueDepth: 6) let parent = try FileDescriptor.open("/tmp/IORingTests/", .readOnly) let fileSlot = try ring.registerFileSlots(count: 1)[0] - let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 64, alignment: 16) + let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) let buffer = try ring.registerBuffers([rawBuffer])[0] + try ring.submit(linkedRequests: .open(path, in: parent, into: fileSlot, mode: .readOnly), .read(fileSlot, into: buffer), - .close(fileSlot) - ) + .close(fileSlot)) _ = try ring.blockingConsumeCompletion() //open _ = try ring.blockingConsumeCompletion() //read _ = try ring.blockingConsumeCompletion() //close + let result = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) XCTAssertEqual(result, "Hello, World!") + try parent.close() rmdir("/tmp/IORingTests/") } } From 321a71c2e999932b983516918254bffcfde912ea Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 3 Jul 2025 00:13:29 +0000 Subject: [PATCH 094/172] Expand test coverage --- Tests/SystemTests/IORequestTests.swift | 33 ----------- Tests/SystemTests/IORingTests.swift | 80 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index d22b527a..0e5c9724 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -26,37 +26,4 @@ final class IORequestTests: XCTestCase { // we're not trying to be bug-compatible with it, so 0 *should* work. XCTAssertEqual(sourceBytes, .init(repeating: 0, count: 64)) } - - func testOpenAndReadFixedFile() throws { - mkdir("/tmp/IORingTests/", 0o777) - let path: FilePath = "/tmp/IORingTests/test.txt" - let fd = try FileDescriptor.open( - path, - .readWrite, - options: .create, - permissions: .ownerReadWrite - ) - try fd.writeAll("Hello, World!".utf8) - try fd.close() - var ring = try IORing(queueDepth: 6) - let parent = try FileDescriptor.open("/tmp/IORingTests/", .readOnly) - let fileSlot = try ring.registerFileSlots(count: 1)[0] - let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) - let buffer = try ring.registerBuffers([rawBuffer])[0] - - try ring.submit(linkedRequests: - .open(path, in: parent, into: fileSlot, mode: .readOnly), - .read(fileSlot, into: buffer), - .close(fileSlot)) - _ = try ring.blockingConsumeCompletion() //open - _ = try ring.blockingConsumeCompletion() //read - _ = try ring.blockingConsumeCompletion() //close - - - let result = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) - XCTAssertEqual(result, "Hello, World!") - - try parent.close() - rmdir("/tmp/IORingTests/") - } } diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 6c086da6..8b32b92d 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -1,4 +1,5 @@ import XCTest +import CSystem //for eventfd #if SYSTEM_PACKAGE import SystemPackage @@ -17,4 +18,83 @@ final class IORingTests: XCTestCase { let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) } + + func makeHelloWorldFile() throws -> (dir: FileDescriptor, file: FilePath) { + mkdir("/tmp/IORingTests/", 0o777) + let path: FilePath = "/tmp/IORingTests/test.txt" + let fd = try FileDescriptor.open( + path, + .readWrite, + options: .create, + permissions: .ownerReadWrite + ) + try fd.writeAll("Hello, World!".utf8) + try fd.close() + let parent = try FileDescriptor.open("/tmp/IORingTests/", .readOnly) + + return (parent, path) + } + + func cleanUpHelloWorldFile(_ parent: FileDescriptor) throws { + try parent.close() + rmdir("/tmp/IORingTests/") + } + + func setupTestRing(depth: Int, fileSlots: Int, buffers: [UnsafeMutableRawBufferPointer]) throws -> IORing { + var ring: IORing = try IORing(queueDepth: 6) + _ = try ring.registerFileSlots(count: 1) + _ = try ring.registerBuffers(buffers) + return ring + } + + // Exercises opening, reading, closing, registered files, registered buffers, and eventfd + func testOpenReadAndWriteFixedFile() throws { + let (parent, path) = try makeHelloWorldFile() + let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) + var ring = try setupTestRing(depth: 3, fileSlots: 1, buffers: [rawBuffer]) + let eventFD = FileDescriptor(rawValue: eventfd(0, Int32(EFD_SEMAPHORE))) + try ring.registerEventFD(eventFD) + + //Part 1: read the file we just created, and make sure the eventfd fires + try ring.submit(linkedRequests: + .open(path, in: parent, into: ring.registeredFileSlots[0], mode: .readOnly), + .read(ring.registeredFileSlots[0], into: ring.registeredBuffers[0]), + .close(ring.registeredFileSlots[0])) + let efdBuf = UnsafeMutableRawBufferPointer.allocate(byteCount: 8, alignment: 0) + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //open + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //read + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //close + let result = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) + XCTAssertEqual(result, "Hello, World!") + + //Part 2: delete that file, then use the ring to write out a new one + let rmResult = path.withPlatformString { + remove($0) + } + XCTAssertEqual(rmResult, 0) + try ring.submit(linkedRequests: + .open(path, in: parent, into: ring.registeredFileSlots[0], mode: .readWrite, options: .create, permissions: .ownerReadWrite), + .write(ring.registeredBuffers[0], into: ring.registeredFileSlots[0]), + .close(ring.registeredFileSlots[0])) + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //open + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //write + _ = try eventFD.read(into: efdBuf) + _ = try ring.blockingConsumeCompletion() //close + memset(rawBuffer.baseAddress!, 0, rawBuffer.count) + //Verify using a non-ring IO method that what we wrote matches our expectations + print("about to open") + let nonRingFD = try FileDescriptor.open(path, .readOnly) + let bytesRead = try nonRingFD.read(into: rawBuffer) + XCTAssert(bytesRead == 13) + let result2 = String(cString: rawBuffer.assumingMemoryBound(to: CChar.self).baseAddress!) + XCTAssertEqual(result2, "Hello, World!") + try cleanUpHelloWorldFile(parent) + efdBuf.deallocate() + rawBuffer.deallocate() + } } From 27ccce55db6615e69056424c747b4b330391b0e0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 3 Jul 2025 16:04:52 -0700 Subject: [PATCH 095/172] Conditionalize IORing stuff for Linux --- Sources/System/IOCompletion.swift | 2 ++ Sources/System/IORequest.swift | 2 ++ Sources/System/IORing.swift | 2 ++ Sources/System/RawIORequest.swift | 4 +++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 5fa29325..ed0feab5 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -1,3 +1,4 @@ +#if os(Linux) @_implementationOnly import CSystem public extension IORing { @@ -56,3 +57,4 @@ public extension IORing.Completion { } } } +#endif diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 3c4d2a27..35741bd7 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,3 +1,4 @@ +#if os(Linux) @_implementationOnly import struct CSystem.io_uring_sqe @usableFromInline @@ -471,3 +472,4 @@ extension IORing.Request { return request } } +#endif diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index ce72aa7c..581e7950 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,3 +1,4 @@ +#if os(Linux) @_implementationOnly import CSystem import Glibc // needed for mmap import Synchronization @@ -742,3 +743,4 @@ extension IORing.RegisteredBuffer { return .init(start: resource.iov_base, count: resource.iov_len) } } +#endif diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 6190b0da..33bea295 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -1,3 +1,4 @@ +#if os(Linux) // TODO: investigate @usableFromInline / @_implementationOnly dichotomy @_implementationOnly import CSystem @_implementationOnly import struct CSystem.io_uring_sqe @@ -196,4 +197,5 @@ extension RawIORequest { return try work() } } -} \ No newline at end of file +} +#endif From 0d4c6fc97147e14c0291d51bab68253b59260e7c Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 3 Jul 2025 16:12:56 -0700 Subject: [PATCH 096/172] Fix Musl --- Sources/System/IORing.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 581e7950..0af574d5 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,6 +1,11 @@ #if os(Linux) @_implementationOnly import CSystem -import Glibc // needed for mmap +// needed for mmap +#if canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif import Synchronization @_implementationOnly import struct CSystem.io_uring_sqe From de2d6e2d391368ac3db87b99f1ea9dc2d6e45a3e Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 3 Jul 2025 16:17:51 -0700 Subject: [PATCH 097/172] Conditionalize tests too --- Tests/SystemTests/IORequestTests.swift | 2 ++ Tests/SystemTests/IORingTests.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 0e5c9724..4d32196a 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -1,3 +1,4 @@ +#if os(Linux) import XCTest #if SYSTEM_PACKAGE @@ -27,3 +28,4 @@ final class IORequestTests: XCTestCase { XCTAssertEqual(sourceBytes, .init(repeating: 0, count: 64)) } } +#endif diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 8b32b92d..29973c63 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -1,3 +1,4 @@ +#if os(Linux) import XCTest import CSystem //for eventfd @@ -98,3 +99,4 @@ final class IORingTests: XCTestCase { rawBuffer.deallocate() } } +#endif From a6149c215ef7f0b0937fe2ca9091f043ce44f65a Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 4 Jul 2025 00:09:19 +0000 Subject: [PATCH 098/172] Cleanup, implement buffer unregistering, inline almost everything because we're not ABI stable on Linux --- Sources/System/IOCompletion.swift | 19 +++--- Sources/System/IORequest.swift | 81 ++++++++++++------------- Sources/System/IORing.swift | 99 +++++++++++++++++++------------ Sources/System/RawIORequest.swift | 69 ++++++++++----------- 4 files changed, 144 insertions(+), 124 deletions(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index ed0feab5..4263bd8d 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -1,9 +1,12 @@ #if os(Linux) -@_implementationOnly import CSystem +import CSystem public extension IORing { struct Completion: ~Copyable { - let rawValue: io_uring_cqe + @inlinable init(rawValue inRawValue: io_uring_cqe) { + rawValue = inRawValue + } + @usableFromInline let rawValue: io_uring_cqe } } @@ -11,7 +14,7 @@ public extension IORing.Completion { struct Flags: OptionSet, Hashable, Codable { public let rawValue: UInt32 - public init(rawValue: UInt32) { + @inlinable public init(rawValue: UInt32) { self.rawValue = rawValue } @@ -23,31 +26,31 @@ public extension IORing.Completion { } public extension IORing.Completion { - var context: UInt64 { + @inlinable var context: UInt64 { get { rawValue.user_data } } - var userPointer: UnsafeRawPointer? { + @inlinable var userPointer: UnsafeRawPointer? { get { UnsafeRawPointer(bitPattern: UInt(rawValue.user_data)) } } - var result: Int32 { + @inlinable var result: Int32 { get { rawValue.res } } - var flags: IORing.Completion.Flags { + @inlinable var flags: IORing.Completion.Flags { get { Flags(rawValue: rawValue.flags & 0x0000FFFF) } } - var bufferIndex: UInt16? { + @inlinable var bufferIndex: UInt16? { get { if self.flags.contains(.allocatedBuffer) { return UInt16(rawValue.flags >> 16) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 35741bd7..8af313a3 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,5 +1,5 @@ #if os(Linux) -@_implementationOnly import struct CSystem.io_uring_sqe +import CSystem @usableFromInline internal enum IORequestCore { @@ -96,7 +96,7 @@ internal enum IORequestCore { ) } -@inline(__always) +@inline(__always) @inlinable internal func makeRawRequest_readWrite_registered( file: FileDescriptor, buffer: IORing.RegisteredBuffer, @@ -112,7 +112,7 @@ internal func makeRawRequest_readWrite_registered( return request } -@inline(__always) +@inline(__always) @inlinable internal func makeRawRequest_readWrite_registered_slot( file: IORing.RegisteredFile, buffer: IORing.RegisteredBuffer, @@ -129,7 +129,7 @@ internal func makeRawRequest_readWrite_registered_slot( return request } -@inline(__always) +@inline(__always) @inlinable internal func makeRawRequest_readWrite_unregistered( file: FileDescriptor, buffer: UnsafeMutableRawBufferPointer, @@ -144,7 +144,7 @@ internal func makeRawRequest_readWrite_unregistered( return request } -@inline(__always) +@inline(__always) @inlinable internal func makeRawRequest_readWrite_unregistered_slot( file: IORing.RegisteredFile, buffer: UnsafeMutableRawBufferPointer, @@ -164,6 +164,10 @@ extension IORing { public struct Request { @usableFromInline var core: IORequestCore + @inlinable internal init(core inCore: IORequestCore) { + core = inCore + } + @inlinable internal consuming func extractCore() -> IORequestCore { return core } @@ -173,11 +177,11 @@ extension IORing { extension IORing.Request { - public static func nop(context: UInt64 = 0) -> IORing.Request { + @inlinable public static func nop(context: UInt64 = 0) -> IORing.Request { .init(core: .nop) } - public static func read( + @inlinable public static func read( _ file: IORing.RegisteredFile, into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, @@ -186,7 +190,7 @@ extension IORing.Request { .init(core: .readSlot(file: file, buffer: buffer, offset: offset, context: context)) } - public static func read( + @inlinable public static func read( _ file: FileDescriptor, into buffer: IORing.RegisteredBuffer, at offset: UInt64 = 0, @@ -195,7 +199,7 @@ extension IORing.Request { .init(core: .read(file: file, buffer: buffer, offset: offset, context: context)) } - public static func read( + @inlinable public static func read( _ file: IORing.RegisteredFile, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, @@ -204,7 +208,7 @@ extension IORing.Request { .init(core: .readUnregisteredSlot(file: file, buffer: buffer, offset: offset, context: context)) } - public static func read( + @inlinable public static func read( _ file: FileDescriptor, into buffer: UnsafeMutableRawBufferPointer, at offset: UInt64 = 0, @@ -213,7 +217,7 @@ extension IORing.Request { .init(core: .readUnregistered(file: file, buffer: buffer, offset: offset, context: context)) } - public static func write( + @inlinable public static func write( _ buffer: IORing.RegisteredBuffer, into file: IORing.RegisteredFile, at offset: UInt64 = 0, @@ -222,7 +226,7 @@ extension IORing.Request { .init(core: .writeSlot(file: file, buffer: buffer, offset: offset, context: context)) } - public static func write( + @inlinable public static func write( _ buffer: IORing.RegisteredBuffer, into file: FileDescriptor, at offset: UInt64 = 0, @@ -231,7 +235,7 @@ extension IORing.Request { .init(core: .write(file: file, buffer: buffer, offset: offset, context: context)) } - public static func write( + @inlinable public static func write( _ buffer: UnsafeMutableRawBufferPointer, into file: IORing.RegisteredFile, at offset: UInt64 = 0, @@ -241,7 +245,7 @@ extension IORing.Request { file: file, buffer: buffer, offset: offset, context: context)) } - public static func write( + @inlinable public static func write( _ buffer: UnsafeMutableRawBufferPointer, into file: FileDescriptor, at offset: UInt64 = 0, @@ -252,21 +256,21 @@ extension IORing.Request { ) } - public static func close( + @inlinable public static func close( _ file: FileDescriptor, context: UInt64 = 0 ) -> IORing.Request { .init(core: .close(file, context: context)) } - public static func close( + @inlinable public static func close( _ file: IORing.RegisteredFile, context: UInt64 = 0 ) -> IORing.Request { .init(core: .closeSlot(file, context: context)) } - public static func open( + @inlinable public static func open( _ path: FilePath, in directory: FileDescriptor, into slot: IORing.RegisteredFile, @@ -281,7 +285,7 @@ extension IORing.Request { permissions: permissions, intoSlot: slot, context: context)) } - public static func open( + @inlinable public static func open( _ path: FilePath, in directory: FileDescriptor, mode: FileDescriptor.AccessMode, @@ -296,7 +300,7 @@ extension IORing.Request { )) } - public static func unlink( + @inlinable public static func unlink( _ path: FilePath, in directory: FileDescriptor, context: UInt64 = 0 @@ -307,31 +311,22 @@ 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 cancelation 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 - */ - //TODO: why aren't these showing up from the header import? - private static var IORING_ASYNC_CANCEL_ALL: UInt32 { (1 as UInt32) << 0 } - private static var IORING_ASYNC_CANCEL_FD: UInt32 { (1 as UInt32) << 1 } - private static var IORING_ASYNC_CANCEL_ANY: UInt32 { (1 as UInt32) << 2 } - private static var IORING_ASYNC_CANCEL_FD_FIXED: UInt32 { (1 as UInt32) << 3 } - private static var IORING_ASYNC_CANCEL_USERDATA: UInt32 { (1 as UInt32) << 4 } - private static var IORING_ASYNC_CANCEL_OP: UInt32 { (1 as UInt32) << 5 } - - + * ASYNC_CANCEL flags. + * + * IORING_ASYNC_CANCEL_ALL Cancel all requests that match the given key + * IORING_ASYNC_CANCEL_FD Key off 'fd' for cancelation 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 + */ public enum CancellationMatch { case all case first } - public static func cancel( + @inlinable public static func cancel( _ matchAll: CancellationMatch, matchingContext: UInt64, ) -> IORing.Request { @@ -343,7 +338,7 @@ extension IORing.Request { } } - public static func cancel( + @inlinable public static func cancel( _ matchAll: CancellationMatch, matching: FileDescriptor, ) -> IORing.Request { @@ -355,7 +350,7 @@ extension IORing.Request { } } - public static func cancel( + @inlinable public static func cancel( _ matchAll: CancellationMatch, matching: IORing.RegisteredFile, ) -> IORing.Request { @@ -369,8 +364,8 @@ extension IORing.Request { //TODO: add support for CANCEL_OP - @inline(__always) - public consuming func makeRawRequest() -> RawIORequest { + @inline(__always) @inlinable + internal consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() switch extractCore() { case .nop: diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 581e7950..6e31a42b 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,11 +1,8 @@ #if os(Linux) -@_implementationOnly import CSystem +import CSystem import Glibc // needed for mmap import Synchronization -@_implementationOnly import struct CSystem.io_uring_sqe - -// XXX: this *really* shouldn't be here. oh well. extension UnsafeMutableRawPointer { func advanced(by offset: UInt32) -> UnsafeMutableRawPointer { return advanced(by: Int(offset)) @@ -20,40 +17,40 @@ extension UnsafeMutableRawBufferPointer { // all pointers in this struct reference kernel-visible memory @usableFromInline struct SQRing: ~Copyable { - let kernelHead: UnsafePointer> - let kernelTail: UnsafePointer> - var userTail: UInt32 + @usableFromInline let kernelHead: UnsafePointer> + @usableFromInline let kernelTail: UnsafePointer> + @usableFromInline var userTail: UInt32 // from liburing: the kernel should never change these // might change in the future with resizable rings? - let ringMask: UInt32 + @usableFromInline let ringMask: UInt32 // let ringEntries: UInt32 - absorbed into array.count // ring flags bitfield // currently used by the kernel only in SQPOLL mode to indicate // when the polling thread needs to be woken up - let flags: UnsafePointer> + @usableFromInline let flags: UnsafePointer> // ring array // maps indexes between the actual ring and the submissionQueueEntries list, // allowing the latter to be used as a kind of freelist with enough work? // currently, just 1:1 mapping (0.. + @usableFromInline let array: UnsafeMutableBufferPointer } -struct CQRing: ~Copyable { - let kernelHead: UnsafePointer> - let kernelTail: UnsafePointer> +@usableFromInline struct CQRing: ~Copyable { + @usableFromInline let kernelHead: UnsafePointer> + @usableFromInline let kernelTail: UnsafePointer> // TODO: determine if this is actually used var userHead: UInt32 - let ringMask: UInt32 + @usableFromInline let ringMask: UInt32 - let cqes: UnsafeBufferPointer + @usableFromInline let cqes: UnsafeBufferPointer } -@inline(__always) +@inline(__always) @inlinable internal func _writeRequest( _ request: __owned RawIORequest, ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer @@ -66,7 +63,7 @@ internal func _writeRequest( return true } -@inline(__always) +@inline(__always) @inlinable internal func _blockingGetSubmissionEntry( ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer ) -> UnsafeMutablePointer< @@ -86,6 +83,7 @@ internal func _blockingGetSubmissionEntry( //TODO: omitting signal mask for now //Tell the kernel that we've submitted requests and/or are waiting for completions +@inlinable internal func _enter( ringDescriptor: Int32, numEvents: UInt32, @@ -113,22 +111,26 @@ internal func _enter( } } +@inlinable internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws(Errno) { let flushedEvents = _flushQueue(ring: ring) _ = try _enter( ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) } +@inlinable internal func _getUnconsumedSubmissionCount(ring: borrowing SQRing) -> UInt32 { return ring.userTail - ring.kernelHead.pointee.load(ordering: .acquiring) } +@inlinable internal func _getUnconsumedCompletionCount(ring: borrowing CQRing) -> UInt32 { return ring.kernelTail.pointee.load(ordering: .acquiring) - ring.kernelHead.pointee.load(ordering: .acquiring) } //TODO: pretty sure this is supposed to do more than it does +@inlinable internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { ring.kernelTail.pointee.store( ring.userTail, ordering: .releasing @@ -136,7 +138,7 @@ internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { return _getUnconsumedSubmissionCount(ring: ring) } -@inline(__always) +@inlinable internal func _getSubmissionEntry( ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer ) -> UnsafeMutablePointer< @@ -188,7 +190,6 @@ private func setUpRing( || params.features & IORING_FEAT_NODROP == 0 { close(ringDescriptor) - // TODO: error handling throw Errno.invalidArgument } @@ -241,22 +242,22 @@ private func setUpRing( public struct IORing: ~Copyable { let ringFlags: UInt32 - let ringDescriptor: Int32 + @usableFromInline let ringDescriptor: Int32 @usableFromInline var submissionRing: SQRing // FEAT: set this eventually let submissionPolling: Bool = false - let completionRing: CQRing + @usableFromInline let completionRing: CQRing - let submissionQueueEntries: UnsafeMutableBufferPointer + @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer // kept around for unmap / cleanup let ringSize: Int let ringPtr: UnsafeMutableRawPointer - var _registeredFiles: [UInt32] - var _registeredBuffers: [iovec] + @usableFromInline var _registeredFiles: [UInt32] + @usableFromInline var _registeredBuffers: [iovec] var features = Features(rawValue: 0) @@ -265,7 +266,7 @@ public struct IORing: ~Copyable { @usableFromInline let resource: T @usableFromInline let index: Int - internal init( + @inlinable internal init( resource: T, index: Int ) { @@ -277,7 +278,6 @@ public struct IORing: ~Copyable { public typealias RegisteredFile = RegisteredResource public typealias RegisteredBuffer = RegisteredResource - @frozen public struct SetupFlags: OptionSet, RawRepresentable, Hashable { public var rawValue: UInt32 @@ -370,7 +370,8 @@ public struct IORing: ~Copyable { self.ringFlags = params.flags } - private func _blockingConsumeCompletionGuts( + @inlinable + internal func _blockingConsumeCompletionGuts( minimumCount: UInt32, maximumCount: UInt32, extraArgs: UnsafeMutablePointer? = nil, @@ -438,6 +439,7 @@ public struct IORing: ~Copyable { } } + @inlinable internal func _blockingConsumeOneCompletion( extraArgs: UnsafeMutablePointer? = nil ) throws(Errno) -> Completion { @@ -454,6 +456,7 @@ public struct IORing: ~Copyable { return result.take()! } + @inlinable public func blockingConsumeCompletion( timeout: Duration? = nil ) throws(Errno) -> Completion { @@ -476,6 +479,7 @@ public struct IORing: ~Copyable { } } + @inlinable public func blockingConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, @@ -507,10 +511,12 @@ public struct IORing: ~Copyable { // } + @inlinable public func tryConsumeCompletion() -> Completion? { return _tryConsumeCompletion(ring: completionRing) } + @inlinable func _tryConsumeCompletion(ring: borrowing CQRing) -> Completion? { let tail = ring.kernelTail.pointee.load(ordering: .acquiring) let head = ring.kernelHead.pointee.load(ordering: .acquiring) @@ -580,6 +586,7 @@ public struct IORing: ~Copyable { fatalError("failed to unregister files") } + @inlinable public var registeredFileSlots: RegisteredResources { RegisteredResources(resources: _registeredFiles) } @@ -610,6 +617,7 @@ public struct IORing: ~Copyable { return registeredBuffers } + @inlinable public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(Errno) -> RegisteredResources { @@ -617,33 +625,44 @@ public struct IORing: ~Copyable { } public struct RegisteredResources: RandomAccessCollection { - let resources: [T] + @usableFromInline let resources: [T] - public var startIndex: Int { 0 } - public var endIndex: Int { resources.endIndex } - init(resources: [T]) { + @inlinable public var startIndex: Int { 0 } + @inlinable public var endIndex: Int { resources.endIndex } + @inlinable init(resources: [T]) { self.resources = resources } - public subscript(position: Int) -> RegisteredResource { + @inlinable public subscript(position: Int) -> RegisteredResource { RegisteredResource(resource: resources[position], index: position) } - public subscript(position: UInt16) -> RegisteredResource { + @inlinable public subscript(position: UInt16) -> RegisteredResource { RegisteredResource(resource: resources[Int(position)], index: Int(position)) } } + @inlinable public var registeredBuffers: RegisteredResources { RegisteredResources(resources: _registeredBuffers) } - public func unregisterBuffers() { - fatalError("failed to unregister buffers: TODO") + public func unregisterBuffers() throws { + let result = io_uring_register( + self.ringDescriptor, + IORING_UNREGISTER_BUFFERS.rawValue, + nil, + 0 + ) + guard result >= 0 else { + throw Errno(rawValue: -result) + } } + @inlinable public func submitPreparedRequests() throws(Errno) { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } + @inlinable public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, timeout: Duration? = nil, @@ -662,12 +681,14 @@ public struct IORing: ~Copyable { ) } + @inlinable public mutating func prepare(request: __owned Request) -> Bool { var raw: RawIORequest? = request.makeRawRequest() return _writeRequest( raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } + @inlinable mutating func prepare(linkedRequests: some BidirectionalCollection) { guard linkedRequests.count > 0 else { return @@ -684,17 +705,17 @@ public struct IORing: ~Copyable { submissionQueueEntries: submissionQueueEntries) } - //@inlinable //TODO: make sure the array allocation gets optimized out... + @inlinable public mutating func prepare(linkedRequests: Request...) { prepare(linkedRequests: linkedRequests) } + @inlinable public mutating func submit(linkedRequests: Request...) throws(Errno) { prepare(linkedRequests: linkedRequests) try submitPreparedRequests() } - @frozen public struct Features: OptionSet, RawRepresentable, Hashable { public let rawValue: UInt32 @@ -734,12 +755,12 @@ public struct IORing: ~Copyable { } extension IORing.RegisteredFile { - public var unsafeFileSlot: Int { + @inlinable public var unsafeFileSlot: Int { return index } } extension IORing.RegisteredBuffer { - public var unsafeBuffer: UnsafeMutableRawBufferPointer { + @inlinable public var unsafeBuffer: UnsafeMutableRawBufferPointer { return .init(start: resource.iov_base, count: resource.iov_len) } } diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 33bea295..e4d0f474 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -1,19 +1,19 @@ #if os(Linux) -// TODO: investigate @usableFromInline / @_implementationOnly dichotomy -@_implementationOnly import CSystem -@_implementationOnly import struct CSystem.io_uring_sqe +import CSystem +import struct CSystem.io_uring_sqe -//TODO: make this internal -public struct RawIORequest: ~Copyable { - var rawValue: io_uring_sqe - var path: FilePath? //buffer owner for the path pointer that the sqe may have +@usableFromInline +internal struct RawIORequest: ~Copyable { + @usableFromInline var rawValue: io_uring_sqe + @usableFromInline var path: FilePath? //buffer owner for the path pointer that the sqe may have - public init() { + @inlinable public init() { self.rawValue = io_uring_sqe() } } extension RawIORequest { + @usableFromInline enum Operation: UInt8 { case nop = 0 case readv = 1 @@ -45,49 +45,49 @@ extension RawIORequest { public struct Flags: OptionSet, Hashable, Codable { public let rawValue: UInt8 - public init(rawValue: UInt8) { + @inlinable public init(rawValue: UInt8) { self.rawValue = rawValue } - public static let fixedFile = Flags(rawValue: 1 << 0) - public static let drainQueue = Flags(rawValue: 1 << 1) - public static let linkRequest = Flags(rawValue: 1 << 2) - public static let hardlinkRequest = Flags(rawValue: 1 << 3) - public static let asynchronous = Flags(rawValue: 1 << 4) - public static let selectBuffer = Flags(rawValue: 1 << 5) - public static let skipSuccess = Flags(rawValue: 1 << 6) + @inlinable public static var fixedFile: RawIORequest.Flags { Flags(rawValue: 1 << 0) } + @inlinable public static var drainQueue: RawIORequest.Flags { Flags(rawValue: 1 << 1) } + @inlinable public static var linkRequest: RawIORequest.Flags { Flags(rawValue: 1 << 2) } + @inlinable public static var hardlinkRequest: RawIORequest.Flags { Flags(rawValue: 1 << 3) } + @inlinable public static var asynchronous: RawIORequest.Flags { Flags(rawValue: 1 << 4) } + @inlinable public static var selectBuffer: RawIORequest.Flags { Flags(rawValue: 1 << 5) } + @inlinable public static var skipSuccess: RawIORequest.Flags { Flags(rawValue: 1 << 6) } } - var operation: Operation { + @inlinable var operation: Operation { get { Operation(rawValue: rawValue.opcode)! } set { rawValue.opcode = newValue.rawValue } } - var cancel_flags: UInt32 { + @inlinable var cancel_flags: UInt32 { get { rawValue.cancel_flags } set { rawValue.cancel_flags = newValue } } - var addr: UInt64 { + @inlinable var addr: UInt64 { get { rawValue.addr } set { rawValue.addr = newValue } } - public var flags: Flags { + @inlinable public var flags: Flags { get { Flags(rawValue: rawValue.flags) } set { rawValue.flags = newValue.rawValue } } - public mutating func linkToNextRequest() { + @inlinable public mutating func linkToNextRequest() { flags = Flags(rawValue: flags.rawValue | Flags.linkRequest.rawValue) } - public var fileDescriptor: FileDescriptor { + @inlinable public var fileDescriptor: FileDescriptor { get { FileDescriptor(rawValue: rawValue.fd) } set { rawValue.fd = newValue.rawValue } } - public var offset: UInt64? { + @inlinable public var offset: UInt64? { get { if (rawValue.off == UInt64.max) { return nil @@ -104,7 +104,7 @@ extension RawIORequest { } } - public var buffer: UnsafeMutableRawBufferPointer { + @inlinable public var buffer: UnsafeMutableRawBufferPointer { get { let ptr = UnsafeMutableRawPointer(bitPattern: UInt(exactly: rawValue.addr)!) return UnsafeMutableRawBufferPointer(start: ptr, count: Int(rawValue.len)) @@ -135,45 +135,46 @@ extension RawIORequest { public struct ReadWriteFlags: OptionSet, Hashable, Codable { public var rawValue: UInt32 - public init(rawValue: UInt32) { + @inlinable public init(rawValue: UInt32) { self.rawValue = rawValue } - public static let highPriority = ReadWriteFlags(rawValue: 1 << 0) + @inlinable public static var highPriority: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 0) } // sync with only data integrity - public static let dataSync = ReadWriteFlags(rawValue: 1 << 1) + @inlinable public static var dataSync: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 1) } // sync with full data + file integrity - public static let fileSync = ReadWriteFlags(rawValue: 1 << 2) + @inlinable public static var fileSync: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 2) } // return -EAGAIN if operation blocks - public static let noWait = ReadWriteFlags(rawValue: 1 << 3) + @inlinable public static var noWait: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 3) } // append to end of the file - public static let append = ReadWriteFlags(rawValue: 1 << 4) + @inlinable public static var append: RawIORequest.ReadWriteFlags { ReadWriteFlags(rawValue: 1 << 4) } } public struct TimeOutFlags: OptionSet, Hashable, Codable { public var rawValue: UInt32 - public init(rawValue: UInt32) { + @inlinable public init(rawValue: UInt32) { self.rawValue = rawValue } - public static let relativeTime: RawIORequest.TimeOutFlags = TimeOutFlags(rawValue: 0) - public static let absoluteTime: RawIORequest.TimeOutFlags = TimeOutFlags(rawValue: 1 << 0) + @inlinable public static var relativeTime: RawIORequest.TimeOutFlags { TimeOutFlags(rawValue: 0) } + @inlinable public static var absoluteTime: RawIORequest.TimeOutFlags { TimeOutFlags(rawValue: 1 << 0) } } } extension RawIORequest { + @inlinable static func nop() -> RawIORequest { var req: RawIORequest = RawIORequest() req.operation = .nop return req } - //TODO: typed errors + @inlinable static func withTimeoutRequest( linkedTo opEntry: UnsafeMutablePointer, in timeoutEntry: UnsafeMutablePointer, From bbe2f1c192eedf9ee1019264fb27173446ac2624 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 00:41:50 +0000 Subject: [PATCH 099/172] Add doc comments, Span/MutableSpan support, unregistering registered fds, variants of submission operations that don't block, address various TODOs --- Package.swift | 1 + Sources/System/IORequest.swift | 2 - Sources/System/IORing.swift | 140 ++++++++++++++++++++++++------ Sources/System/RawIORequest.swift | 2 - 4 files changed, 116 insertions(+), 29 deletions(-) diff --git a/Package.swift b/Package.swift index 14b040e7..48b348af 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ let swiftSettings: [SwiftSetting] = [ .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .visionOS])), .define("SYSTEM_PACKAGE"), .define("ENABLE_MOCKING", .when(configuration: .debug)), + .enableExperimentalFeature("Lifetimes"), ] let package = Package( diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 8af313a3..1d0638b4 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -362,8 +362,6 @@ extension IORing.Request { } } - //TODO: add support for CANCEL_OP - @inline(__always) @inlinable internal consuming func makeRawRequest() -> RawIORequest { var request = RawIORequest() diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 15d9cd79..681ca7e8 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -47,25 +47,35 @@ extension UnsafeMutableRawBufferPointer { @usableFromInline let kernelHead: UnsafePointer> @usableFromInline let kernelTail: UnsafePointer> - // TODO: determine if this is actually used - var userHead: UInt32 - @usableFromInline let ringMask: UInt32 @usableFromInline let cqes: UnsafeBufferPointer } @inline(__always) @inlinable -internal func _writeRequest( +internal func _tryWriteRequest( _ request: __owned RawIORequest, ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer ) -> Bool +{ + if let entry = _getSubmissionEntry( + ring: &ring, submissionQueueEntries: submissionQueueEntries) { + entry.pointee = request.rawValue + return true + } + return false +} + +@inline(__always) @inlinable +internal func _writeRequest( + _ request: __owned RawIORequest, ring: inout SQRing, + submissionQueueEntries: UnsafeMutableBufferPointer +) { let entry = _blockingGetSubmissionEntry( ring: &ring, submissionQueueEntries: submissionQueueEntries) entry.pointee = request.rawValue - return true } @inline(__always) @inlinable @@ -151,8 +161,7 @@ internal func _getSubmissionEntry( >? { let next = ring.userTail &+ 1 //this is expected to wrap - // FEAT: smp load when SQPOLL in use (not in MVP) - let kernelHead = ring.kernelHead.pointee.load(ordering: .acquiring) + let kernelHead: UInt32 = ring.kernelHead.pointee.load(ordering: .acquiring) // FEAT: 128-bit event support (not in MVP) if next - kernelHead <= ring.array.count { @@ -245,6 +254,11 @@ private func setUpRing( return (params: params, ringDescriptor: ringDescriptor, ringPtr: ringPtr!, ringSize: ringSize, sqes: sqes!) } +///IORing provides facilities for +/// * Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file IOdescriptors that improves their efficiency +/// * Registering and unregistering eventfds, which allow asynchronous waiting for completions +/// * Enqueueing IO requests +/// * Dequeueing IO completions public struct IORing: ~Copyable { let ringFlags: UInt32 @usableFromInline let ringDescriptor: Int32 @@ -266,10 +280,11 @@ public struct IORing: ~Copyable { var features = Features(rawValue: 0) + /// RegisteredResource is used via its typealiases, RegisteredFile and RegisteredBuffer. Registering file descriptors and buffers with the IORing allows for more efficient access to them. public struct RegisteredResource { public typealias Resource = T @usableFromInline let resource: T - @usableFromInline let index: Int + public let index: Int @inlinable internal init( resource: T, @@ -283,6 +298,7 @@ public struct IORing: ~Copyable { public typealias RegisteredFile = RegisteredResource public typealias RegisteredBuffer = RegisteredResource + /// SetupFlags represents configuration options to an IORing as it's being created public struct SetupFlags: OptionSet, RawRepresentable, Hashable { public var rawValue: UInt32 @@ -298,14 +314,15 @@ public struct IORing: ~Copyable { //TODO: do we want to expose IORING_SETUP_COOP_TASKRUN and IORING_SETUP_TASKRUN_FLAG? //public static var runTasksCooperatively: SetupFlags { .init(rawValue: UInt32(1) << 8) } //IORING_SETUP_COOP_TASKRUN //TODO: can we even do different size sqe/cqe? It requires a kernel feature, but how do we convince swift to let the types be different sizes? - internal static var use128ByteSQEs: SetupFlags { .init(rawValue: UInt32(1) << 10) } //IORING_SETUP_SQE128 - internal static var use32ByteCQEs: SetupFlags { .init(rawValue: UInt32(1) << 11) } //IORING_SETUP_CQE32 + //internal static var use128ByteSQEs: SetupFlags { .init(rawValue: UInt32(1) << 10) } //IORING_SETUP_SQE128 + //internal static var use32ByteCQEs: SetupFlags { .init(rawValue: UInt32(1) << 11) } //IORING_SETUP_CQE32 @inlinable public static var singleSubmissionThread: SetupFlags { .init(rawValue: UInt32(1) << 12) } //IORING_SETUP_SINGLE_ISSUER @inlinable public static var deferRunningTasks: SetupFlags { .init(rawValue: UInt32(1) << 13) } //IORING_SETUP_DEFER_TASKRUN //pretty sure we don't want to expose IORING_SETUP_NO_MMAP or IORING_SETUP_REGISTERED_FD_ONLY currently //TODO: should IORING_SETUP_NO_SQARRAY be the default? do we need to adapt anything to it? } + /// Initializes an IORing with enough space for `queueDepth` prepared requests and completed operations public init(queueDepth: UInt32, flags: SetupFlags = []) throws(Errno) { let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags) // All throws need to be before initializing ivars here to avoid @@ -361,7 +378,6 @@ public struct IORing: ~Copyable { ringPtr.advanced(by: params.cq_off.tail) .assumingMemoryBound(to: Atomic.self) ), - userHead: 0, // no completions yet ringMask: ringPtr.advanced(by: params.cq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, cqes: UnsafeBufferPointer( @@ -461,6 +477,7 @@ public struct IORing: ~Copyable { return result.take()! } + /// Synchronously waits for an operation to complete for up to `timeout` (or forever if not specified) @inlinable public func blockingConsumeCompletion( timeout: Duration? = nil @@ -484,6 +501,7 @@ public struct IORing: ~Copyable { } } + /// Synchronously waits for `minimumCount` or more operations to complete for up to `timeout` (or forever if not specified). For each completed operation found, `consumer` is called to handle processing it @inlinable public func blockingConsumeCompletions( minimumCount: UInt32 = 1, @@ -516,6 +534,7 @@ public struct IORing: ~Copyable { // } + /// Takes a completed operation from the ring and returns it, if one is ready. Otherwise, returns nil. @inlinable public func tryConsumeCompletion() -> Completion? { return _tryConsumeCompletion(ring: completionRing) @@ -536,6 +555,7 @@ public struct IORing: ~Copyable { return nil } + /// Registers an event monitoring file descriptor with the ring. The file descriptor becomes readable whenever completions are ready to be dequeued. See `man eventfd(2)` for additional information. public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) { var rawfd = descriptor.rawValue let result = withUnsafePointer(to: &rawfd) { fdptr in @@ -552,6 +572,7 @@ public struct IORing: ~Copyable { } } + /// Removes a registered event file descriptor from the ring public mutating func unregisterEventFD() throws(Errno) { let result = io_uring_register( ringDescriptor, @@ -564,6 +585,7 @@ public struct IORing: ~Copyable { } } + /// Registers `count` files with the ring for later use in IO operations public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources { precondition(_registeredFiles.isEmpty) precondition(count < UInt32.max) @@ -587,21 +609,31 @@ public struct IORing: ~Copyable { return registeredFileSlots } - public func unregisterFiles() { - fatalError("failed to unregister files") + /// Removes registered files from the ring + public func unregisterFiles() throws { + let result = io_uring_register( + ringDescriptor, + IORING_UNREGISTER_FILES.rawValue, + nil, + 0 + ) + if result < 0 { + throw Errno(rawValue: -result) + } } + /// Allows access to registered files by index @inlinable public var registeredFileSlots: RegisteredResources { RegisteredResources(resources: _registeredFiles) } + /// Registers buffers with the ring for later use in IO operations public mutating func registerBuffers(_ buffers: some Collection) throws(Errno) -> RegisteredResources { precondition(buffers.count < UInt32.max) precondition(_registeredBuffers.isEmpty) - //TODO: check if io_uring has preconditions it needs for the buffers (e.g. alignment) let iovecs = buffers.map { $0.to_iovec() } let regResult = iovecs.withUnsafeBufferPointer { bPtr in let result = io_uring_register( @@ -617,11 +649,11 @@ public struct IORing: ~Copyable { throw regResult } - // TODO: error handling _registeredBuffers = iovecs return registeredBuffers } + /// Registers buffers with the ring for later use in IO operations @inlinable public mutating func registerBuffers(_ buffers: UnsafeMutableRawBufferPointer...) throws(Errno) -> RegisteredResources @@ -629,6 +661,7 @@ public struct IORing: ~Copyable { try registerBuffers(buffers) } + /// A view of the registered files or buffers in a ring public struct RegisteredResources: RandomAccessCollection { @usableFromInline let resources: [T] @@ -645,6 +678,7 @@ public struct IORing: ~Copyable { } } + /// Allows access to registered files by index @inlinable public var registeredBuffers: RegisteredResources { RegisteredResources(resources: _registeredBuffers) @@ -662,11 +696,13 @@ public struct IORing: ~Copyable { } } + /// Sends all prepared requests to the kernel for processing. Results will be delivered as completions, which can be dequeued from the ring. @inlinable public func submitPreparedRequests() throws(Errno) { try _submitRequests(ring: submissionRing, ringDescriptor: ringDescriptor) } + /// Sends all prepared requests to the kernel for processing, and then dequeues at least `minimumCount` completions, waiting up to `timeout` for them to become available. `consumer` is called to process each completed IO operation as it becomes available. @inlinable public func submitPreparedRequestsAndConsumeCompletions( minimumCount: UInt32 = 1, @@ -686,13 +722,48 @@ public struct IORing: ~Copyable { ) } + /// Attempts to prepare an IO request for submission to the kernel. Returns false if no space is available to enqueue the request @inlinable - public mutating func prepare(request: __owned Request) -> Bool { + public mutating func tryPrepare(request: __owned Request) -> Bool { var raw: RawIORequest? = request.makeRawRequest() - return _writeRequest( + return _tryWriteRequest( raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } + /// Attempts to prepare an IO request for submission to the kernel. Blocks if needed until space becomes available to enqueue the request + @inlinable + public mutating func prepare(request: __owned Request) { + var raw: RawIORequest? = request.makeRawRequest() + _writeRequest( + raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + } + + /// Attempts to prepare a chain of linked IO requests for submission to the kernel. Returns false if not enough space is available to enqueue the request. If any linked operation fails, subsequent operations will be canceled. Linked operations always execute in order. + @inlinable + mutating func tryPrepare(linkedRequests: some BidirectionalCollection) -> Bool { + guard linkedRequests.count > 0 else { + return true + } + let freeSQECount = _getUnconsumedSubmissionCount(ring: submissionRing) + guard freeSQECount >= linkedRequests.count else { + return false + } + let last = linkedRequests.last! + for req in linkedRequests.dropLast() { + var raw = req.makeRawRequest() + raw.linkToNextRequest() + let successfullyAdded = _tryWriteRequest( + raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) + assert(successfullyAdded) + } + let successfullyAdded = _tryWriteRequest( + last.makeRawRequest(), ring: &submissionRing, + submissionQueueEntries: submissionQueueEntries) + assert(successfullyAdded) + return true + } + + /// Prepares a chain of linked IO requests for submission to the kernel. Blocks if needed until space becomes available to enqueue the requests. If any linked operation fails, subsequent operations will be canceled. Linked operations always execute in order. @inlinable mutating func prepare(linkedRequests: some BidirectionalCollection) { guard linkedRequests.count > 0 else { @@ -702,25 +773,34 @@ public struct IORing: ~Copyable { for req in linkedRequests.dropLast() { var raw = req.makeRawRequest() raw.linkToNextRequest() - _ = _writeRequest( + _writeRequest( raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } - _ = _writeRequest( + _writeRequest( last.makeRawRequest(), ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } + /// Prepares a sequence of requests for submission to the ring. Returns false if the submission queue doesn't have enough available space. + @inlinable + public mutating func tryPrepare(linkedRequests: Request...) -> Bool { + tryPrepare(linkedRequests: linkedRequests) + } + + /// Prepares a sequence of requests for submission to the ring. Blocks if the submission queue doesn't have enough available space. @inlinable public mutating func prepare(linkedRequests: Request...) { prepare(linkedRequests: linkedRequests) } + /// Prepares and submits a sequence of requests to the ring. Blocks if the submission queue doesn't have enough available space. @inlinable public mutating func submit(linkedRequests: Request...) throws(Errno) { prepare(linkedRequests: linkedRequests) try submitPreparedRequests() } + /// Describes which io_uring features are supported by the kernel this program is running on public struct Features: OptionSet, RawRepresentable, Hashable { public let rawValue: UInt32 @@ -745,6 +825,8 @@ public struct IORing: ~Copyable { @inlinable public static var minimumTimeout: Features { .init(rawValue: UInt32(1) << 15) } //IORING_FEAT_MIN_TIMEOUT @inlinable public static var bundledSendReceive: Features { .init(rawValue: UInt32(1) << 14) } //IORING_FEAT_RECVSEND_BUNDLE } + + /// Describes which io_uring features are supported by the kernel this program is running on public var supportedFeatures: Features { return features } @@ -759,14 +841,22 @@ public struct IORing: ~Copyable { } } -extension IORing.RegisteredFile { - @inlinable public var unsafeFileSlot: Int { - return index - } -} extension IORing.RegisteredBuffer { - @inlinable public var unsafeBuffer: UnsafeMutableRawBufferPointer { + @unsafe @inlinable public var unsafeBuffer: UnsafeMutableRawBufferPointer { return .init(start: resource.iov_base, count: resource.iov_len) } + + @inlinable public var mutableBytes: MutableRawSpan { + @_lifetime(&self) + mutating get { + let span = MutableRawSpan(_unsafeBytes: unsafeBuffer) + return unsafe _overrideLifetime(span, mutating: &self) + } + } + + @inlinable public var bytes: RawSpan { + let span = RawSpan(_unsafeBytes: UnsafeRawBufferPointer(unsafeBuffer)) + return unsafe _overrideLifetime(span, borrowing: self) + } } #endif diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index e4d0f474..3734760f 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -1,6 +1,5 @@ #if os(Linux) import CSystem -import struct CSystem.io_uring_sqe @usableFromInline internal struct RawIORequest: ~Copyable { @@ -111,7 +110,6 @@ extension RawIORequest { } set { - // TODO: cleanup? rawValue.addr = UInt64(Int(bitPattern: newValue.baseAddress!)) rawValue.len = UInt32(exactly: newValue.count)! } From 227a38f230f5ad94df7fa1fc72e6b24f1e8b9056 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 22:41:06 +0000 Subject: [PATCH 100/172] Remove blocking enqueue --- Sources/System/IORing.swift | 47 ++++++----------------------- Tests/SystemTests/IORingTests.swift | 8 +++-- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 681ca7e8..64515c3f 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -724,23 +724,15 @@ public struct IORing: ~Copyable { /// Attempts to prepare an IO request for submission to the kernel. Returns false if no space is available to enqueue the request @inlinable - public mutating func tryPrepare(request: __owned Request) -> Bool { + public mutating func prepare(request: __owned Request) -> Bool { var raw: RawIORequest? = request.makeRawRequest() return _tryWriteRequest( raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) } - /// Attempts to prepare an IO request for submission to the kernel. Blocks if needed until space becomes available to enqueue the request - @inlinable - public mutating func prepare(request: __owned Request) { - var raw: RawIORequest? = request.makeRawRequest() - _writeRequest( - raw.take()!, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) - } - /// Attempts to prepare a chain of linked IO requests for submission to the kernel. Returns false if not enough space is available to enqueue the request. If any linked operation fails, subsequent operations will be canceled. Linked operations always execute in order. @inlinable - mutating func tryPrepare(linkedRequests: some BidirectionalCollection) -> Bool { + mutating func prepare(linkedRequests: some BidirectionalCollection) -> Bool { guard linkedRequests.count > 0 else { return true } @@ -763,41 +755,20 @@ public struct IORing: ~Copyable { return true } - /// Prepares a chain of linked IO requests for submission to the kernel. Blocks if needed until space becomes available to enqueue the requests. If any linked operation fails, subsequent operations will be canceled. Linked operations always execute in order. - @inlinable - mutating func prepare(linkedRequests: some BidirectionalCollection) { - guard linkedRequests.count > 0 else { - return - } - let last = linkedRequests.last! - for req in linkedRequests.dropLast() { - var raw = req.makeRawRequest() - raw.linkToNextRequest() - _writeRequest( - raw, ring: &submissionRing, submissionQueueEntries: submissionQueueEntries) - } - _writeRequest( - last.makeRawRequest(), ring: &submissionRing, - submissionQueueEntries: submissionQueueEntries) - } - /// Prepares a sequence of requests for submission to the ring. Returns false if the submission queue doesn't have enough available space. @inlinable - public mutating func tryPrepare(linkedRequests: Request...) -> Bool { - tryPrepare(linkedRequests: linkedRequests) - } - - /// Prepares a sequence of requests for submission to the ring. Blocks if the submission queue doesn't have enough available space. - @inlinable - public mutating func prepare(linkedRequests: Request...) { + public mutating func prepare(linkedRequests: Request...) -> Bool { prepare(linkedRequests: linkedRequests) } - /// Prepares and submits a sequence of requests to the ring. Blocks if the submission queue doesn't have enough available space. + /// Prepares and submits a sequence of requests to the ring. Returns false if the submission queue doesn't have enough available space. @inlinable - public mutating func submit(linkedRequests: Request...) throws(Errno) { - prepare(linkedRequests: linkedRequests) + public mutating func submit(linkedRequests: Request...) throws(Errno) -> Bool { + if !prepare(linkedRequests: linkedRequests) { + return false + } try submitPreparedRequests() + return true } /// Describes which io_uring features are supported by the kernel this program is running on diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 29973c63..1297bf2d 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -15,7 +15,7 @@ final class IORingTests: XCTestCase { func testNop() throws { var ring = try IORing(queueDepth: 32, flags: []) - try ring.submit(linkedRequests: .nop()) + _ = try ring.submit(linkedRequests: .nop()) let completion = try ring.blockingConsumeCompletion() XCTAssertEqual(completion.result, 0) } @@ -57,10 +57,11 @@ final class IORingTests: XCTestCase { try ring.registerEventFD(eventFD) //Part 1: read the file we just created, and make sure the eventfd fires - try ring.submit(linkedRequests: + let enqueued = try ring.submit(linkedRequests: .open(path, in: parent, into: ring.registeredFileSlots[0], mode: .readOnly), .read(ring.registeredFileSlots[0], into: ring.registeredBuffers[0]), .close(ring.registeredFileSlots[0])) + XCTAssert(enqueued) let efdBuf = UnsafeMutableRawBufferPointer.allocate(byteCount: 8, alignment: 0) _ = try eventFD.read(into: efdBuf) _ = try ring.blockingConsumeCompletion() //open @@ -76,10 +77,11 @@ final class IORingTests: XCTestCase { remove($0) } XCTAssertEqual(rmResult, 0) - try ring.submit(linkedRequests: + let enqueued2 = try ring.submit(linkedRequests: .open(path, in: parent, into: ring.registeredFileSlots[0], mode: .readWrite, options: .create, permissions: .ownerReadWrite), .write(ring.registeredBuffers[0], into: ring.registeredFileSlots[0]), .close(ring.registeredFileSlots[0])) + XCTAssert(enqueued2) _ = try eventFD.read(into: efdBuf) _ = try ring.blockingConsumeCompletion() //open _ = try eventFD.read(into: efdBuf) From bc097e91dc7efe4c79324ec970aace5ed7b63ddd Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 22:42:15 +0000 Subject: [PATCH 101/172] Remove more blocking enqueue infrastructure --- Sources/System/IORing.swift | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 64515c3f..5c483734 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -67,35 +67,6 @@ internal func _tryWriteRequest( return false } -@inline(__always) @inlinable -internal func _writeRequest( - _ request: __owned RawIORequest, ring: inout SQRing, - submissionQueueEntries: UnsafeMutableBufferPointer -) -{ - let entry = _blockingGetSubmissionEntry( - ring: &ring, submissionQueueEntries: submissionQueueEntries) - entry.pointee = request.rawValue -} - -@inline(__always) @inlinable -internal func _blockingGetSubmissionEntry( - ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer -) -> UnsafeMutablePointer< - io_uring_sqe -> { - while true { - if let entry = _getSubmissionEntry( - ring: &ring, - submissionQueueEntries: submissionQueueEntries - ) { - return entry - } - // TODO: actually block here instead of spinning - } - -} - //TODO: omitting signal mask for now //Tell the kernel that we've submitted requests and/or are waiting for completions @inlinable From 55e55c197880d5f4422937d59d537e8266560f4a Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 23:15:17 +0000 Subject: [PATCH 102/172] Add test and fix submission queue capacity check --- Sources/System/IORing.swift | 12 ++++++++---- Tests/SystemTests/IORingTests.swift | 10 ++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 5c483734..4a28f7c8 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -105,23 +105,27 @@ internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) thr } @inlinable -internal func _getUnconsumedSubmissionCount(ring: borrowing SQRing) -> UInt32 { +internal func _getSubmissionQueueCount(ring: borrowing SQRing) -> UInt32 { return ring.userTail - ring.kernelHead.pointee.load(ordering: .acquiring) } +@inlinable +internal func _getRemainingSubmissionQueueCapacity(ring: borrowing SQRing) -> UInt32 { + return UInt32(truncatingIfNeeded: ring.array.count) - _getSubmissionQueueCount(ring: ring) +} + @inlinable internal func _getUnconsumedCompletionCount(ring: borrowing CQRing) -> UInt32 { return ring.kernelTail.pointee.load(ordering: .acquiring) - ring.kernelHead.pointee.load(ordering: .acquiring) } -//TODO: pretty sure this is supposed to do more than it does @inlinable internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { ring.kernelTail.pointee.store( ring.userTail, ordering: .releasing ) - return _getUnconsumedSubmissionCount(ring: ring) + return _getSubmissionQueueCount(ring: ring) } @inlinable @@ -707,7 +711,7 @@ public struct IORing: ~Copyable { guard linkedRequests.count > 0 else { return true } - let freeSQECount = _getUnconsumedSubmissionCount(ring: submissionRing) + let freeSQECount = _getRemainingSubmissionQueueCapacity(ring: submissionRing) guard freeSQECount >= linkedRequests.count else { return false } diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 1297bf2d..757866ad 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -42,17 +42,23 @@ final class IORingTests: XCTestCase { } func setupTestRing(depth: Int, fileSlots: Int, buffers: [UnsafeMutableRawBufferPointer]) throws -> IORing { - var ring: IORing = try IORing(queueDepth: 6) + var ring: IORing = try IORing(queueDepth: UInt32(depth)) _ = try ring.registerFileSlots(count: 1) _ = try ring.registerBuffers(buffers) return ring } + func testUndersizedSubmissionQueue() throws { + var ring: IORing = try IORing(queueDepth: 1) + let enqueued = ring.prepare(linkedRequests: .nop(), .nop()) + XCTAssertFalse(enqueued) + } + // Exercises opening, reading, closing, registered files, registered buffers, and eventfd func testOpenReadAndWriteFixedFile() throws { let (parent, path) = try makeHelloWorldFile() let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) - var ring = try setupTestRing(depth: 3, fileSlots: 1, buffers: [rawBuffer]) + var ring = try setupTestRing(depth: 6, fileSlots: 1, buffers: [rawBuffer]) let eventFD = FileDescriptor(rawValue: eventfd(0, Int32(EFD_SEMAPHORE))) try ring.registerEventFD(eventFD) From a6c5a39ca53ec58d4d7cf5ebbd98e32cfcdd1185 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 16:24:20 -0700 Subject: [PATCH 103/172] Remove the proposal doc --- NNNN-swift-system-io-uring.md | 463 ---------------------------------- 1 file changed, 463 deletions(-) delete mode 100644 NNNN-swift-system-io-uring.md diff --git a/NNNN-swift-system-io-uring.md b/NNNN-swift-system-io-uring.md deleted file mode 100644 index d103578e..00000000 --- a/NNNN-swift-system-io-uring.md +++ /dev/null @@ -1,463 +0,0 @@ -# IORing, a Swift System API for io_uring - -* Proposal: [SE-NNNN](NNNN-filename.md) -* Authors: [Lucy Satheesan](https://github.com/oxy), [David Smith](https://github.com/Catfish-Man/) -* Review Manager: TBD -* Status: **Awaiting implementation** -* Implementation: [apple/swift-system#208](https://github.com/apple/swift-system/pull/208) - -## Introduction - -`io_uring` is Linux's solution to asynchronous and batched syscalls, with a particular focus on IO. We propose a low-level Swift API for it in Swift System that could either be used directly by projects with unusual needs, or via intermediaries like Swift NIO, to address scalability and thread pool starvation issues. - -## Motivation - -Up until recently, the overwhelmingly dominant file IO syscalls on major Unix platforms have been synchronous, e.g. `read(2)`. This design is very simple and proved sufficient for many uses for decades, but is less than ideal for Swift's needs in a few major ways: - -1. Requiring an entire OS thread for each concurrent operation imposes significant memory overhead -2. Requiring a separate syscall for each operation imposes significant CPU/time overhead to switch into and out of kernel mode repeatedly. This has been exacerbated in recent years by mitigations for the Meltdown family of security exploits increasing the cost of syscalls. -3. Swift's N:M coroutine-on-thread-pool concurrency model assumes that threads will not be blocked. Each thread waiting for a syscall means a CPU core being left idle. In practice systems like NIO that deal in highly concurrent IO have had to work around this by providing their own thread pools. - -Non-file IO (network, pipes, etc…) has been in a somewhat better place with `epoll` and `kqueue` for asynchronously waiting for readability, but syscall overhead remains a significant issue for highly scalable systems. - -With the introduction of `io_uring` in 2019, Linux now has the kernel level tools to address these three problems directly. However, `io_uring` is quite complex and maps poorly into Swift. We expect that by providing a Swift interface to it, we can enable Swift on Linux servers to scale better and be more efficient than it has been in the past. - -## Proposed solution - -We propose a *low level, unopinionated* Swift interface for io_uring on Linux (see Future Directions for discussion of possible more abstract interfaces). - -`struct IORing: ~Copyable` provides facilities for - -* Registering and unregistering resources (files and buffers), an `io_uring` specific variation on Unix file IOdescriptors that improves their efficiency -* Registering and unregistering eventfds, which allow asynchronous waiting for completions -* Enqueueing IO requests -* Dequeueing IO completions - -`struct IORing.RegisteredResource` represents, via its two typealiases `IORing.RegisteredFile` and `IORing.RegisteredBuffer`, registered file descriptors and buffers. - -`struct IORing.Request: ~Copyable` represents an IO operation that can be enqueued for the kernel to execute. It supports a wide variety of operations matching traditional unix file and socket operations. - -Request operations are expressed as overloaded static methods on `Request`, e.g. `openat` is spelled - -```swift - public static func open( - _ path: FilePath, - in directory: FileDescriptor, - into slot: IORing.RegisteredFile, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - context: UInt64 = 0 - ) -> Request - - public static func open( - _ path: FilePath, - in directory: FileDescriptor, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - context: UInt64 = 0 - ) -> Request -``` - -which allows clients to decide whether they want to open the file into a slot on the ring, or have it return a file descriptor via a completion. Similarly, read operations have overloads for "use a buffer from the ring" or "read into this `UnsafeMutableBufferPointer`" - -Multiple `Requests` can be enqueued on a single `IORing` using the `prepare(…)` family of methods, and then submitted together using `submitPreparedRequests`, allowing for things like "open this file, read its contents, and then close it" to be a single syscall. Conveniences are provided for preparing and submitting requests in one call. - -Since IO operations can execute in parallel or out of order by default, linked chains of operations can be established with `prepare(linkedRequests:…)` and related methods. Separate chains can still execute in parallel, and if an operation early in the chain fails, all subsequent operations will deliver cancellation errors as their completion. - -Already-completed results can be retrieved from the ring using `tryConsumeCompletion`, which never waits but may return nil, or `blockingConsumeCompletion(timeout:)`, which synchronously waits (up to an optional timeout) until an operation completes. There's also a bulk version of `blockingConsumeCompletion`, which may reduce the number of syscalls issued. It takes a closure which will be called repeatedly as completions are available (see Future Directions for potential improvements to this API). - -Since neither polling nor synchronously waiting is optimal in many cases, `IORing` also exposes the ability to register an eventfd (see `man eventfd(2)`), which will become readable when completions are available on the ring. This can then be monitored asynchronously with `epoll`, `kqueue`, or for clients who are linking libdispatch, `DispatchSource`. - -`struct IORing.Completion: ~Copyable` represents the result of an IO operation and provides - -* Flags indicating various operation-specific metadata about the now-completed syscall -* The context associated with the operation when it was enqueued, as an `UnsafeRawPointer` or a `UInt64` -* The result of the operation, as an `Int32` with operation-specific meaning -* The error, if one occurred - -Unfortunately the underlying kernel API makes it relatively difficult to determine which `Request` led to a given `Completion`, so it's expected that users will need to create this association themselves via the context parameter. - -`IORing.Features` describes the supported features of the underlying kernel `IORing` implementation, which can be used to provide graceful reduction in functionality when running on older systems. - -## Detailed design - -```swift -// IORing is intentionally not Sendable, to avoid internal locking overhead -public struct IORing: ~Copyable { - - public init(queueDepth: UInt32, flags: IORing.SetupFlags = []) throws(Errno) - - public struct SetupFlags: OptionSet, RawRepresentable, Hashable { - public var rawValue: UInt32 - public init(rawValue: UInt32) - public static var pollCompletions: SetupFlags //IORING_SETUP_IOPOLL - public static var pollSubmissions: SetupFlags //IORING_SETUP_SQPOLL - public static var clampMaxEntries: SetupFlags //IORING_SETUP_CLAMP - public static var startDisabled: SetupFlags //IORING_SETUP_R_DISABLED - public static var continueSubmittingOnError: SetupFlags //IORING_SETUP_SUBMIT_ALL - public static var singleSubmissionThread: SetupFlags //IORING_SETUP_SINGLE_ISSUER - public static var deferRunningTasks: SetupFlags //IORING_SETUP_DEFER_TASKRUN - } - - public mutating func registerEventFD(_ descriptor: FileDescriptor) throws(Errno) - public mutating func unregisterEventFD() throws(Errno) - - public struct RegisteredResource { } - public typealias RegisteredFile = RegisteredResource - public typealias RegisteredBuffer = RegisteredResource - - // A `RegisteredResources` is a view into the buffers or files registered with the ring, if any - public struct RegisteredResources: RandomAccessCollection { - public subscript(position: Int) -> RegisteredResource - public subscript(position: UInt16) -> RegisteredResource // This is useful because io_uring likes to use UInt16s as indexes - } - - public mutating func registerFileSlots(count: Int) throws(Errno) -> RegisteredResources - public func unregisterFiles() - public var registeredFileSlots: RegisteredResources - - public mutating func registerBuffers( - _ buffers: some Collection - ) throws(Errno) -> RegisteredResources - - public mutating func registerBuffers( - _ buffers: UnsafeMutableRawBufferPointer... - ) throws(Errno) -> RegisteredResources - - public func unregisterBuffers() - - public var registeredBuffers: RegisteredResources - - public func prepare(requests: Request...) - public func prepare(linkedRequests: Request...) - - public func submitPreparedRequests(timeout: Duration? = nil) throws(Errno) - public func submit(requests: Request..., timeout: Duration? = nil) throws(Errno) - public func submit(linkedRequests: Request..., timeout: Duration? = nil) throws(Errno) - - public func submitPreparedRequests() throws(Errno) - public func submitPreparedRequestsAndWait(timeout: Duration? = nil) throws(Errno) - - public func submitPreparedRequestsAndConsumeCompletions( - minimumCount: UInt32 = 1, - timeout: Duration? = nil, - consumer: (consuming Completion?, Errno?, Bool) throws(E) -> Void - ) throws(E) - - public func blockingConsumeCompletion( - timeout: Duration? = nil - ) throws(Errno) -> Completion - - public func blockingConsumeCompletions( - minimumCount: UInt32 = 1, - timeout: Duration? = nil, - consumer: (consuming Completion?, Errno?, Bool) throws(E) -> Void - ) throws(E) - - public func tryConsumeCompletion() -> Completion? - - public struct Features: OptionSet, RawRepresentable, Hashable { - let rawValue: UInt32 - - public init(rawValue: UInt32) - - //IORING_FEAT_SINGLE_MMAP is handled internally - public static let nonDroppingCompletions: Features //IORING_FEAT_NODROP - public static let stableSubmissions: Features //IORING_FEAT_SUBMIT_STABLE - public static let currentFilePosition: Features //IORING_FEAT_RW_CUR_POS - public static let assumingTaskCredentials: Features //IORING_FEAT_CUR_PERSONALITY - public static let fastPolling: Features //IORING_FEAT_FAST_POLL - public static let epoll32BitFlags: Features //IORING_FEAT_POLL_32BITS - public static let pollNonFixedFiles: Features //IORING_FEAT_SQPOLL_NONFIXED - public static let extendedArguments: Features //IORING_FEAT_EXT_ARG - public static let nativeWorkers: Features //IORING_FEAT_NATIVE_WORKERS - public static let resourceTags: Features //IORING_FEAT_RSRC_TAGS - public static let allowsSkippingSuccessfulCompletions: Features //IORING_FEAT_CQE_SKIP - public static let improvedLinkedFiles: Features //IORING_FEAT_LINKED_FILE - public static let registerRegisteredRings: Features //IORING_FEAT_REG_REG_RING - public static let minimumTimeout: Features //IORING_FEAT_MIN_TIMEOUT - public static let bundledSendReceive: Features //IORING_FEAT_RECVSEND_BUNDLE - } - public var supportedFeatures: Features -} - -public extension IORing.RegisteredBuffer { - var unsafeBuffer: UnsafeMutableRawBufferPointer -} - -public extension IORing { - struct Request: ~Copyable { - public static func nop(context: UInt64 = 0) -> Request - - // overloads for each combination of registered vs unregistered buffer/descriptor - // Read - public static func read( - _ file: IORing.RegisteredFile, - into buffer: IORing.RegisteredBuffer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - public static func read( - _ file: FileDescriptor, - into buffer: IORing.RegisteredBuffer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - public static func read( - _ file: IORing.RegisteredFile, - into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - public static func read( - _ file: FileDescriptor, - into buffer: UnsafeMutableRawBufferPointer, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - // Write - public static func write( - _ buffer: IORing.RegisteredBuffer, - into file: IORing.RegisteredFile, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - public static func write( - _ buffer: IORing.RegisteredBuffer, - into file: FileDescriptor, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - public static func write( - _ buffer: UnsafeMutableRawBufferPointer, - into file: IORing.RegisteredFile, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - public static func write( - _ buffer: UnsafeMutableRawBufferPointer, - into file: FileDescriptor, - at offset: UInt64 = 0, - context: UInt64 = 0 - ) -> Request - - // Close - public static func close( - _ file: FileDescriptor, - context: UInt64 = 0 - ) -> Request - - public static func close( - _ file: IORing.RegisteredFile, - context: UInt64 = 0 - ) -> Request - - // Open At - public static func open( - _ path: FilePath, - in directory: FileDescriptor, - into slot: IORing.RegisteredFile, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - context: UInt64 = 0 - ) -> Request - - public static func open( - _ path: FilePath, - in directory: FileDescriptor, - mode: FileDescriptor.AccessMode, - options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), - permissions: FilePermissions? = nil, - context: UInt64 = 0 - ) -> Request - - public static func unlink( - _ path: FilePath, - in directory: FileDescriptor, - context: UInt64 = 0 - ) -> Request - - // Cancel - - public enum CancellationMatch { - case all - case first - } - - public static func cancel( - _ matchAll: CancellationMatch, - matchingContext: UInt64, - ) -> Request - - public static func cancel( - _ matchAll: CancellationMatch, - matching: FileDescriptor, - ) -> Request - - public static func cancel( - _ matchAll: CancellationMatch, - matching: IORing.RegisteredFile, - ) -> Request - - // Other operations follow in the same pattern - } - - struct Completion { - public struct Flags: OptionSet, Hashable, Codable { - public let rawValue: UInt32 - - public init(rawValue: UInt32) - - public static let moreCompletions: Flags - public static let socketNotEmpty: Flags - public static let isNotificationEvent: Flags - } - - //These are both the same value, but having both eliminates some ugly casts in client code - public var context: UInt64 - public var contextPointer: UnsafeRawPointer - - public var result: Int32 - - public var error: Errno? // Convenience wrapper over `result` - - public var flags: Flags - } -} - -``` - -## Usage Examples - -### Blocking - -```swift -let ring = try IORing(queueDepth: 2) - -//Make space on the ring for our file (this is optional, but improves performance with repeated use) -let file = ring.registerFiles(count: 1)[0] - -var statInfo = Glibc.stat() // System doesn't have an abstraction for stat() right now -// Build our requests to open the file and find out how big it is -ring.prepare(linkedRequests: - .open(path, - in: parentDirectory, - into: file, - mode: mode, - options: openOptions, - permissions: nil - ), - .stat(file, - into: &statInfo - ) -) -//batch submit 2 syscalls in 1! -try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming Completion?, error, done) in - if let error { - throw error //or other error handling as desired - } -} - -// We could register our buffer with the ring too, but we're only using it once -let buffer = UnsafeMutableRawBufferPointer.allocate(Int(statInfo.st_size)) - -// Build our requests to read the file and close it -ring.prepare(linkedRequests: - .read(file, - into: buffer - ), - .close(file) -) - -//batch submit 2 syscalls in 1! -try ring.submitPreparedRequestsAndConsumeCompletions(minimumCount: 2) { (completion: consuming Completion?, error, done) in - if let error { - throw error //or other error handling as desired - } -} - -processBuffer(buffer) -``` - -### Using libdispatch to wait for the read asynchronously - -```swift -//Initial setup as above up through creating buffer, omitted for brevity - -//Make the read request with a context so we can get the buffer out of it in the completion handler -… -.read(file, into: buffer, context: UInt64(buffer.baseAddress!)) -… - -// Make an eventfd and register it with the ring -let eventfd = eventfd(0, 0) -ring.registerEventFD(eventfd) - -// Make a read source to monitor the eventfd for readability -let readabilityMonitor = DispatchSource.makeReadSource(fileDescriptor: eventfd) -readabilityMonitor.setEventHandler { - let completion = ring.blockingConsumeCompletion() - if let error = completion.error { - //handle failure to read the file - } - processBuffer(completion.contextPointer) -} -readabilityMonitor.activate() - -ring.submitPreparedRequests //note, not "AndConsumeCompletions" this time -``` - -## Source compatibility - -This is an all-new API in Swift System, so has no backwards compatibility implications. Of note, though, this API is only available on Linux. - -## ABI compatibility - -Swift on Linux does not have a stable ABI, and we will likely take advantage of this to evolve IORing as compiler support improves, as described in Future Directions. - -## Implications on adoption - -This feature is intrinsically linked to Linux kernel support, so constrains the deployment target of anything that adopts it to newer kernels. Exactly which features of the evolving io_uring syscall surface area we need is under consideration. - -## Future directions - -* While most Swift users on Darwin are not limited by IO scalability issues, the thread pool considerations still make introducing something similar to this appealing if and when the relevant OS support is available. We should attempt to the best of our ability to not design this in a way that's gratuitously incompatible with non-Linux OSs, although Swift System does not attempt to have an API that's identical on all platforms. -* The set of syscalls covered by `io_uring` has grown significantly and is still growing. We should leave room for supporting additional operations in the future. -* Once same-element requirements and pack counts as integer generic arguments are supported by the compiler, we should consider adding something along the lines of the following to allow preparing, submitting, and waiting for an entire set of operations at once: - -``` -func submitLinkedRequestsAndWait( - _ requests: repeat each Request -) where Request == IORing.Request - -> InlineArray<(repeat each Request).count, IORing.Completion> -``` -* Once mutable borrows are supported, we should consider replacing the closure-taking bulk completion APIs (e.g. `blockingConsumeCompletions(…)`) with ones that return a sequence of completions instead -* We should consider making more types noncopyable as compiler support improves -* liburing has a "peek next completion" operation that doesn't consume it, and then a "mark consumed" operation. We may want to add something similar -* liburing has support for operations allocating their own buffers and returning them via the completion, we may want to support this -* We may want to provide API for asynchronously waiting, rather than just exposing the eventfd to let people roll their own async waits. Doing this really well has *considerable* implications for the concurrency runtime though. -* We should almost certainly expose API for more of the configuration options in `io_uring_setup` -* Stronger safety guarantees around cancellation and resource lifetimes (e.g. as described in https://without.boats/blog/io-uring/) would be very welcome, but require an API that is much more strongly opinionated about how io_uring is used. A future higher level abstraction focused on the goal of being "an async IO API for Swift" rather than "a Swifty interface to io_uring" seems like a good place for that. - -## Alternatives considered - -* We could use a NIO-style separate thread pool, but we believe `io_uring` is likely a better option for scalability. We may still want to provide a thread-pool backed version as an option, because many Linux systems currently disable `io_uring` due to security concerns. -* We could multiplex all IO onto a single actor as `AsyncBytes` currently does, but this has a number of downsides that make it entirely unsuitable to server usage. Most notably, it eliminates IO parallelism entirely. -* Using POSIX AIO instead of or as well as io_uring would greatly increase our ability to support older kernels and other Unix systems, but it has well-documented performance and usability issues that have prevented its adoption elsewhere, and apply just as much to Swift. -* Earlier versions of this proposal had higher level "managed" abstractions over IORing. These have been removed due to lack of interest from clients, but could be added back later if needed. -* I considered having dedicated error types for IORing, but eventually decided throwing Errno was more consistent with other platform APIs -* RegisteredResource was originally a class in an attempt to manage the lifetime of the resource via language features. Changing to the current model of it being a copyable struct didn't make the lifetime management any less safe (the IORing still owns the actual resource), and reduces overhead. In the future it would be neat if we could express RegisteredResources as being borrowed from the IORing so they can't be used after its lifetime. - -## Acknowledgments - -The NIO team, in particular Cory Benfield and Franz Busch, have provided invaluable feedback and direction on this project. From 528e7485ba5b3293114f93a8be33fe2b11fee9d2 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 16:38:21 -0700 Subject: [PATCH 104/172] Document completion flags and disable ones we don't currently support --- Sources/System/IOCompletion.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 4263bd8d..395e8927 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -18,10 +18,15 @@ public extension IORing.Completion { self.rawValue = rawValue } - public static let allocatedBuffer = Flags(rawValue: 1 << 0) - public static let moreCompletions = Flags(rawValue: 1 << 1) - public static let socketNotEmpty = Flags(rawValue: 1 << 2) - public static let isNotificationEvent = Flags(rawValue: 1 << 3) + ///`IORING_CQE_F_BUFFER` Indicates the buffer ID is stored in the upper 16 bits + @inlinable public static var allocatedBuffer: Flags { Flags(rawValue: 1 << 0) } + ///`IORING_CQE_F_MORE` Indicates more completions will be generated from the request that generated this + @inlinable public static var moreCompletions: Flags { Flags(rawValue: 1 << 1) } + //`IORING_CQE_F_SOCK_NONEMPTY`, but currently unused + //@inlinable public static var socketNotEmpty: Flags { Flags(rawValue: 1 << 2) } + //`IORING_CQE_F_NOTIF`, but currently unused + //@inlinable public static var isNotificationEvent: Flags { Flags(rawValue: 1 << 3) } + //IORING_CQE_F_BUF_MORE will eventually be (1U << 4) if we add IOU_PBUF_RING_INC support } } From d6687d2bbb73e8f93980be1f67ec5cafdc185775 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 16:51:24 -0700 Subject: [PATCH 105/172] Add deleted file back --- Sources/CSystem/shims.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Sources/CSystem/shims.c diff --git a/Sources/CSystem/shims.c b/Sources/CSystem/shims.c new file mode 100644 index 00000000..f492a2ae --- /dev/null +++ b/Sources/CSystem/shims.c @@ -0,0 +1,18 @@ +/* + 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 +*/ + +#ifdef __linux__ + +#include + +#endif + +#if defined(_WIN32) +#include +#endif From d2b78f801bf5dba3f72167e1f5e05e6a5c5edd10 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 23:53:50 +0000 Subject: [PATCH 106/172] Remove unneeded defines --- Sources/CSystem/include/io_uring.h | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index df1b36a8..2d081af0 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -8,21 +8,6 @@ #ifndef SWIFT_IORING_C_WRAPPER #define SWIFT_IORING_C_WRAPPER -#ifdef __alpha__ -/* - * alpha is the only exception, all other architectures - * have common numbers for new system calls. - */ -# ifndef __NR_io_uring_setup -# define __NR_io_uring_setup 535 -# endif -# ifndef __NR_io_uring_enter -# define __NR_io_uring_enter 536 -# endif -# ifndef __NR_io_uring_register -# define __NR_io_uring_register 537 -# endif -#else /* !__alpha__ */ # ifndef __NR_io_uring_setup # define __NR_io_uring_setup 425 # endif @@ -32,7 +17,6 @@ # ifndef __NR_io_uring_register # define __NR_io_uring_register 427 # endif -#endif /* struct io_uring_getevents_arg { From 8757fae97c67a4a74c49610d8748e691cdac26f2 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 8 Jul 2025 16:57:30 -0700 Subject: [PATCH 107/172] Remove some stray commas the API breakage test complains about --- Sources/System/IORequest.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 1d0638b4..f6c1f749 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -328,7 +328,7 @@ extension IORing.Request { @inlinable public static func cancel( _ matchAll: CancellationMatch, - matchingContext: UInt64, + matchingContext: UInt64 ) -> IORing.Request { switch matchAll { case .all: @@ -340,7 +340,7 @@ extension IORing.Request { @inlinable public static func cancel( _ matchAll: CancellationMatch, - matching: FileDescriptor, + matching: FileDescriptor ) -> IORing.Request { switch matchAll { case .all: @@ -352,7 +352,7 @@ extension IORing.Request { @inlinable public static func cancel( _ matchAll: CancellationMatch, - matching: IORing.RegisteredFile, + matching: IORing.RegisteredFile ) -> IORing.Request { switch matchAll { case .all: From 4b70ffe375f6a8891e81081b97ff055b754a610b Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 9 Jul 2025 20:53:23 +0000 Subject: [PATCH 108/172] Fix short submits and also fix the registration ops enum, which somehow stopped compiling overnight --- Sources/CSystem/include/io_uring.h | 18 ---------------- Sources/System/IORing.swift | 33 +++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index 2d081af0..0068f503 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -33,24 +33,6 @@ struct swift_io_uring_getevents_arg { __u64 ts; }; -//This was #defines in older headers, so we redeclare it to get a consistent import -typedef enum : __u32 { - SWIFT_IORING_REGISTER_BUFFERS = 0, - SWIFT_IORING_UNREGISTER_BUFFERS = 1, - SWIFT_IORING_REGISTER_FILES = 2, - SWIFT_IORING_UNREGISTER_FILES = 3, - SWIFT_IORING_REGISTER_EVENTFD = 4, - SWIFT_IORING_UNREGISTER_EVENTFD = 5, - SWIFT_IORING_REGISTER_FILES_UPDATE = 6, - SWIFT_IORING_REGISTER_EVENTFD_ASYNC = 7, - SWIFT_IORING_REGISTER_PROBE = 8, - SWIFT_IORING_REGISTER_PERSONALITY = 9, - SWIFT_IORING_UNREGISTER_PERSONALITY = 10, - - /* this goes last */ - SWIFT_IORING_REGISTER_LAST -} SWIFT_IORING_REGISTER_OPS; - static inline int io_uring_register(int fd, unsigned int opcode, void *arg, unsigned int nr_args) { diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 4a28f7c8..5546540f 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -8,6 +8,21 @@ import Musl #endif import Synchronization +//This was #defines in older headers, so we redeclare it to get a consistent import +internal enum RegistrationOps: UInt32 { + case registerBuffers = 0 + case unregisterBuffers = 1 + case registerFiles = 2 + case unregisterFiles = 3 + case registerEventFD = 4 + case unregisterEventFD = 5 + case registerFilesUpdate = 6 + case registerEventFDAsync = 7 + case registerProbe = 8 + case registerPersonality = 9 + case unregisterPersonality = 10 +} + extension UnsafeMutableRawPointer { func advanced(by offset: UInt32) -> UnsafeMutableRawPointer { return advanced(by: Int(offset)) @@ -71,6 +86,7 @@ internal func _tryWriteRequest( //Tell the kernel that we've submitted requests and/or are waiting for completions @inlinable internal func _enter( + ring: borrowing SQRing, ringDescriptor: Int32, numEvents: UInt32, minCompletions: UInt32, @@ -91,6 +107,9 @@ internal func _enter( continue } else if ret < 0 { throw(Errno(rawValue: -ret)) + } else if _getSubmissionQueueCount(ring: ring) > 0 { + // See https://github.com/axboe/liburing/issues/309, in some cases not all pending requests are submitted + continue } else { return ret } @@ -101,7 +120,7 @@ internal func _enter( internal func _submitRequests(ring: borrowing SQRing, ringDescriptor: Int32) throws(Errno) { let flushedEvents = _flushQueue(ring: ring) _ = try _enter( - ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) + ring: ring, ringDescriptor: ringDescriptor, numEvents: flushedEvents, minCompletions: 0, flags: 0) } @inlinable @@ -536,7 +555,7 @@ public struct IORing: ~Copyable { let result = withUnsafePointer(to: &rawfd) { fdptr in let result = io_uring_register( ringDescriptor, - SWIFT_IORING_REGISTER_EVENTFD.rawValue, + RegistrationOps.registerEventFD.rawValue, UnsafeMutableRawPointer(mutating: fdptr), 1 ) @@ -551,7 +570,7 @@ public struct IORing: ~Copyable { public mutating func unregisterEventFD() throws(Errno) { let result = io_uring_register( ringDescriptor, - IORING_UNREGISTER_EVENTFD.rawValue, + RegistrationOps.unregisterEventFD.rawValue, nil, 0 ) @@ -569,7 +588,7 @@ public struct IORing: ~Copyable { let regResult = files.withUnsafeBufferPointer { bPtr in let result = io_uring_register( self.ringDescriptor, - IORING_REGISTER_FILES.rawValue, + RegistrationOps.registerFiles.rawValue, UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: count) ) @@ -588,7 +607,7 @@ public struct IORing: ~Copyable { public func unregisterFiles() throws { let result = io_uring_register( ringDescriptor, - IORING_UNREGISTER_FILES.rawValue, + RegistrationOps.unregisterFiles.rawValue, nil, 0 ) @@ -613,7 +632,7 @@ public struct IORing: ~Copyable { let regResult = iovecs.withUnsafeBufferPointer { bPtr in let result = io_uring_register( self.ringDescriptor, - IORING_REGISTER_BUFFERS.rawValue, + RegistrationOps.registerBuffers.rawValue, UnsafeMutableRawPointer(mutating: bPtr.baseAddress!), UInt32(truncatingIfNeeded: buffers.count) ) @@ -662,7 +681,7 @@ public struct IORing: ~Copyable { public func unregisterBuffers() throws { let result = io_uring_register( self.ringDescriptor, - IORING_UNREGISTER_BUFFERS.rawValue, + RegistrationOps.unregisterBuffers.rawValue, nil, 0 ) From 329c5f59a25efc5a96468ead91d5a1a073b396e1 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 9 Jul 2025 21:17:42 +0000 Subject: [PATCH 109/172] Conditionalize IORing on compiler >= 6.2 --- Sources/System/IOCompletion.swift | 2 ++ Sources/System/IORequest.swift | 2 ++ Sources/System/IORing.swift | 3 +++ Sources/System/RawIORequest.swift | 3 +++ Tests/SystemTests/IORequestTests.swift | 3 +++ Tests/SystemTests/IORingTests.swift | 3 +++ 6 files changed, 16 insertions(+) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IOCompletion.swift index 395e8927..65616e27 100644 --- a/Sources/System/IOCompletion.swift +++ b/Sources/System/IOCompletion.swift @@ -1,4 +1,5 @@ #if os(Linux) +#if compiler(>=6.2) import CSystem public extension IORing { @@ -66,3 +67,4 @@ public extension IORing.Completion { } } #endif +#endif diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index f6c1f749..5d4143de 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -1,4 +1,5 @@ #if os(Linux) +#if compiler(>=6.2) import CSystem @usableFromInline @@ -466,3 +467,4 @@ extension IORing.Request { } } #endif +#endif diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 5546540f..cb47e341 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -1,4 +1,6 @@ #if os(Linux) +#if compiler(>=6.2) + import CSystem // needed for mmap #if canImport(Glibc) @@ -825,3 +827,4 @@ extension IORing.RegisteredBuffer { } } #endif +#endif \ No newline at end of file diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 3734760f..461bae09 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -1,4 +1,6 @@ #if os(Linux) +#if compiler(>=6.2) + import CSystem @usableFromInline @@ -198,3 +200,4 @@ extension RawIORequest { } } #endif +#endif \ No newline at end of file diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 4d32196a..53a0a82e 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -1,4 +1,6 @@ #if os(Linux) +#if compiler(>=6.2) + import XCTest #if SYSTEM_PACKAGE @@ -29,3 +31,4 @@ final class IORequestTests: XCTestCase { } } #endif +#endif \ No newline at end of file diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 757866ad..7fb899f1 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -1,4 +1,6 @@ #if os(Linux) +#if compiler(>=6.2) + import XCTest import CSystem //for eventfd @@ -108,3 +110,4 @@ final class IORingTests: XCTestCase { } } #endif +#endif \ No newline at end of file From a6675d7e5c54d7a9dfdae14451aa48b6d86f819c Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 10 Jul 2025 23:22:11 +0000 Subject: [PATCH 110/172] timespec should be compatible here, let's try it to fix the Ubuntu 22 build --- Sources/System/IORing.swift | 12 ++++++------ Sources/System/RawIORequest.swift | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index cb47e341..6a281a0e 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -479,9 +479,9 @@ public struct IORing: ~Copyable { timeout: Duration? = nil ) throws(Errno) -> Completion { if let timeout { - var ts = __kernel_timespec( - tv_sec: timeout.components.seconds, - tv_nsec: timeout.components.attoseconds / 1_000_000_000 + var ts = timespec( + tv_sec: Int(timeout.components.seconds), + tv_nsec: Int(timeout.components.attoseconds / 1_000_000_000) ) return try withUnsafePointer(to: &ts) { (tsPtr) throws(Errno) -> Completion in var args = swift_io_uring_getevents_arg( @@ -505,9 +505,9 @@ public struct IORing: ~Copyable { consumer: (consuming Completion?, Errno?, Bool) throws(Err) -> Void ) throws(Err) { if let timeout { - var ts = __kernel_timespec( - tv_sec: timeout.components.seconds, - tv_nsec: timeout.components.attoseconds / 1_000_000_000 + var ts = timespec( + tv_sec: Int(timeout.components.seconds), + tv_nsec: Int(timeout.components.attoseconds / 1_000_000_000) ) try withUnsafePointer(to: &ts) { (tsPtr) throws(Err) in var args = swift_io_uring_getevents_arg( diff --git a/Sources/System/RawIORequest.swift b/Sources/System/RawIORequest.swift index 461bae09..b22c1c73 100644 --- a/Sources/System/RawIORequest.swift +++ b/Sources/System/RawIORequest.swift @@ -184,9 +184,9 @@ extension RawIORequest { opEntry.pointee.flags |= Flags.linkRequest.rawValue opEntry.pointee.off = 1 - var ts = __kernel_timespec( - tv_sec: duration.components.seconds, - tv_nsec: duration.components.attoseconds / 1_000_000_000 + var ts = timespec( + tv_sec: Int(duration.components.seconds), + tv_nsec: Int(duration.components.attoseconds / 1_000_000_000) ) return try withUnsafePointer(to: &ts) { tsPtr in var req: RawIORequest = RawIORequest() From d121081c01de1970730f3623e4d42b648e7bf013 Mon Sep 17 00:00:00 2001 From: David Smith Date: Thu, 10 Jul 2025 23:30:42 +0000 Subject: [PATCH 111/172] More Ubuntu 22 workarounds. This should error at runtime rather than not compiling, since we don't have Linux version checks at compile time --- Sources/System/IORequest.swift | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index 5d4143de..b7a0e1a4 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -321,7 +321,15 @@ extension IORing.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 } + public enum CancellationMatch { case all case first @@ -333,9 +341,9 @@ extension IORing.Request { ) -> IORing.Request { switch matchAll { case .all: - .init(core: .cancel(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) case .first: - .init(core: .cancel(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ANY | SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) } } @@ -345,9 +353,9 @@ extension IORing.Request { ) -> IORing.Request { switch matchAll { case .all: - .init(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD, targetFD: matching)) + .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) case .first: - .init(core: .cancelFD(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD, targetFD: matching)) + .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_ANY | SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) } } @@ -357,9 +365,9 @@ extension IORing.Request { ) -> IORing.Request { switch matchAll { case .all: - .init(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ALL | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) case .first: - .init(core: .cancelFDSlot(flags: IORING_ASYNC_CANCEL_ANY | IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_ANY | SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) } } From 2f6c4e0f03638880fe92081a52b0bca07f295bf4 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 16:14:36 +0000 Subject: [PATCH 112/172] Add support for non-IORING_FEAT_SINGLE_MMAP kernels --- Sources/System/IORing.swift | 123 ++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 34 deletions(-) diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index 6a281a0e..ed451c39 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -179,7 +179,7 @@ internal func _getSubmissionEntry( private func setUpRing( queueDepth: UInt32, flags: IORing.SetupFlags ) throws(Errno) -> - (params: io_uring_params, ringDescriptor: Int32, ringPtr: UnsafeMutableRawPointer, ringSize: Int, sqes: UnsafeMutableRawPointer) { + (params: io_uring_params, ringDescriptor: Int32, ringPtr: UnsafeMutableRawPointer?, ringSize: Int, submissionRingPtr: UnsafeMutableRawPointer?, submissionRingSize: Int, completionRingPtr: UnsafeMutableRawPointer?, completionRingSize: Int, sqes: UnsafeMutableRawPointer) { var params = io_uring_params() params.flags = flags.rawValue @@ -196,8 +196,7 @@ private func setUpRing( throw err } - if params.features & IORING_FEAT_SINGLE_MMAP == 0 - || params.features & IORING_FEAT_NODROP == 0 + if params.features & IORING_FEAT_NODROP == 0 { close(ringDescriptor) throw Errno.invalidArgument @@ -213,20 +212,55 @@ private func setUpRing( let ringSize = Int(max(submitRingSize, completionRingSize)) - let ringPtr: UnsafeMutableRawPointer! = mmap( - /* addr: */ nil, - /* len: */ ringSize, - /* prot: */ PROT_READ | PROT_WRITE, - /* flags: */ MAP_SHARED | MAP_POPULATE, - /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_SQ_RING) - ) + var ringPtr: UnsafeMutableRawPointer! + var sqPtr: UnsafeMutableRawPointer! + var cqPtr: UnsafeMutableRawPointer! + + if params.features & IORING_FEAT_SINGLE_MMAP != 0{ + ringPtr = mmap( + /* addr: */ nil, + /* len: */ ringSize, + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_SQ_RING) + ) - if ringPtr == MAP_FAILED { - let errno = Errno.current - perror("mmap") - close(ringDescriptor) - throw errno + if ringPtr == MAP_FAILED { + let errno = Errno.current + close(ringDescriptor) + throw errno + } + } else { + sqPtr = mmap( + /* addr: */ nil, + /* len: */ Int(submitRingSize), + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_SQ_RING) + ) + + if sqPtr == MAP_FAILED { + let errno = Errno.current + close(ringDescriptor) + throw errno + } + + cqPtr = mmap( + /* addr: */ nil, + /* len: */ Int(completionRingSize), + /* prot: */ PROT_READ | PROT_WRITE, + /* flags: */ MAP_SHARED | MAP_POPULATE, + /* fd: */ ringDescriptor, + /* offset: */ __off_t(IORING_OFF_CQ_RING) + ) + + if cqPtr == MAP_FAILED { + let errno: Errno = Errno.current + close(ringDescriptor) + throw errno + } } // map the submission queue @@ -241,13 +275,21 @@ private func setUpRing( if sqes == MAP_FAILED { let errno = Errno.current - perror("mmap") - munmap(ringPtr, ringSize) + if ringPtr != nil { + munmap(ringPtr, ringSize) + } else { + if sqPtr != nil { + munmap(sqPtr, Int(submitRingSize)) + } + if cqPtr != nil { + munmap(cqPtr, Int(completionRingSize)) + } + } close(ringDescriptor) throw errno } - return (params: params, ringDescriptor: ringDescriptor, ringPtr: ringPtr!, ringSize: ringSize, sqes: sqes!) + return (params: params, ringDescriptor: ringDescriptor, ringPtr: ringPtr, ringSize: ringSize, submissionRingPtr: sqPtr, submissionRingSize: Int(submitRingSize), completionRingPtr: cqPtr, completionRingSize: Int(completionRingSize), sqes: sqes!) } ///IORing provides facilities for @@ -267,9 +309,13 @@ public struct IORing: ~Copyable { @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer - // kept around for unmap / cleanup + // kept around for unmap / cleanup. TODO: we can save a few words of memory by figuring out how to handle cleanup for non-IORING_FEAT_SINGLE_MMAP better let ringSize: Int - let ringPtr: UnsafeMutableRawPointer + let ringPtr: UnsafeMutableRawPointer? + let submissionRingSize: Int + let submissionRingPtr: UnsafeMutableRawPointer? + let completionRingSize: Int + let completionRingPtr: UnsafeMutableRawPointer? @usableFromInline var _registeredFiles: [UInt32] @usableFromInline var _registeredBuffers: [iovec] @@ -320,7 +366,7 @@ public struct IORing: ~Copyable { /// Initializes an IORing with enough space for `queueDepth` prepared requests and completed operations public init(queueDepth: UInt32, flags: SetupFlags = []) throws(Errno) { - let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags) + let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, tmpSQPtr, tmpSQSize, tmpCQPtr, tmpCQSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags) // All throws need to be before initializing ivars here to avoid // "error: conditional initialization or destruction of noncopyable types is not supported; // this variable must be consistently in an initialized or uninitialized state through every code path" @@ -328,29 +374,33 @@ public struct IORing: ~Copyable { ringDescriptor = tmpRingDescriptor ringPtr = tmpRingPtr ringSize = tmpRingSize + submissionRingPtr = tmpSQPtr + submissionRingSize = tmpSQSize + completionRingPtr = tmpCQPtr + completionRingSize = tmpCQSize _registeredFiles = [] _registeredBuffers = [] submissionRing = SQRing( kernelHead: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.head) + (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.head) .assumingMemoryBound(to: Atomic.self) ), kernelTail: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.tail) + (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.tail) .assumingMemoryBound(to: Atomic.self) ), userTail: 0, // no requests yet - ringMask: ringPtr.advanced(by: params.sq_off.ring_mask) + ringMask: (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, flags: UnsafePointer>( - ringPtr.advanced(by: params.sq_off.flags) + (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.flags) .assumingMemoryBound(to: Atomic.self) ), array: UnsafeMutableBufferPointer( - start: ringPtr.advanced(by: params.sq_off.array) + start: (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.array) .assumingMemoryBound(to: UInt32.self), count: Int( - ringPtr.advanced(by: params.sq_off.ring_entries) + (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.ring_entries) .assumingMemoryBound(to: UInt32.self).pointee) ) ) @@ -367,20 +417,20 @@ public struct IORing: ~Copyable { completionRing = CQRing( kernelHead: UnsafePointer>( - ringPtr.advanced(by: params.cq_off.head) + (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.head) .assumingMemoryBound(to: Atomic.self) ), kernelTail: UnsafePointer>( - ringPtr.advanced(by: params.cq_off.tail) + (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.tail) .assumingMemoryBound(to: Atomic.self) ), - ringMask: ringPtr.advanced(by: params.cq_off.ring_mask) + ringMask: (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, cqes: UnsafeBufferPointer( - start: ringPtr.advanced(by: params.cq_off.cqes) + start: (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.cqes) .assumingMemoryBound(to: io_uring_cqe.self), count: Int( - ringPtr.advanced(by: params.cq_off.ring_entries) + (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.ring_entries) .assumingMemoryBound(to: UInt32.self).pointee) ) ) @@ -799,7 +849,12 @@ public struct IORing: ~Copyable { } deinit { - munmap(ringPtr, ringSize) + if let ringPtr { + munmap(ringPtr, ringSize) + } else if let submissionRingPtr, let completionRingPtr { + munmap(submissionRingPtr, submissionRingSize) + munmap(completionRingPtr, completionRingSize) + } munmap( UnsafeMutableRawPointer(submissionQueueEntries.baseAddress!), submissionQueueEntries.count * MemoryLayout.size From 779c9ac6285b8d8fd389e6496decf5ee3a480733 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 16:50:58 +0000 Subject: [PATCH 113/172] Fix misuse of _CANCEL_ANY, and add support for non-redicated cancellation --- Sources/System/IORequest.swift | 28 +++++++++++++++++++++++----- Sources/System/IORing.swift | 1 + 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Sources/System/IORequest.swift b/Sources/System/IORequest.swift index b7a0e1a4..275d27bb 100644 --- a/Sources/System/IORequest.swift +++ b/Sources/System/IORequest.swift @@ -84,6 +84,9 @@ internal enum IORequestCore { context: UInt64 = 0 ) case cancel( + flags:UInt32 + ) + case cancelContext( flags: UInt32, targetContext: UInt64 ) @@ -95,6 +98,7 @@ internal enum IORequestCore { flags: UInt32, target: IORing.RegisteredFile ) + } @inline(__always) @inlinable @@ -341,9 +345,9 @@ extension IORing.Request { ) -> IORing.Request { switch matchAll { case .all: - .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + .init(core: .cancelContext(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) case .first: - .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ANY | SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) + .init(core: .cancelContext(flags: SWIFT_IORING_ASYNC_CANCEL_USERDATA, targetContext: matchingContext)) } } @@ -355,7 +359,7 @@ extension IORing.Request { case .all: .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) case .first: - .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_ANY | SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) + .init(core: .cancelFD(flags: SWIFT_IORING_ASYNC_CANCEL_FD, targetFD: matching)) } } @@ -367,7 +371,18 @@ extension IORing.Request { case .all: .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_ALL | SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) case .first: - .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_ANY | SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + .init(core: .cancelFDSlot(flags: SWIFT_IORING_ASYNC_CANCEL_FD_FIXED, target: matching)) + } + } + + @inlinable public static func cancel( + _ matchAll: CancellationMatch, + ) -> IORing.Request { + switch matchAll { + case .all: + .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ALL)) + case .first: + .init(core: .cancel(flags: SWIFT_IORING_ASYNC_CANCEL_ANY)) } } @@ -457,7 +472,7 @@ extension IORing.Request { ) request.path = path request.rawValue.user_data = context - case .cancel(let flags, let targetContext): + case .cancelContext(let flags, let targetContext): request.operation = .asyncCancel request.cancel_flags = flags request.addr = targetContext @@ -469,6 +484,9 @@ extension IORing.Request { request.operation = .asyncCancel request.cancel_flags = flags request.rawValue.fd = Int32(target.index) + case .cancel(let flags): + request.operation = .asyncCancel + request.cancel_flags = flags } return request diff --git a/Sources/System/IORing.swift b/Sources/System/IORing.swift index ed451c39..604f3fde 100644 --- a/Sources/System/IORing.swift +++ b/Sources/System/IORing.swift @@ -378,6 +378,7 @@ public struct IORing: ~Copyable { submissionRingSize = tmpSQSize completionRingPtr = tmpCQPtr completionRingSize = tmpCQSize + _registeredFiles = [] _registeredBuffers = [] submissionRing = SQRing( From 7fc787e6ba4ba3a68b43562dcde165560e7b9855 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 17:00:32 +0000 Subject: [PATCH 114/172] Move IORing files to a subdirectory --- Sources/System/{ => IORing}/IOCompletion.swift | 0 Sources/System/{ => IORing}/IORequest.swift | 0 Sources/System/{ => IORing}/IORing.swift | 0 Sources/System/{ => IORing}/RawIORequest.swift | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Sources/System/{ => IORing}/IOCompletion.swift (100%) rename Sources/System/{ => IORing}/IORequest.swift (100%) rename Sources/System/{ => IORing}/IORing.swift (100%) rename Sources/System/{ => IORing}/RawIORequest.swift (100%) diff --git a/Sources/System/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift similarity index 100% rename from Sources/System/IOCompletion.swift rename to Sources/System/IORing/IOCompletion.swift diff --git a/Sources/System/IORequest.swift b/Sources/System/IORing/IORequest.swift similarity index 100% rename from Sources/System/IORequest.swift rename to Sources/System/IORing/IORequest.swift diff --git a/Sources/System/IORing.swift b/Sources/System/IORing/IORing.swift similarity index 100% rename from Sources/System/IORing.swift rename to Sources/System/IORing/IORing.swift diff --git a/Sources/System/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift similarity index 100% rename from Sources/System/RawIORequest.swift rename to Sources/System/IORing/RawIORequest.swift From f7c05e1fb846fb4748f53d015256f85c8819a7ff Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 18:53:55 +0000 Subject: [PATCH 115/172] Combine platform conditionals --- Sources/System/IORing/IOCompletion.swift | 4 +--- Sources/System/IORing/IORequest.swift | 4 +--- Sources/System/IORing/IORing.swift | 4 +--- Sources/System/IORing/RawIORequest.swift | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift index 65616e27..375abae8 100644 --- a/Sources/System/IORing/IOCompletion.swift +++ b/Sources/System/IORing/IOCompletion.swift @@ -1,5 +1,4 @@ -#if os(Linux) -#if compiler(>=6.2) +#if os(Linux) && compiler(>=6.2) import CSystem public extension IORing { @@ -67,4 +66,3 @@ public extension IORing.Completion { } } #endif -#endif diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 275d27bb..f7130f2f 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -1,5 +1,4 @@ -#if os(Linux) -#if compiler(>=6.2) +#if os(Linux) && compiler(>=6.2) import CSystem @usableFromInline @@ -493,4 +492,3 @@ extension IORing.Request { } } #endif -#endif diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index 604f3fde..3b49d5ce 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -1,5 +1,4 @@ -#if os(Linux) -#if compiler(>=6.2) +#if os(Linux) && compiler(>=6.2) import CSystem // needed for mmap @@ -883,4 +882,3 @@ extension IORing.RegisteredBuffer { } } #endif -#endif \ No newline at end of file diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index b22c1c73..2fc8a999 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -1,5 +1,4 @@ -#if os(Linux) -#if compiler(>=6.2) +#if os(Linux) && compiler(>=6.2) import CSystem @@ -200,4 +199,3 @@ extension RawIORequest { } } #endif -#endif \ No newline at end of file From a56bcda3c1bd85131f1e36d90ff11ffcdae900e8 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 19:07:04 +0000 Subject: [PATCH 116/172] Un-combine conditionals but flip their order. This should fix the Windows build with older compilers --- Sources/System/IORing/IOCompletion.swift | 5 ++++- Sources/System/IORing/IORequest.swift | 5 ++++- Sources/System/IORing/IORing.swift | 4 +++- Sources/System/IORing/RawIORequest.swift | 4 +++- Tests/SystemTests/IORequestTests.swift | 2 +- Tests/SystemTests/IORingTests.swift | 2 +- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift index 375abae8..ce23444d 100644 --- a/Sources/System/IORing/IOCompletion.swift +++ b/Sources/System/IORing/IOCompletion.swift @@ -1,4 +1,6 @@ -#if os(Linux) && compiler(>=6.2) +#if compiler(>=6.2) +#if os(Linux) + import CSystem public extension IORing { @@ -66,3 +68,4 @@ public extension IORing.Completion { } } #endif +#endif \ No newline at end of file diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index f7130f2f..5ab5ac94 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -1,4 +1,6 @@ -#if os(Linux) && compiler(>=6.2) +#if compiler(>=6.2) +#if os(Linux) + import CSystem @usableFromInline @@ -492,3 +494,4 @@ extension IORing.Request { } } #endif +#endif \ No newline at end of file diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index 3b49d5ce..c0fe005a 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -1,4 +1,5 @@ -#if os(Linux) && compiler(>=6.2) +#if compiler(>=6.2) +#if os(Linux) import CSystem // needed for mmap @@ -882,3 +883,4 @@ extension IORing.RegisteredBuffer { } } #endif +#endif \ No newline at end of file diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index 2fc8a999..332cd917 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -1,4 +1,5 @@ -#if os(Linux) && compiler(>=6.2) +#if compiler(>=6.2) +#if os(Linux) import CSystem @@ -199,3 +200,4 @@ extension RawIORequest { } } #endif +#endif \ No newline at end of file diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 53a0a82e..9a809689 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -1,5 +1,5 @@ -#if os(Linux) #if compiler(>=6.2) +#if os(Linux) import XCTest diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 7fb899f1..3a2473c3 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -1,5 +1,5 @@ -#if os(Linux) #if compiler(>=6.2) +#if os(Linux) import XCTest import CSystem //for eventfd From 9a046498d07bf9e3c1f087b51373e603a51d2024 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 19:23:24 +0000 Subject: [PATCH 117/172] Try adding newlines to the ends of files to work around a compiler bug --- Sources/System/IORing/IOCompletion.swift | 2 +- Sources/System/IORing/IORequest.swift | 2 +- Sources/System/IORing/IORing.swift | 2 +- Sources/System/IORing/RawIORequest.swift | 2 +- Tests/SystemTests/IORequestTests.swift | 2 +- Tests/SystemTests/IORingTests.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift index ce23444d..ea99cc70 100644 --- a/Sources/System/IORing/IOCompletion.swift +++ b/Sources/System/IORing/IOCompletion.swift @@ -68,4 +68,4 @@ public extension IORing.Completion { } } #endif -#endif \ No newline at end of file +#endif diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 5ab5ac94..cc91e476 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -494,4 +494,4 @@ extension IORing.Request { } } #endif -#endif \ No newline at end of file +#endif diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index c0fe005a..c676f78e 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -883,4 +883,4 @@ extension IORing.RegisteredBuffer { } } #endif -#endif \ No newline at end of file +#endif diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index 332cd917..dea378fb 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -200,4 +200,4 @@ extension RawIORequest { } } #endif -#endif \ No newline at end of file +#endif diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 9a809689..50728234 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -31,4 +31,4 @@ final class IORequestTests: XCTestCase { } } #endif -#endif \ No newline at end of file +#endif diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 3a2473c3..63e4e78b 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -110,4 +110,4 @@ final class IORingTests: XCTestCase { } } #endif -#endif \ No newline at end of file +#endif From b93169b8e28b44664dc7ff93ec79fbca4f7e9e35 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 20:37:23 +0000 Subject: [PATCH 118/172] Work around windows bugs harder --- Package.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 48b348af..cb19f158 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,12 @@ let swiftSettings: [SwiftSetting] = [ .enableExperimentalFeature("Lifetimes"), ] +#if os(Linux) +let filesToExclude = ["CMakeLists.txt"] +#else +let filesToExclude = ["CMakeLists.txt", "Sources/IORing/", "Tests/SystemTests/IORequestTests.swift", "Tests/SystemTests/IORingTests.swift"] +#endif + let package = Package( name: "swift-system", products: [ @@ -41,7 +47,7 @@ let package = Package( name: "SystemPackage", dependencies: ["CSystem"], path: "Sources/System", - exclude: ["CMakeLists.txt"], + exclude: filesToExclude, cSettings: cSettings, swiftSettings: swiftSettings), .testTarget( From 52d9c99a9476ad19e57b55b94803a8a3534aa8da Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 13:42:49 -0700 Subject: [PATCH 119/172] Fix excludes --- Package.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index cb19f158..a5013fed 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,13 @@ let swiftSettings: [SwiftSetting] = [ #if os(Linux) let filesToExclude = ["CMakeLists.txt"] #else -let filesToExclude = ["CMakeLists.txt", "Sources/IORing/", "Tests/SystemTests/IORequestTests.swift", "Tests/SystemTests/IORingTests.swift"] +let filesToExclude = ["CMakeLists.txt", "IORing"] +#endif + +#if os(Linux) +let testsToExclude = [] +#else +let testsToExclude = ["IORequestTests.swift", "IORingTests.swift"] #endif let package = Package( @@ -53,6 +59,7 @@ let package = Package( .testTarget( name: "SystemTests", dependencies: ["SystemPackage"], + exclude: testsToExclude, cSettings: cSettings, swiftSettings: swiftSettings), ]) \ No newline at end of file From 33b58a81106df8b6c9fa61caf2313aec2a936f1a Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 13:44:46 -0700 Subject: [PATCH 120/172] Fix excludes harder --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index a5013fed..9cbfc6e9 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let filesToExclude = ["CMakeLists.txt", "IORing"] #endif #if os(Linux) -let testsToExclude = [] +let testsToExclude:[String] = [] #else let testsToExclude = ["IORequestTests.swift", "IORingTests.swift"] #endif @@ -62,4 +62,4 @@ let package = Package( exclude: testsToExclude, cSettings: cSettings, swiftSettings: swiftSettings), - ]) \ No newline at end of file + ]) From a66176f51829683d280decf7044683364ce5ceb4 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 22:42:35 +0000 Subject: [PATCH 121/172] Only run IORing tests if io_uring support is enabled --- Tests/SystemTests/IORingTests.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 63e4e78b..5095f079 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -10,12 +10,25 @@ import SystemPackage import System #endif +func uringEnabled() throws -> Bool { + let procPath = FilePath("/proc/sys/kernel/io_uring_disabled") + let fd = try FileDescriptor.open(procPath, .readOnly) + let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1024, alignment: 0) + _ = try fd.read(into: buffer) + if buffer.load(fromByteOffset: 0, as: Int.self) == 0 { + return true + } + return false +} + final class IORingTests: XCTestCase { func testInit() throws { + guard try uringEnabled() else { return } _ = try IORing(queueDepth: 32, flags: []) } func testNop() throws { + guard try uringEnabled() else { return } var ring = try IORing(queueDepth: 32, flags: []) _ = try ring.submit(linkedRequests: .nop()) let completion = try ring.blockingConsumeCompletion() @@ -51,6 +64,7 @@ final class IORingTests: XCTestCase { } func testUndersizedSubmissionQueue() throws { + guard try uringEnabled() else { return } var ring: IORing = try IORing(queueDepth: 1) let enqueued = ring.prepare(linkedRequests: .nop(), .nop()) XCTAssertFalse(enqueued) @@ -58,6 +72,7 @@ final class IORingTests: XCTestCase { // Exercises opening, reading, closing, registered files, registered buffers, and eventfd func testOpenReadAndWriteFixedFile() throws { + guard try uringEnabled() else { return } let (parent, path) = try makeHelloWorldFile() let rawBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 13, alignment: 16) var ring = try setupTestRing(depth: 6, fileSlots: 1, buffers: [rawBuffer]) From 08ed962064a04dd002c2175416e0098c180a33e0 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 11 Jul 2025 23:05:26 +0000 Subject: [PATCH 122/172] Don't abort the test if we can't open /proc/sys/kernel/io_uring_disabled --- Tests/SystemTests/IORingTests.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 5095f079..8e0e8fa6 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -11,12 +11,16 @@ import System #endif func uringEnabled() throws -> Bool { - let procPath = FilePath("/proc/sys/kernel/io_uring_disabled") - let fd = try FileDescriptor.open(procPath, .readOnly) - let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1024, alignment: 0) - _ = try fd.read(into: buffer) - if buffer.load(fromByteOffset: 0, as: Int.self) == 0 { - return true + do { + let procPath = FilePath("/proc/sys/kernel/io_uring_disabled") + let fd = try FileDescriptor.open(procPath, .readOnly) + let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1024, alignment: 0) + _ = try fd.read(into: buffer) + if buffer.load(fromByteOffset: 0, as: Int.self) == 0 { + return true + } + } catch (_) { + return false } return false } From 8de80270cb7898dabf7394ef4d6f64de93acc314 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Fri, 25 Jul 2025 13:57:20 -0600 Subject: [PATCH 123/172] Fix WASI build (dirent, umask, errno and open flag tests) --- Sources/CSystem/include/CSystemWASI.h | 37 +++++++++++++++++++++- Sources/System/FileOperations.swift | 4 ++- Sources/System/Internals/Syscalls.swift | 18 +++++++++-- Tests/SystemTests/ErrnoTest.swift | 20 ++++++++---- Tests/SystemTests/FileOperationsTest.swift | 16 ++++++++-- Tests/SystemTests/FileTypesTest.swift | 4 ++- 6 files changed, 83 insertions(+), 16 deletions(-) diff --git a/Sources/CSystem/include/CSystemWASI.h b/Sources/CSystem/include/CSystemWASI.h index 9877853e..1c8cd0f2 100644 --- a/Sources/CSystem/include/CSystemWASI.h +++ b/Sources/CSystem/include/CSystemWASI.h @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2024 Apple Inc. and the Swift System project authors + Copyright (c) 2024 - 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 @@ -11,8 +11,10 @@ #if __wasi__ +#include #include #include +#include // For NAME_MAX // wasi-libc defines the following constants in a way that Clang Importer can't // understand, so we need to expose them manually. @@ -28,4 +30,37 @@ static inline int32_t _getConst_O_WRONLY(void) { return O_WRONLY; } static inline int32_t _getConst_EWOULDBLOCK(void) { return EWOULDBLOCK; } static inline int32_t _getConst_EOPNOTSUPP(void) { return EOPNOTSUPP; } +static inline uint8_t _getConst_DT_DIR(void) { return DT_DIR; } + +// Modified dirent struct that can be imported to Swift +struct _system_dirent { + ino_t d_ino; + unsigned char d_type; + // char d_name[] cannot be imported to Swift + char d_name[NAME_MAX + 1]; +}; + +// Convert WASI dirent with d_name[] to _system_dirent +static inline +struct _system_dirent * +_system_dirent_from_wasi_dirent(const struct dirent *wasi_dirent) { + + // Match readdir behavior and use thread-local storage for the converted dirent + static __thread struct _system_dirent _converted_dirent; + + if (wasi_dirent == NULL) { + return NULL; + } + + memset(&_converted_dirent, 0, sizeof(struct _system_dirent)); + + _converted_dirent.d_ino = wasi_dirent->d_ino; + _converted_dirent.d_type = wasi_dirent->d_type; + + strncpy(_converted_dirent.d_name, wasi_dirent->d_name, NAME_MAX); + _converted_dirent.d_name[NAME_MAX] = '\0'; + + return &_converted_dirent; +} + #endif diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index fc9fc932..2a8509bd 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 - 2024 Apple Inc. and the Swift System project authors + Copyright (c) 2020 - 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 @@ -509,6 +509,7 @@ extension FileDescriptor { } } +#if !os(WASI) // WASI has no umask extension FilePermissions { /// The file creation permission mask (aka "umask"). /// @@ -549,3 +550,4 @@ extension FilePermissions { return system_umask(mode) } } +#endif diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index 1627273c..f6eb5339 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 - 2024 Apple Inc. and the Swift System project authors + Copyright (c) 2020 - 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 @@ -14,6 +14,7 @@ import Glibc #elseif canImport(Musl) import Musl #elseif canImport(WASILibc) +import CSystem import WASILibc #elseif os(Windows) import ucrt @@ -189,9 +190,14 @@ internal func system_confstr( #if !os(Windows) internal let SYSTEM_AT_REMOVE_DIR = AT_REMOVEDIR +#if os(WASI) +internal let SYSTEM_DT_DIR = _getConst_DT_DIR() +internal typealias system_dirent = _system_dirent +#else internal let SYSTEM_DT_DIR = DT_DIR internal typealias system_dirent = dirent -#if os(Linux) || os(Android) || os(FreeBSD) || os(OpenBSD) +#endif +#if os(Linux) || os(Android) || os(FreeBSD) || os(OpenBSD) || os(WASI) internal typealias system_DIRPtr = OpaquePointer #else internal typealias system_DIRPtr = UnsafeMutablePointer @@ -216,8 +222,12 @@ internal func system_fdopendir( internal func system_readdir( _ dir: system_DIRPtr -) -> UnsafeMutablePointer? { +) -> UnsafeMutablePointer? { + #if os(WASI) + return _system_dirent_from_wasi_dirent(readdir(dir)) + #else return readdir(dir) + #endif } internal func system_rewinddir( @@ -246,11 +256,13 @@ internal func system_openat( } #endif +#if !os(WASI) // WASI has no umask internal func system_umask( _ mode: CInterop.Mode ) -> CInterop.Mode { return umask(mode) } +#endif internal func system_getenv( _ name: UnsafePointer diff --git a/Tests/SystemTests/ErrnoTest.swift b/Tests/SystemTests/ErrnoTest.swift index da09657e..5f4551ba 100644 --- a/Tests/SystemTests/ErrnoTest.swift +++ b/Tests/SystemTests/ErrnoTest.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 Apple Inc. and the Swift System project authors + Copyright (c) 2020 - 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 @@ -36,7 +36,7 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENOMEM == Errno.noMemory.rawValue) XCTAssert(EACCES == Errno.permissionDenied.rawValue) XCTAssert(EFAULT == Errno.badAddress.rawValue) -#if !os(Windows) +#if !os(Windows) && !os(WASI) XCTAssert(ENOTBLK == Errno.notBlockDevice.rawValue) #endif XCTAssert(EBUSY == Errno.resourceBusy.rawValue) @@ -74,9 +74,11 @@ final class ErrnoTest: XCTestCase { XCTAssert(WSAEOPNOTSUPP == Errno.notSupported.rawValue) XCTAssert(WSAEPFNOSUPPORT == Errno.protocolFamilyNotSupported.rawValue) #else - XCTAssert(ESOCKTNOSUPPORT == Errno.socketTypeNotSupported.rawValue) XCTAssert(ENOTSUP == Errno.notSupported.rawValue) +#if !os(WASI) + XCTAssert(ESOCKTNOSUPPORT == Errno.socketTypeNotSupported.rawValue) XCTAssert(EPFNOSUPPORT == Errno.protocolFamilyNotSupported.rawValue) +#endif #endif XCTAssert(EAFNOSUPPORT == Errno.addressFamilyNotSupported.rawValue) XCTAssert(EADDRINUSE == Errno.addressInUse.rawValue) @@ -91,7 +93,7 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENOTCONN == Errno.socketNotConnected.rawValue) #if os(Windows) XCTAssert(WSAESHUTDOWN == Errno.socketShutdown.rawValue) -#else +#elseif !os(WASI) XCTAssert(ESHUTDOWN == Errno.socketShutdown.rawValue) #endif XCTAssert(ETIMEDOUT == Errno.timedOut.rawValue) @@ -100,7 +102,7 @@ final class ErrnoTest: XCTestCase { XCTAssert(ENAMETOOLONG == Errno.fileNameTooLong.rawValue) #if os(Windows) XCTAssert(WSAEHOSTDOWN == Errno.hostIsDown.rawValue) -#else +#elseif !os(WASI) XCTAssert(EHOSTDOWN == Errno.hostIsDown.rawValue) #endif XCTAssert(EHOSTUNREACH == Errno.noRouteToHost.rawValue) @@ -115,7 +117,9 @@ final class ErrnoTest: XCTestCase { XCTAssert(WSAEDQUOT == Errno.diskQuotaExceeded.rawValue) XCTAssert(WSAESTALE == Errno.staleNFSFileHandle.rawValue) #else +#if !os(WASI) XCTAssert(EUSERS == Errno.tooManyUsers.rawValue) +#endif XCTAssert(EDQUOT == Errno.diskQuotaExceeded.rawValue) XCTAssert(ESTALE == Errno.staleNFSFileHandle.rawValue) #endif @@ -171,7 +175,7 @@ final class ErrnoTest: XCTestCase { XCTAssert(EPROTO == Errno.protocolError.rawValue) #endif -#if !os(Windows) && !os(FreeBSD) +#if !os(Windows) && !os(FreeBSD) && !os(WASI) XCTAssert(ENODATA == Errno.noData.rawValue) XCTAssert(ENOSR == Errno.noStreamResources.rawValue) XCTAssert(ENOSTR == Errno.notStream.rawValue) @@ -181,11 +185,13 @@ final class ErrnoTest: XCTestCase { XCTAssert(EOPNOTSUPP == Errno.notSupportedOnSocket.rawValue) // From headers but not man page +#if !os(WASI) // Would need to use _getConst func from CSystem XCTAssert(EWOULDBLOCK == Errno.wouldBlock.rawValue) +#endif #if os(Windows) XCTAssert(WSAETOOMANYREFS == Errno.tooManyReferences.rawValue) XCTAssert(WSAEREMOTE == Errno.tooManyRemoteLevels.rawValue) -#else +#elseif !os(WASI) XCTAssert(ETOOMANYREFS == Errno.tooManyReferences.rawValue) XCTAssert(EREMOTE == Errno.tooManyRemoteLevels.rawValue) #endif diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 18adde37..479e503f 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 Apple Inc. and the Swift System project authors + Copyright (c) 2020 - 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 @@ -16,10 +16,13 @@ import XCTest #endif #if canImport(Android) import Android +#elseif os(WASI) +import CSystem #endif @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) final class FileOperationsTest: XCTestCase { + #if !os(WASI) // Would need to use _getConst funcs from CSystem func testSyscalls() { let fd = FileDescriptor(rawValue: 1) @@ -88,6 +91,7 @@ final class FileOperationsTest: XCTestCase { for test in syscallTestCases { test.runAllTests() } } + #endif // !os(WASI) func testWriteFromEmptyBuffer() throws { #if os(Windows) @@ -153,6 +157,7 @@ final class FileOperationsTest: XCTestCase { // TODO: Test writeAll, writeAll(toAbsoluteOffset), closeAfter } + #if !os(WASI) // WASI has no pipe func testAdHocPipe() throws { // Ad-hoc test testing `Pipe` functionality. // We cannot test `Pipe` using `MockTestCase` because it calls `pipe` with a pointer to an array local to the `Pipe`, the address of which we do not know prior to invoking `Pipe`. @@ -171,6 +176,7 @@ final class FileOperationsTest: XCTestCase { } } } + #endif func testAdHocOpen() { // Ad-hoc test touching a file system. @@ -211,8 +217,13 @@ final class FileOperationsTest: XCTestCase { func testGithubIssues() { // https://github.com/apple/swift-system/issues/26 + #if os(WASI) + let openOptions = _getConst_O_WRONLY() | _getConst_O_CREAT() + #else + let openOptions = O_WRONLY | O_CREAT + #endif let issue26 = MockTestCase( - name: "open", .interruptable, "a path", O_WRONLY | O_CREAT, 0o020 + name: "open", .interruptable, "a path", openOptions, 0o020 ) { retryOnInterrupt in _ = try FileDescriptor.open( @@ -221,7 +232,6 @@ final class FileOperationsTest: XCTestCase { retryOnInterrupt: retryOnInterrupt) } issue26.runAllTests() - } func testResizeFile() throws { diff --git a/Tests/SystemTests/FileTypesTest.swift b/Tests/SystemTests/FileTypesTest.swift index 5258709a..a818dfba 100644 --- a/Tests/SystemTests/FileTypesTest.swift +++ b/Tests/SystemTests/FileTypesTest.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2020 - 2021 Apple Inc. and the Swift System project authors + Copyright (c) 2020 - 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 @@ -32,6 +32,7 @@ final class FileDescriptorTest: XCTestCase { XCTAssertEqual(O_WRONLY, FileDescriptor.AccessMode.writeOnly.rawValue) XCTAssertEqual(O_RDWR, FileDescriptor.AccessMode.readWrite.rawValue) +#if !os(WASI) // Would need to use _getConst funcs from CSystem #if !os(Windows) XCTAssertEqual(O_NONBLOCK, FileDescriptor.OpenOptions.nonBlocking.rawValue) #endif @@ -39,6 +40,7 @@ final class FileDescriptorTest: XCTestCase { XCTAssertEqual(O_CREAT, FileDescriptor.OpenOptions.create.rawValue) XCTAssertEqual(O_TRUNC, FileDescriptor.OpenOptions.truncate.rawValue) XCTAssertEqual(O_EXCL, FileDescriptor.OpenOptions.exclusiveCreate.rawValue) +#endif // !os(WASI #if !os(Windows) XCTAssertEqual(O_NOFOLLOW, FileDescriptor.OpenOptions.noFollow.rawValue) XCTAssertEqual(O_CLOEXEC, FileDescriptor.OpenOptions.closeOnExec.rawValue) From 5059730d24a515d9fa7c3892663f4b0f24b9b332 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 28 Jul 2025 10:52:09 -0600 Subject: [PATCH 124/172] Fix IORing release build --- Sources/System/IORing/IORing.swift | 80 +++++++++++++++++------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index c676f78e..7dcdcde7 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -370,72 +370,82 @@ public struct IORing: ~Copyable { // All throws need to be before initializing ivars here to avoid // "error: conditional initialization or destruction of noncopyable types is not supported; // this variable must be consistently in an initialized or uninitialized state through every code path" - features = Features(rawValue: params.features) - ringDescriptor = tmpRingDescriptor - ringPtr = tmpRingPtr - ringSize = tmpRingSize - submissionRingPtr = tmpSQPtr - submissionRingSize = tmpSQSize - completionRingPtr = tmpCQPtr - completionRingSize = tmpCQSize - - _registeredFiles = [] - _registeredBuffers = [] - submissionRing = SQRing( + + // Pre-compute values to avoid accessing partially initialized state + let ringBasePtr = tmpRingPtr ?? tmpSQPtr! + let completionBasePtr = tmpRingPtr ?? tmpCQPtr! + + let submissionRing = SQRing( kernelHead: UnsafePointer>( - (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.head) + ringBasePtr.advanced(by: params.sq_off.head) .assumingMemoryBound(to: Atomic.self) ), kernelTail: UnsafePointer>( - (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.tail) + ringBasePtr.advanced(by: params.sq_off.tail) .assumingMemoryBound(to: Atomic.self) ), userTail: 0, // no requests yet - ringMask: (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.ring_mask) + ringMask: ringBasePtr.advanced(by: params.sq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, flags: UnsafePointer>( - (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.flags) + ringBasePtr.advanced(by: params.sq_off.flags) .assumingMemoryBound(to: Atomic.self) ), array: UnsafeMutableBufferPointer( - start: (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.array) + start: ringBasePtr.advanced(by: params.sq_off.array) .assumingMemoryBound(to: UInt32.self), count: Int( - (ringPtr ?? submissionRingPtr!).advanced(by: params.sq_off.ring_entries) + ringBasePtr.advanced(by: params.sq_off.ring_entries) .assumingMemoryBound(to: UInt32.self).pointee) ) ) - - // fill submission ring array with 1:1 map to underlying SQEs - for i in 0 ..< submissionRing.array.count { - submissionRing.array[i] = UInt32(i) - } - - submissionQueueEntries = UnsafeMutableBufferPointer( - start: sqes.assumingMemoryBound(to: io_uring_sqe.self), - count: Int(params.sq_entries) - ) - - completionRing = CQRing( + + let completionRing = CQRing( kernelHead: UnsafePointer>( - (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.head) + completionBasePtr.advanced(by: params.cq_off.head) .assumingMemoryBound(to: Atomic.self) ), kernelTail: UnsafePointer>( - (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.tail) + completionBasePtr.advanced(by: params.cq_off.tail) .assumingMemoryBound(to: Atomic.self) ), - ringMask: (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.ring_mask) + ringMask: completionBasePtr.advanced(by: params.cq_off.ring_mask) .assumingMemoryBound(to: UInt32.self).pointee, cqes: UnsafeBufferPointer( - start: (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.cqes) + start: completionBasePtr.advanced(by: params.cq_off.cqes) .assumingMemoryBound(to: io_uring_cqe.self), count: Int( - (ringPtr ?? completionRingPtr!).advanced(by: params.cq_off.ring_entries) + completionBasePtr.advanced(by: params.cq_off.ring_entries) .assumingMemoryBound(to: UInt32.self).pointee) ) ) + + let submissionQueueEntries = UnsafeMutableBufferPointer( + start: sqes.assumingMemoryBound(to: io_uring_sqe.self), + count: Int(params.sq_entries) + ) + + // Now initialize all stored properties + self.features = Features(rawValue: params.features) + self.ringDescriptor = tmpRingDescriptor + self.ringPtr = tmpRingPtr + self.ringSize = tmpRingSize + self.submissionRingPtr = tmpSQPtr + self.submissionRingSize = tmpSQSize + self.completionRingPtr = tmpCQPtr + self.completionRingSize = tmpCQSize + self._registeredFiles = [] + self._registeredBuffers = [] + self.submissionRing = submissionRing + self.completionRing = completionRing + self.submissionQueueEntries = submissionQueueEntries self.ringFlags = params.flags + + // fill submission ring array with 1:1 map to underlying SQEs + // (happens after all properties are initialized) + for i in 0 ..< self.submissionRing.array.count { + self.submissionRing.array[i] = UInt32(i) + } } @inlinable From 4f5b5fb40d597d3b3db8bf37bef0674b4f195f95 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 28 Jul 2025 12:07:34 -0600 Subject: [PATCH 125/172] Fix IORing static Linux SDK --- Sources/CSystem/include/io_uring.h | 157 ++++++++++++++++++++++++++++- Sources/System/IORing/IORing.swift | 8 +- 2 files changed, 160 insertions(+), 5 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index 0068f503..08bce8bd 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -1,9 +1,164 @@ #include #include #include - #include + +#if __has_include() #include +#else +// Minimal fallback definitions when linux/io_uring.h is not available (e.g. static SDK) +#include + +#define IORING_OFF_SQ_RING 0ULL +#define IORING_OFF_CQ_RING 0x8000000ULL +#define IORING_OFF_SQES 0x10000000ULL + +#define IORING_ENTER_GETEVENTS (1U << 0) + +#define IORING_FEAT_SINGLE_MMAP (1U << 0) +#define IORING_FEAT_NODROP (1U << 1) +#define IORING_FEAT_SUBMIT_STABLE (1U << 2) +#define IORING_FEAT_RW_CUR_POS (1U << 3) +#define IORING_FEAT_CUR_PERSONALITY (1U << 4) +#define IORING_FEAT_FAST_POLL (1U << 5) +#define IORING_FEAT_POLL_32BITS (1U << 6) +#define IORING_FEAT_SQPOLL_NONFIXED (1U << 7) +#define IORING_FEAT_EXT_ARG (1U << 8) +#define IORING_FEAT_NATIVE_WORKERS (1U << 9) +#define IORING_FEAT_RSRC_TAGS (1U << 10) +#define IORING_FEAT_CQE_SKIP (1U << 11) +#define IORING_FEAT_LINKED_FILE (1U << 12) +#define IORING_FEAT_REG_REG_RING (1U << 13) +#define IORING_FEAT_RECVSEND_BUNDLE (1U << 14) +#define IORING_FEAT_MIN_TIMEOUT (1U << 15) +#define IORING_FEAT_RW_ATTR (1U << 16) +#define IORING_FEAT_NO_IOWAIT (1U << 17) + +typedef uint8_t __u8; +typedef uint16_t __u16; +typedef uint32_t __u32; +typedef uint64_t __u64; +typedef int32_t __s32; + +#ifndef __kernel_rwf_t +typedef int __kernel_rwf_t; +#endif + +struct io_uring_sqe { + __u8 opcode; + __u8 flags; + __u16 ioprio; + __s32 fd; + union { + __u64 off; + __u64 addr2; + struct { + __u32 cmd_op; + __u32 __pad1; + }; + }; + union { + __u64 addr; + __u64 splice_off_in; + struct { + __u32 level; + __u32 optname; + }; + }; + __u32 len; + union { + __kernel_rwf_t rw_flags; + __u32 fsync_flags; + __u16 poll_events; + __u32 poll32_events; + __u32 sync_range_flags; + __u32 msg_flags; + __u32 timeout_flags; + __u32 accept_flags; + __u32 cancel_flags; + __u32 open_flags; + __u32 statx_flags; + __u32 fadvise_advice; + __u32 splice_flags; + __u32 rename_flags; + __u32 unlink_flags; + __u32 hardlink_flags; + __u32 xattr_flags; + __u32 msg_ring_flags; + __u32 uring_cmd_flags; + __u32 waitid_flags; + __u32 futex_flags; + __u32 install_fd_flags; + __u32 nop_flags; + }; + __u64 user_data; + union { + __u16 buf_index; + __u16 buf_group; + } __attribute__((packed)); + __u16 personality; + union { + __s32 splice_fd_in; + __u32 file_index; + __u32 optlen; + struct { + __u16 addr_len; + __u16 __pad3[1]; + }; + }; + union { + struct { + __u64 addr3; + __u64 __pad2[1]; + }; + __u64 optval; + __u8 cmd[0]; + }; +}; + +struct io_uring_cqe { + __u64 user_data; + __s32 res; + __u32 flags; +}; + +struct io_sqring_offsets { + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 flags; + __u32 dropped; + __u32 array; + __u32 resv1; + __u64 user_addr; +}; + +struct io_cqring_offsets { + __u32 head; + __u32 tail; + __u32 ring_mask; + __u32 ring_entries; + __u32 overflow; + __u32 cqes; + __u32 flags; + __u32 resv1; + __u64 user_addr; +}; + +struct io_uring_params { + __u32 sq_entries; + __u32 cq_entries; + __u32 flags; + __u32 sq_thread_cpu; + __u32 sq_thread_idle; + __u32 features; + __u32 wq_fd; + __u32 resv[3]; + struct io_sqring_offsets sq_off; + struct io_cqring_offsets cq_off; +}; +#endif // __has_include() #ifndef SWIFT_IORING_C_WRAPPER #define SWIFT_IORING_C_WRAPPER diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index c676f78e..66f191de 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -223,7 +223,7 @@ private func setUpRing( /* prot: */ PROT_READ | PROT_WRITE, /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_SQ_RING) + /* offset: */ off_t(IORING_OFF_SQ_RING) ) if ringPtr == MAP_FAILED { @@ -238,7 +238,7 @@ private func setUpRing( /* prot: */ PROT_READ | PROT_WRITE, /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_SQ_RING) + /* offset: */ off_t(IORING_OFF_SQ_RING) ) if sqPtr == MAP_FAILED { @@ -253,7 +253,7 @@ private func setUpRing( /* prot: */ PROT_READ | PROT_WRITE, /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_CQ_RING) + /* offset: */ off_t(IORING_OFF_CQ_RING) ) if cqPtr == MAP_FAILED { @@ -270,7 +270,7 @@ private func setUpRing( /* prot: */ PROT_READ | PROT_WRITE, /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, - /* offset: */ __off_t(IORING_OFF_SQES) + /* offset: */ off_t(IORING_OFF_SQES) ) if sqes == MAP_FAILED { From c72c7589532b1d969f32c6457a524166a28ff109 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 28 Jul 2025 17:40:24 -0700 Subject: [PATCH 126/172] Update Readme --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c18ba496..0089f681 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ To use the `SystemPackage` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: ```swift -.package(url: "https://github.com/apple/swift-system", from: "1.4.0"), +.package(url: "https://github.com/apple/swift-system", from: "1.6.0"), ``` Finally, include `"SystemPackage"` as a dependency for your executable target: @@ -41,7 +41,7 @@ Finally, include `"SystemPackage"` as a dependency for your executable target: let package = Package( // name, platforms, products, etc. dependencies: [ - .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-system", from: "1.6.0"), // other dependencies ], targets: [ @@ -87,7 +87,7 @@ The following table maps existing package releases to their minimum required Swi | Package version | Swift version | Xcode release | | ----------------------- | --------------- | ------------- | | swift-system 1.3.x | >= Swift 5.8 | >= Xcode 14.3 | -| swift-system 1.4.x | >= Swift 5.9 | >= Xcode 15.0 | +| swift-system 1.4.x through 1.6.x | >= Swift 5.9 | >= Xcode 15.0 | We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, new versions of this package require clients to upgrade to a more recent Swift toolchain release. (This allows the package to make use of new language/stdlib features, build on compiler bug fixes, and adopt new package manager functionality as soon as they are available.) Patch (i.e., bugfix) releases will not increase the required toolchain version, but any minor (i.e., new feature) release may do so. @@ -105,11 +105,13 @@ Before contributing, please read [CONTRIBUTING.md](CONTRIBUTING.md). We maintain separate branches for each active minor version of the package: -| Package version | Branch | +| Package version | Branch | | ----------------------- | ----------- | | swift-system 1.3.x | release/1.3 | -| swift-system 1.4.x (unreleased) | release/1.4 | -| swift-system 1.5.x (unreleased) | main | +| swift-system 1.4.x | release/1.4 | +| swift-system 1.5.x | release/1.5 | +| swift-system 1.6.x | release/1.6 | +| swift-system 1.7.x (unreleased) | main | Changes must land on the branch corresponding to the earliest release that they will need to ship on. They are periodically propagated to subsequent branches, in the following direction: From 3f68d422462feb65962db3d14344e0eb12cb4132 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 28 Jul 2025 17:40:24 -0700 Subject: [PATCH 127/172] Update Readme --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c18ba496..0089f681 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ To use the `SystemPackage` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: ```swift -.package(url: "https://github.com/apple/swift-system", from: "1.4.0"), +.package(url: "https://github.com/apple/swift-system", from: "1.6.0"), ``` Finally, include `"SystemPackage"` as a dependency for your executable target: @@ -41,7 +41,7 @@ Finally, include `"SystemPackage"` as a dependency for your executable target: let package = Package( // name, platforms, products, etc. dependencies: [ - .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-system", from: "1.6.0"), // other dependencies ], targets: [ @@ -87,7 +87,7 @@ The following table maps existing package releases to their minimum required Swi | Package version | Swift version | Xcode release | | ----------------------- | --------------- | ------------- | | swift-system 1.3.x | >= Swift 5.8 | >= Xcode 14.3 | -| swift-system 1.4.x | >= Swift 5.9 | >= Xcode 15.0 | +| swift-system 1.4.x through 1.6.x | >= Swift 5.9 | >= Xcode 15.0 | We'd like this package to quickly embrace Swift language and toolchain improvements that are relevant to its mandate. Accordingly, from time to time, new versions of this package require clients to upgrade to a more recent Swift toolchain release. (This allows the package to make use of new language/stdlib features, build on compiler bug fixes, and adopt new package manager functionality as soon as they are available.) Patch (i.e., bugfix) releases will not increase the required toolchain version, but any minor (i.e., new feature) release may do so. @@ -105,11 +105,13 @@ Before contributing, please read [CONTRIBUTING.md](CONTRIBUTING.md). We maintain separate branches for each active minor version of the package: -| Package version | Branch | +| Package version | Branch | | ----------------------- | ----------- | | swift-system 1.3.x | release/1.3 | -| swift-system 1.4.x (unreleased) | release/1.4 | -| swift-system 1.5.x (unreleased) | main | +| swift-system 1.4.x | release/1.4 | +| swift-system 1.5.x | release/1.5 | +| swift-system 1.6.x | release/1.6 | +| swift-system 1.7.x (unreleased) | main | Changes must land on the branch corresponding to the earliest release that they will need to ship on. They are periodically propagated to subsequent branches, in the following direction: From 5fedb102b5da7ea93a4d9dc7e3eaa74287490dc5 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 29 Jul 2025 14:24:17 -0600 Subject: [PATCH 128/172] Fix IORing build for older Linux kernel versions --- Sources/CSystem/include/io_uring.h | 172 +++++++++++++---------- Sources/System/IORing/IORing.swift | 20 +-- Sources/System/IORing/RawIORequest.swift | 10 +- 3 files changed, 119 insertions(+), 83 deletions(-) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index 08bce8bd..d8287b1a 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -3,12 +3,112 @@ #include #include +#define __SWIFT_IORING_SQE_FALLBACK_STRUCT { \ + __u8 opcode; \ + __u8 flags; \ + __u16 ioprio; \ + __s32 fd; \ + union { \ + __u64 off; \ + __u64 addr2; \ + struct { \ + __u32 cmd_op; \ + __u32 __pad1; \ + }; \ + }; \ + union { \ + __u64 addr; \ + __u64 splice_off_in; \ + struct { \ + __u32 level; \ + __u32 optname; \ + }; \ + }; \ + __u32 len; \ + union { \ + __kernel_rwf_t rw_flags; \ + __u32 fsync_flags; \ + __u16 poll_events; \ + __u32 poll32_events; \ + __u32 sync_range_flags; \ + __u32 msg_flags; \ + __u32 timeout_flags; \ + __u32 accept_flags; \ + __u32 cancel_flags; \ + __u32 open_flags; \ + __u32 statx_flags; \ + __u32 fadvise_advice; \ + __u32 splice_flags; \ + __u32 rename_flags; \ + __u32 unlink_flags; \ + __u32 hardlink_flags; \ + __u32 xattr_flags; \ + __u32 msg_ring_flags; \ + __u32 uring_cmd_flags; \ + __u32 waitid_flags; \ + __u32 futex_flags; \ + __u32 install_fd_flags; \ + __u32 nop_flags; \ + }; \ + __u64 user_data; \ + union { \ + __u16 buf_index; \ + __u16 buf_group; \ + } __attribute__((packed)); \ + __u16 personality; \ + union { \ + __s32 splice_fd_in; \ + __u32 file_index; \ + __u32 optlen; \ + struct { \ + __u16 addr_len; \ + __u16 __pad3[1]; \ + }; \ + }; \ + union { \ + struct { \ + __u64 addr3; \ + __u64 __pad2[1]; \ + }; \ + __u64 optval; \ + __u8 cmd[0]; \ + }; \ +} + #if __has_include() #include + +#ifdef IORING_TIMEOUT_BOOTTIME +// Kernel version >= 5.15, io_uring_sqe has file_index +// and all current Swift operations are supported. +#define __SWIFT_IORING_SUPPORTED true +typedef struct io_uring_sqe swift_io_uring_sqe; +#else +// io_uring_sqe is missing properties that IORequest expects. +// This configuration is not supported for now. +// +// Define a fallback struct to avoid build errors, but IORing +// will throw ENOTSUP on initialization. +#define __SWIFT_IORING_SUPPORTED false +typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe; +#endif + +// We can define more specific availability later + +#ifdef IORING_FEAT_RW_CUR_POS +// Kernel version >= 5.6, io_uring_sqe has open_flags +#endif + +#ifdef IORING_FEAT_NODROP +// Kernel version >= 5.5, io_uring_sqe has cancel_flags +#endif + #else // Minimal fallback definitions when linux/io_uring.h is not available (e.g. static SDK) #include +#define __SWIFT_IORING_SUPPORTED false + #define IORING_OFF_SQ_RING 0ULL #define IORING_OFF_CQ_RING 0x8000000ULL #define IORING_OFF_SQES 0x10000000ULL @@ -44,77 +144,7 @@ typedef int32_t __s32; typedef int __kernel_rwf_t; #endif -struct io_uring_sqe { - __u8 opcode; - __u8 flags; - __u16 ioprio; - __s32 fd; - union { - __u64 off; - __u64 addr2; - struct { - __u32 cmd_op; - __u32 __pad1; - }; - }; - union { - __u64 addr; - __u64 splice_off_in; - struct { - __u32 level; - __u32 optname; - }; - }; - __u32 len; - union { - __kernel_rwf_t rw_flags; - __u32 fsync_flags; - __u16 poll_events; - __u32 poll32_events; - __u32 sync_range_flags; - __u32 msg_flags; - __u32 timeout_flags; - __u32 accept_flags; - __u32 cancel_flags; - __u32 open_flags; - __u32 statx_flags; - __u32 fadvise_advice; - __u32 splice_flags; - __u32 rename_flags; - __u32 unlink_flags; - __u32 hardlink_flags; - __u32 xattr_flags; - __u32 msg_ring_flags; - __u32 uring_cmd_flags; - __u32 waitid_flags; - __u32 futex_flags; - __u32 install_fd_flags; - __u32 nop_flags; - }; - __u64 user_data; - union { - __u16 buf_index; - __u16 buf_group; - } __attribute__((packed)); - __u16 personality; - union { - __s32 splice_fd_in; - __u32 file_index; - __u32 optlen; - struct { - __u16 addr_len; - __u16 __pad3[1]; - }; - }; - union { - struct { - __u64 addr3; - __u64 __pad2[1]; - }; - __u64 optval; - __u8 cmd[0]; - }; -}; +typedef struct __SWIFT_IORING_SQE_FALLBACK_STRUCT swift_io_uring_sqe; struct io_uring_cqe { __u64 user_data; diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index f37d925a..a751a049 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -72,7 +72,7 @@ extension UnsafeMutableRawBufferPointer { @inline(__always) @inlinable internal func _tryWriteRequest( _ request: __owned RawIORequest, ring: inout SQRing, - submissionQueueEntries: UnsafeMutableBufferPointer + submissionQueueEntries: UnsafeMutableBufferPointer ) -> Bool { @@ -151,9 +151,9 @@ internal func _flushQueue(ring: borrowing SQRing) -> UInt32 { @inlinable internal func _getSubmissionEntry( - ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer + ring: inout SQRing, submissionQueueEntries: UnsafeMutableBufferPointer ) -> UnsafeMutablePointer< - io_uring_sqe + swift_io_uring_sqe >? { let next = ring.userTail &+ 1 //this is expected to wrap @@ -196,7 +196,7 @@ private func setUpRing( throw err } - if params.features & IORING_FEAT_NODROP == 0 + if params.features & IORing.Features.nonDroppingCompletions.rawValue == 0 { close(ringDescriptor) throw Errno.invalidArgument @@ -266,7 +266,7 @@ private func setUpRing( // map the submission queue let sqes = mmap( /* addr: */ nil, - /* len: */ Int(params.sq_entries) * MemoryLayout.size, + /* len: */ Int(params.sq_entries) * MemoryLayout.size, /* prot: */ PROT_READ | PROT_WRITE, /* flags: */ MAP_SHARED | MAP_POPULATE, /* fd: */ ringDescriptor, @@ -307,7 +307,7 @@ public struct IORing: ~Copyable { @usableFromInline let completionRing: CQRing - @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer + @usableFromInline let submissionQueueEntries: UnsafeMutableBufferPointer // kept around for unmap / cleanup. TODO: we can save a few words of memory by figuring out how to handle cleanup for non-IORING_FEAT_SINGLE_MMAP better let ringSize: Int @@ -366,6 +366,10 @@ public struct IORing: ~Copyable { /// Initializes an IORing with enough space for `queueDepth` prepared requests and completed operations public init(queueDepth: UInt32, flags: SetupFlags = []) throws(Errno) { + guard __SWIFT_IORING_SUPPORTED != 0 else { + throw Errno.notSupported + } + let (params, tmpRingDescriptor, tmpRingPtr, tmpRingSize, tmpSQPtr, tmpSQSize, tmpCQPtr, tmpCQSize, sqes) = try setUpRing(queueDepth: queueDepth, flags: flags) // All throws need to be before initializing ivars here to avoid // "error: conditional initialization or destruction of noncopyable types is not supported; @@ -421,7 +425,7 @@ public struct IORing: ~Copyable { ) let submissionQueueEntries = UnsafeMutableBufferPointer( - start: sqes.assumingMemoryBound(to: io_uring_sqe.self), + start: sqes.assumingMemoryBound(to: swift_io_uring_sqe.self), count: Int(params.sq_entries) ) @@ -868,7 +872,7 @@ public struct IORing: ~Copyable { } munmap( UnsafeMutableRawPointer(submissionQueueEntries.baseAddress!), - submissionQueueEntries.count * MemoryLayout.size + submissionQueueEntries.count * MemoryLayout.size ) close(ringDescriptor) } diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index dea378fb..4958a480 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -5,11 +5,13 @@ import CSystem @usableFromInline internal struct RawIORequest: ~Copyable { - @usableFromInline var rawValue: io_uring_sqe + // swift_io_uring_sqe is a typedef of io_uring_sqe on platforms where + // IORing is supported (currently requires kernel version >= 5.15). + @usableFromInline var rawValue: swift_io_uring_sqe @usableFromInline var path: FilePath? //buffer owner for the path pointer that the sqe may have @inlinable public init() { - self.rawValue = io_uring_sqe() + self.rawValue = swift_io_uring_sqe() } } @@ -176,8 +178,8 @@ extension RawIORequest { @inlinable static func withTimeoutRequest( - linkedTo opEntry: UnsafeMutablePointer, - in timeoutEntry: UnsafeMutablePointer, + linkedTo opEntry: UnsafeMutablePointer, + in timeoutEntry: UnsafeMutablePointer, duration: Duration, flags: TimeOutFlags, work: () throws -> R) rethrows -> R { From d75acaac3a832cc4ce55ec7728deaf44082e07f6 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Mon, 28 Jul 2025 15:45:23 -0700 Subject: [PATCH 129/172] [ci] expand github actions --- .github/workflows/pull_request.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d543be29..ec43b5b5 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,10 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_exclude_swift_versions: '[{"swift_version": "5.8"}]' + linux_os_versions: '["jammy", "focal"]' + enable_macos_checks: false + macos_xcode_versions: '["16.3"]' + soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main From d32abd8c6ea5482fad9e15f85e532d01cc5a777d Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 29 Jul 2025 15:35:30 -0600 Subject: [PATCH 130/172] Fix warning: will never be executed --- Sources/System/IORing/IORing.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index a751a049..3e54c789 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -10,6 +10,10 @@ import Musl #endif import Synchronization +private var ioringSupported: Bool { + __SWIFT_IORING_SUPPORTED != 0 +} + //This was #defines in older headers, so we redeclare it to get a consistent import internal enum RegistrationOps: UInt32 { case registerBuffers = 0 @@ -366,7 +370,7 @@ public struct IORing: ~Copyable { /// Initializes an IORing with enough space for `queueDepth` prepared requests and completed operations public init(queueDepth: UInt32, flags: SetupFlags = []) throws(Errno) { - guard __SWIFT_IORING_SUPPORTED != 0 else { + guard ioringSupported else { throw Errno.notSupported } From 9a1530813e138699aca84b0408e4adf2cfa78ed8 Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Sat, 7 Jun 2025 10:49:36 -0700 Subject: [PATCH 131/172] Replace bespoke availability system Removes the script based availability system with the experimental availability macro feature. --- .github/workflows/pull_request.yml | 23 +++- Package.swift | 83 ++++++++++++- Sources/System/Errno.swift | 12 +- Sources/System/FileDescriptor.swift | 18 +-- Sources/System/FileHelpers.swift | 2 +- Sources/System/FileOperations.swift | 20 +-- Sources/System/FilePath/FilePath.swift | 8 +- .../FilePath/FilePathComponentView.swift | 16 +-- .../System/FilePath/FilePathComponents.swift | 28 ++--- Sources/System/FilePath/FilePathParsing.swift | 16 +-- Sources/System/FilePath/FilePathString.swift | 34 ++--- Sources/System/FilePath/FilePathSyntax.swift | 16 +-- Sources/System/FilePermissions.swift | 4 +- Sources/System/Internals/CInterop.swift | 4 +- Sources/System/MachPort.swift | 32 ++--- Sources/System/PlatformString.swift | 6 +- Sources/System/Util.swift | 8 +- Tests/SystemTests/ErrnoTest.swift | 2 +- Tests/SystemTests/FileOperationsTest.swift | 2 +- .../FilePathComponentsTest.swift | 4 +- .../FilePathTests/FilePathDecodable.swift | 2 +- .../FilePathTests/FilePathExtras.swift | 4 +- .../FilePathTests/FilePathParsingTest.swift | 2 +- .../FilePathTests/FilePathSyntaxTest.swift | 6 +- .../FilePathTests/FilePathTest.swift | 4 +- Tests/SystemTests/FileTypesTest.swift | 4 +- Tests/SystemTests/MachPortTests.swift | 2 +- Tests/SystemTests/MockingTest.swift | 2 +- Utilities/expand-availability.py | 117 ------------------ 29 files changed, 230 insertions(+), 251 deletions(-) delete mode 100755 Utilities/expand-availability.py diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ec43b5b5..d4949eb1 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,8 +10,27 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_os_versions: '["jammy", "focal"]' - enable_macos_checks: false - macos_xcode_versions: '["16.3"]' + enable_macos_checks: true + swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" + + build-abi-stable: + name: Build ABI Stable + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + enable_linux_checks: false + enable_macos_checks: true + enable_windows_checks: false + # Only build + macos_build_command: "xcrun swift build --build-tests" + # Only test against latest Xcode + macos_exclude_xcode_versions: | + [ + {"xcode_version": "16.0"}, + {"xcode_version": "16.1"}, + {"xcode_version": "16.2"}, + ] + # Enable availability to match ABI stable verion of system. + swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI -Xbuild-tools-swiftc -DSYSTEM_ABI_STABLE" soundness: name: Soundness diff --git a/Package.swift b/Package.swift index 9cbfc6e9..74a70160 100644 --- a/Package.swift +++ b/Package.swift @@ -12,11 +12,70 @@ import PackageDescription -let cSettings: [CSetting] = [ - .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), +struct Available { + var name: String + var version: String + var osAvailability: String + var sourceAvailability: String + + init( + _ version: String, + _ osAvailability: String + ) { + self.name = "System" + self.version = version + self.osAvailability = osAvailability + self.sourceAvailability = "macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, visionOS 1.0" + } + + var swiftSetting: SwiftSetting { + #if SYSTEM_ABI_STABLE + // Use availability matching Darwin API. + let availability = self.osAvailability + #else + // Use availability matching SwiftPM default. + let availability = self.sourceAvailability + #endif + return .enableExperimentalFeature( + "AvailabilityMacro=\(self.name) \(version):\(availability)") + } +} + +let availability: [Available] = [ + Available("0.0.1", "macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0"), + + Available("0.0.2", "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0"), + + Available("0.0.3", "macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4"), + Available("1.1.0", "macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4"), + + Available("1.1.1", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + Available("1.2.0", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + + Available("1.2.1", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + Available("1.3.0", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4"), + + Available("1.3.1", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4, visionOS 1.0"), + Available("1.3.2", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4, visionOS 1.0"), + Available("1.4.0", "macOS 14.4, iOS 17.4, watchOS 10.4, tvOS 17.4, visionOS 1.0"), + + Available("1.4.1", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.4.2", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.5.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.6.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), +] + +let swiftSettingsAvailability = availability.map(\.swiftSetting) + +#if SYSTEM_CI +let swiftSettingsCI: [SwiftSetting] = [ + .unsafeFlags(["-require-explicit-availability=error"]), ] +#else +let swiftSettingsCI: [SwiftSetting] = [] +#endif -let swiftSettings: [SwiftSetting] = [ +let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ .define( "SYSTEM_PACKAGE_DARWIN", .when(platforms: [.macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .visionOS])), @@ -25,6 +84,22 @@ let swiftSettings: [SwiftSetting] = [ .enableExperimentalFeature("Lifetimes"), ] +let cSettings: [CSetting] = [ + .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), +] + +#if SYSTEM_ABI_STABLE +let platforms: [SupportedPlatform] = [ + .macOS("26"), + .iOS("26"), + .watchOS("26"), + .tvOS("26"), + .visionOS("26"), +] +#else +let platforms: [SupportedPlatform]? = nil +#endif + #if os(Linux) let filesToExclude = ["CMakeLists.txt"] #else @@ -39,6 +114,7 @@ let testsToExclude = ["IORequestTests.swift", "IORingTests.swift"] let package = Package( name: "swift-system", + platforms: platforms, products: [ .library(name: "SystemPackage", targets: ["SystemPackage"]), ], @@ -63,3 +139,4 @@ let package = Package( cSettings: cSettings, swiftSettings: swiftSettings), ]) + diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index e88b96d9..43b46af5 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -10,7 +10,7 @@ /// An error number used by system calls to communicate what kind of error /// occurred. @frozen -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) public struct Errno: RawRepresentable, Error, Hashable, Codable { /// The raw C error number. @_alwaysEmitIntoClient @@ -1391,7 +1391,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { } // Constants defined in header but not man page -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension Errno { /// Operation would block. /// @@ -1520,7 +1520,7 @@ extension Errno { #endif } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension Errno { // TODO: We want to provide safe access to `errno`, but we need a // release-barrier to do so. @@ -1535,14 +1535,14 @@ extension Errno { } // Use "hidden" entry points for `NSError` bridging -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension Errno { public var _code: Int { Int(rawValue) } public var _domain: String { "NSPOSIXErrorDomain" } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension Errno: CustomStringConvertible, CustomDebugStringConvertible { /// A textual representation of the most recent error /// returned by a system call. @@ -1562,7 +1562,7 @@ extension Errno: CustomStringConvertible, CustomDebugStringConvertible { public var debugDescription: String { self.description } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension Errno { @_alwaysEmitIntoClient public static func ~=(_ lhs: Errno, _ rhs: Error) -> Bool { diff --git a/Sources/System/FileDescriptor.swift b/Sources/System/FileDescriptor.swift index c9953e8b..d5f5883b 100644 --- a/Sources/System/FileDescriptor.swift +++ b/Sources/System/FileDescriptor.swift @@ -14,7 +14,7 @@ /// of `FileDescriptor` values, /// in the same way as you manage a raw C file handle. @frozen -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) public struct FileDescriptor: RawRepresentable, Hashable, Codable { /// The raw C file handle. @_alwaysEmitIntoClient @@ -26,7 +26,7 @@ public struct FileDescriptor: RawRepresentable, Hashable, Codable { } // Standard file descriptors. -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor { /// The standard input file descriptor, with a numeric value of 0. @_alwaysEmitIntoClient @@ -41,11 +41,11 @@ extension FileDescriptor { public static var standardError: FileDescriptor { .init(rawValue: 2) } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor { /// The desired read and write access for a newly opened file. @frozen - @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) + @available(System 0.0.1, *) public struct AccessMode: RawRepresentable, Sendable, Hashable, Codable { /// The raw C access mode. @_alwaysEmitIntoClient @@ -88,7 +88,7 @@ extension FileDescriptor { /// Options that specify behavior for a newly-opened file. @frozen - @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) + @available(System 0.0.1, *) public struct OpenOptions: OptionSet, Sendable, Hashable, Codable { /// The raw C options. @_alwaysEmitIntoClient @@ -326,7 +326,7 @@ extension FileDescriptor { /// Options for specifying what a file descriptor's offset is relative to. @frozen - @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) + @available(System 0.0.1, *) public struct SeekOrigin: RawRepresentable, Sendable, Hashable, Codable { /// The raw C value. @_alwaysEmitIntoClient @@ -402,7 +402,7 @@ extension FileDescriptor { } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor.AccessMode : CustomStringConvertible, CustomDebugStringConvertible { @@ -421,7 +421,7 @@ extension FileDescriptor.AccessMode public var debugDescription: String { self.description } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor.SeekOrigin : CustomStringConvertible, CustomDebugStringConvertible { @@ -444,7 +444,7 @@ extension FileDescriptor.SeekOrigin public var debugDescription: String { self.description } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor.OpenOptions : CustomStringConvertible, CustomDebugStringConvertible { diff --git a/Sources/System/FileHelpers.swift b/Sources/System/FileHelpers.swift index 2ddb0729..5b082766 100644 --- a/Sources/System/FileHelpers.swift +++ b/Sources/System/FileHelpers.swift @@ -7,7 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor { /// Runs a closure and then closes the file descriptor, even if an error occurs. /// diff --git a/Sources/System/FileOperations.swift b/Sources/System/FileOperations.swift index 2a8509bd..9ddc16c3 100644 --- a/Sources/System/FileOperations.swift +++ b/Sources/System/FileOperations.swift @@ -7,7 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FileDescriptor { /// Opens or creates a file for reading or writing. /// @@ -368,7 +368,7 @@ extension FileDescriptor { } #if !os(WASI) -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FileDescriptor { /// Duplicates this file descriptor and return the newly created copy. /// @@ -398,7 +398,7 @@ extension FileDescriptor { /// /// The corresponding C functions are `dup` and `dup2`. @_alwaysEmitIntoClient - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) public func duplicate( as target: FileDescriptor? = nil, retryOnInterrupt: Bool = true @@ -406,7 +406,7 @@ extension FileDescriptor { try _duplicate(as: target, retryOnInterrupt: retryOnInterrupt).get() } - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) @usableFromInline internal func _duplicate( as target: FileDescriptor?, @@ -435,7 +435,7 @@ extension FileDescriptor { #endif #if !os(WASI) -@available(/*System 1.1.0: macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4*/iOS 8, *) +@available(System 1.1.0, *) extension FileDescriptor { /// Creates a unidirectional data channel, which can be used for interprocess communication. /// @@ -443,12 +443,12 @@ extension FileDescriptor { /// /// The corresponding C function is `pipe`. @_alwaysEmitIntoClient - @available(/*System 1.1.0: macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4*/iOS 8, *) + @available(System 1.1.0, *) public static func pipe() throws -> (readEnd: FileDescriptor, writeEnd: FileDescriptor) { try _pipe().get() } - @available(/*System 1.1.0: macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4*/iOS 8, *) + @available(System 1.1.0, *) @usableFromInline internal static func _pipe() -> Result<(readEnd: FileDescriptor, writeEnd: FileDescriptor), Errno> { var fds: (Int32, Int32) = (-1, -1) @@ -463,7 +463,7 @@ extension FileDescriptor { } #endif -@available(/*System 1.2.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.2.0, *) extension FileDescriptor { /// Truncates or extends the file referenced by this file descriptor. /// @@ -485,7 +485,7 @@ extension FileDescriptor { /// associated with the file. /// /// The corresponding C function is `ftruncate`. - @available(/*System 1.2.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.2.0, *) @_alwaysEmitIntoClient public func resize( to newSize: Int64, @@ -497,7 +497,7 @@ extension FileDescriptor { ).get() } - @available(/*System 1.2.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.2.0, *) @usableFromInline internal func _resize( to newSize: Int64, diff --git a/Sources/System/FilePath/FilePath.swift b/Sources/System/FilePath/FilePath.swift index f056759d..b27d053a 100644 --- a/Sources/System/FilePath/FilePath.swift +++ b/Sources/System/FilePath/FilePath.swift @@ -37,7 +37,7 @@ /// However, the rules for path equivalence /// are file-system–specific and have additional considerations /// like case insensitivity, Unicode normalization, and symbolic links. -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) public struct FilePath: Sendable { // TODO(docs): Section on all the new syntactic operations, lexical normalization, decomposition, // components, etc. @@ -59,16 +59,16 @@ public struct FilePath: Sendable { } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { /// The length of the file path, excluding the null terminator. public var length: Int { _storage.length } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath: Hashable {} -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath: Codable { // Encoder is synthesized; it probably should have been explicit and used // a single-value container, but making that change now is somewhat risky. diff --git a/Sources/System/FilePath/FilePathComponentView.swift b/Sources/System/FilePath/FilePathComponentView.swift index 39381b4b..be176305 100644 --- a/Sources/System/FilePath/FilePathComponentView.swift +++ b/Sources/System/FilePath/FilePathComponentView.swift @@ -9,7 +9,7 @@ // MARK: - API -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// A bidirectional, range replaceable collection of the non-root components /// that make up a file path. @@ -28,7 +28,7 @@ extension FilePath { /// /// path.components.removeAll { $0.kind == .currentDirectory } /// // path is "/home/username/bin/scripts/tree" - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) public struct ComponentView: Sendable { internal var _path: FilePath internal var _start: SystemString.Index @@ -64,11 +64,11 @@ extension FilePath { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.ComponentView: BidirectionalCollection { public typealias Element = FilePath.Component - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) public struct Index: Sendable, Comparable, Hashable { internal typealias Storage = SystemString.Index @@ -100,7 +100,7 @@ extension FilePath.ComponentView: BidirectionalCollection { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.ComponentView: RangeReplaceableCollection { public init() { self.init(FilePath()) @@ -150,7 +150,7 @@ extension FilePath.ComponentView: RangeReplaceableCollection { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Create a file path from a root and a collection of components. public init( @@ -179,7 +179,7 @@ extension FilePath { // MARK: - Internals -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.ComponentView: _PathSlice { internal var _range: Range { _start ..< _path._storage.endIndex @@ -192,7 +192,7 @@ extension FilePath.ComponentView: _PathSlice { // MARK: - Invariants -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.ComponentView { internal func _invariantCheck() { #if DEBUG diff --git a/Sources/System/FilePath/FilePathComponents.swift b/Sources/System/FilePath/FilePathComponents.swift index b304a0a7..f2352617 100644 --- a/Sources/System/FilePath/FilePathComponents.swift +++ b/Sources/System/FilePath/FilePathComponents.swift @@ -9,7 +9,7 @@ // MARK: - API -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Represents a root of a file path. /// @@ -28,7 +28,7 @@ extension FilePath { /// * `\\server\share\` /// * `\\?\UNC\server\share\` /// * `\\?\Volume{12345678-abcd-1111-2222-123445789abc}\` - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) public struct Root: Sendable { internal var _path: FilePath internal var _rootEnd: SystemString.Index @@ -55,7 +55,7 @@ extension FilePath { /// file.kind == .regular // true /// file.extension // "txt" /// path.append(file) // path is "/tmp/foo.txt" - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) public struct Component: Sendable { internal var _path: FilePath internal var _range: Range @@ -74,13 +74,13 @@ extension FilePath { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { /// Whether a component is a regular file or directory name, or a special /// directory `.` or `..` @frozen - @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) + @available(System 0.0.2, *) public enum Kind: Sendable { /// The special directory `.`, representing the current directory. case currentDirectory @@ -100,7 +100,7 @@ extension FilePath.Component { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root { // TODO: Windows analysis APIs } @@ -186,17 +186,17 @@ extension _PathSlice { internal var _storage: SystemString { _path._storage } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component: _PathSlice { } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root: _PathSlice { internal var _range: Range { (..<_rootEnd).relative(to: _path._storage) } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath: _PlatformStringable { func _withPlatformString(_ body: (UnsafePointer) throws -> Result) rethrows -> Result { try _storage.withPlatformString(body) @@ -208,7 +208,7 @@ extension FilePath: _PlatformStringable { } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { // The index of the `.` denoting an extension internal func _extensionIndex() -> SystemString.Index? { @@ -237,7 +237,7 @@ internal func _makeExtension(_ ext: String) -> SystemString { return result } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { internal init?(_ str: SystemString) { // FIXME: explicit null root? Or something else? @@ -250,7 +250,7 @@ extension FilePath.Component { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root { internal init?(_ str: SystemString) { // FIXME: explicit null root? Or something else? @@ -265,7 +265,7 @@ extension FilePath.Root { // MARK: - Invariants -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { // TODO: ensure this all gets easily optimized away in release... internal func _invariantCheck() { @@ -278,7 +278,7 @@ extension FilePath.Component { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root { internal func _invariantCheck() { #if DEBUG diff --git a/Sources/System/FilePath/FilePathParsing.swift b/Sources/System/FilePath/FilePathParsing.swift index c31e6a5b..f372dd67 100644 --- a/Sources/System/FilePath/FilePathParsing.swift +++ b/Sources/System/FilePath/FilePathParsing.swift @@ -116,7 +116,7 @@ extension SystemString { } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { internal mutating func _removeTrailingSeparator() { _storage._removeTrailingSeparator() @@ -197,7 +197,7 @@ extension SystemString { } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { internal var _relativeStart: SystemString.Index { _storage._relativePathStart @@ -209,7 +209,7 @@ extension FilePath { // Parse separators -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { internal typealias _Index = SystemString.Index @@ -273,7 +273,7 @@ extension FilePath { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.ComponentView { // TODO: Store this... internal var _relativeStart: SystemString.Index { @@ -298,7 +298,7 @@ extension SystemString { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root { // Asserting self is a root, returns whether this is an // absolute root. @@ -323,7 +323,7 @@ extension FilePath.Root { } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { internal var _portableDescription: String { guard _windowsPaths else { return description } @@ -345,7 +345,7 @@ internal var _windowsPaths: Bool { #endif } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { // Whether we should add a separator when doing an append internal var _needsSeparatorForAppend: Bool { @@ -373,7 +373,7 @@ extension FilePath { } // MARK: - Invariants -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { internal func _invariantsSatisfied() -> Bool { var normal = self diff --git a/Sources/System/FilePath/FilePathString.swift b/Sources/System/FilePath/FilePathString.swift index 4fe9c1fd..45f79c8a 100644 --- a/Sources/System/FilePath/FilePathString.swift +++ b/Sources/System/FilePath/FilePathString.swift @@ -9,7 +9,7 @@ // MARK: - Platform string -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Creates a file path by copying bytes from a null-terminated platform /// string. @@ -109,7 +109,7 @@ extension FilePath { #endif } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { /// Creates a file path component by copying bytes from a null-terminated /// platform string. @@ -198,7 +198,7 @@ extension FilePath.Component { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root { /// Creates a file path root by copying bytes from a null-terminated platform /// string. @@ -287,7 +287,7 @@ extension FilePath.Root { // MARK: - String literals -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath: ExpressibleByStringLiteral { /// Creates a file path from a string literal. /// @@ -306,7 +306,7 @@ extension FilePath: ExpressibleByStringLiteral { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component: ExpressibleByStringLiteral { /// Create a file path component from a string literal. /// @@ -331,7 +331,7 @@ extension FilePath.Component: ExpressibleByStringLiteral { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root: ExpressibleByStringLiteral { /// Create a file path root from a string literal. /// @@ -356,7 +356,7 @@ extension FilePath.Root: ExpressibleByStringLiteral { // MARK: - Printing and dumping -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath: CustomStringConvertible, CustomDebugStringConvertible { /// A textual representation of the file path. /// @@ -372,7 +372,7 @@ extension FilePath: CustomStringConvertible, CustomDebugStringConvertible { public var debugDescription: String { description.debugDescription } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component: CustomStringConvertible, CustomDebugStringConvertible { /// A textual representation of the path component. @@ -389,7 +389,7 @@ extension FilePath.Component: CustomStringConvertible, CustomDebugStringConverti public var debugDescription: String { description.debugDescription } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root: CustomStringConvertible, CustomDebugStringConvertible { /// A textual representation of the path root. @@ -409,7 +409,7 @@ extension FilePath.Root: CustomStringConvertible, CustomDebugStringConvertible { // MARK: - Convenience helpers // Convenience helpers -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Creates a string by interpreting the path’s content as UTF-8 on Unix /// and UTF-16 on Windows. @@ -420,7 +420,7 @@ extension FilePath { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { /// Creates a string by interpreting the component’s content as UTF-8 on Unix /// and UTF-16 on Windows. @@ -431,7 +431,7 @@ extension FilePath.Component { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Root { /// On Unix, this returns `"/"`. /// @@ -445,7 +445,7 @@ extension FilePath.Root { // MARK: - Decoding and validating -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension String { /// Creates a string by interpreting the file path's content as UTF-8 on Unix /// and UTF-16 on Windows. @@ -475,7 +475,7 @@ extension String { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension String { /// Creates a string by interpreting the path component's content as UTF-8 on /// Unix and UTF-16 on Windows. @@ -505,7 +505,7 @@ extension String { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension String { /// On Unix, creates the string `"/"` /// @@ -558,7 +558,7 @@ extension String { // MARK: - Deprecations -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension String { @available(*, deprecated, renamed: "init(decoding:)") public init(_ path: FilePath) { self.init(decoding: path) } @@ -568,7 +568,7 @@ extension String { } #if !os(Windows) -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePath { /// For backwards compatibility only. This initializer is equivalent to /// the preferred `FilePath(platformString:)`. diff --git a/Sources/System/FilePath/FilePathSyntax.swift b/Sources/System/FilePath/FilePathSyntax.swift index 1c3fc097..eb1f25df 100644 --- a/Sources/System/FilePath/FilePathSyntax.swift +++ b/Sources/System/FilePath/FilePathSyntax.swift @@ -9,7 +9,7 @@ // MARK: - Query API -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Returns true if this path uniquely identifies the location of /// a file without reference to an additional starting location. @@ -99,7 +99,7 @@ extension FilePath { } // MARK: - Decompose a path -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Returns the root of a path if there is one, otherwise `nil`. /// @@ -182,7 +182,7 @@ extension FilePath { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Returns the final component of the path. /// Returns `nil` if the path is empty or only contains a root. @@ -252,7 +252,7 @@ extension FilePath { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath.Component { /// The extension of this file or directory component. /// @@ -283,7 +283,7 @@ extension FilePath.Component { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// The extension of the file or directory last component. @@ -349,7 +349,7 @@ extension FilePath { } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Whether the path is in lexical-normal form, that is `.` and `..` /// components have been collapsed lexically (i.e. without following @@ -430,7 +430,7 @@ extension FilePath { } // Modification and concatenation API -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { // TODO(Windows docs): example with roots /// If `prefix` is a prefix of `self`, removes it and returns `true`. @@ -583,7 +583,7 @@ extension FilePath { } // MARK - Renamed -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { @available(*, unavailable, renamed: "removingLastComponent()") public var dirname: FilePath { removingLastComponent() } diff --git a/Sources/System/FilePermissions.swift b/Sources/System/FilePermissions.swift index 918246ef..f849a997 100644 --- a/Sources/System/FilePermissions.swift +++ b/Sources/System/FilePermissions.swift @@ -17,7 +17,7 @@ /// let perms = FilePermissions(rawValue: 0o644) /// perms == [.ownerReadWrite, .groupRead, .otherRead] // true @frozen -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) public struct FilePermissions: OptionSet, Sendable, Hashable, Codable { /// The raw C file permissions. @_alwaysEmitIntoClient @@ -132,7 +132,7 @@ public struct FilePermissions: OptionSet, Sendable, Hashable, Codable { public static var saveText: FilePermissions { .init(rawValue: 0o1000) } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) extension FilePermissions : CustomStringConvertible, CustomDebugStringConvertible { diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 80d37d05..b6de1233 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -37,12 +37,12 @@ import Bionic public typealias CModeT = CInt #else /// The C `mode_t` type. -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) public typealias CModeT = mode_t #endif /// A namespace for C and platform types -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) public enum CInterop { #if os(Windows) public typealias Mode = CInt diff --git a/Sources/System/MachPort.swift b/Sources/System/MachPort.swift index 8cc05096..313f6c28 100644 --- a/Sources/System/MachPort.swift +++ b/Sources/System/MachPort.swift @@ -11,10 +11,10 @@ import Darwin.Mach -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) public protocol MachPortRight {} -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) @inlinable internal func _machPrecondition( file: StaticString = #file, @@ -26,10 +26,10 @@ internal func _machPrecondition( precondition(kr == expected, file: file, line: line) } -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) @frozen public enum Mach { - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public struct Port: ~Copyable { @usableFromInline internal var _name: mach_port_name_t @@ -133,7 +133,7 @@ public enum Mach { public struct SendOnceRight: MachPortRight {} } -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) extension Mach.Port where RightType == Mach.ReceiveRight { /// Transfer ownership of an existing, unmanaged, but already guarded, /// Mach port right into a Mach.Port by name. @@ -158,7 +158,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// This initializer will abort if the right could not be created. /// Callers may assert that a valid right is always returned. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public init() { var storage: mach_port_name_t = mach_port_name_t(MACH_PORT_NULL) _machPrecondition( @@ -181,7 +181,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// After this function completes, the Mach.Port is destroyed and no longer /// usable. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public consuming func relinquish( ) -> (name: mach_port_name_t, context: mach_port_context_t) { let destructured = (name: _name, context: _context) @@ -204,7 +204,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// Mach.ReceiveRights. Use relinquish() to avoid the syscall and extract /// the context value along with the port name. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public consuming func unguardAndRelinquish() -> mach_port_name_t { let (name, context) = self.relinquish() _machPrecondition(mach_port_unguard(mach_task_self_, name, context)) @@ -221,7 +221,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// The body block may optionally return something, which will then be /// returned to the caller of withBorrowedName. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public func withBorrowedName( body: (mach_port_name_t, mach_port_context_t) -> ReturnType ) -> ReturnType { @@ -235,7 +235,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// This function will abort if the right could not be created. /// Callers may assert that a valid right is always returned. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public func makeSendOnceRight() -> Mach.Port { // send once rights do not coalesce var newRight: mach_port_name_t = mach_port_name_t(MACH_PORT_NULL) @@ -264,7 +264,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// This function will abort if the right could not be created. /// Callers may assert that a valid right is always returned. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public func makeSendRight() -> Mach.Port { let how = MACH_MSG_TYPE_MAKE_SEND @@ -282,7 +282,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { /// /// Each get/set of this property makes a syscall. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public var makeSendCount: mach_port_mscount_t { get { var status: mach_port_status = mach_port_status() @@ -311,7 +311,7 @@ extension Mach.Port where RightType == Mach.ReceiveRight { } } -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) extension Mach.Port where RightType == Mach.SendRight { /// Transfer ownership of the underlying port right to the caller. /// @@ -337,7 +337,7 @@ extension Mach.Port where RightType == Mach.SendRight { /// receiving side has been deallocated, then copySendRight() will throw /// a Mach.PortRightError.deadName error. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public func copySendRight() throws -> Mach.Port { let how = MACH_MSG_TYPE_COPY_SEND @@ -354,7 +354,7 @@ extension Mach.Port where RightType == Mach.SendRight { } } -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) extension Mach.Port where RightType == Mach.SendOnceRight { /// Transfer ownership of the underlying port right to the caller. /// @@ -366,7 +366,7 @@ extension Mach.Port where RightType == Mach.SendOnceRight { /// After this function completes, the Mach.Port is destroyed and no longer /// usable. @inlinable - @available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) + @available(System 1.4.0, *) public consuming func relinquish() -> mach_port_name_t { let name = _name discard self diff --git a/Sources/System/PlatformString.swift b/Sources/System/PlatformString.swift index 664a6089..7b8fd90c 100644 --- a/Sources/System/PlatformString.swift +++ b/Sources/System/PlatformString.swift @@ -7,7 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension String { /// Creates a string by interpreting the null-terminated platform string as /// UTF-8 on Unix and UTF-16 on Windows. @@ -165,7 +165,7 @@ extension String { } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension CInterop.PlatformChar { internal var _platformCodeUnit: CInterop.PlatformUnicodeEncoding.CodeUnit { #if os(Windows) @@ -176,7 +176,7 @@ extension CInterop.PlatformChar { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension CInterop.PlatformUnicodeEncoding.CodeUnit { internal var _platformChar: CInterop.PlatformChar { #if os(Windows) diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index 3a8df9ac..e4832ac5 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -8,21 +8,21 @@ */ // Results in errno if i == -1 -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) private func valueOrErrno( _ i: I ) -> Result { i == -1 ? .failure(Errno.current) : .success(i) } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) private func nothingOrErrno( _ i: I ) -> Result<(), Errno> { valueOrErrno(i).map { _ in () } } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) internal func valueOrErrno( retryOnInterrupt: Bool, _ f: () -> I ) -> Result { @@ -36,7 +36,7 @@ internal func valueOrErrno( } while true } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) internal func nothingOrErrno( retryOnInterrupt: Bool, _ f: () -> I ) -> Result<(), Errno> { diff --git a/Tests/SystemTests/ErrnoTest.swift b/Tests/SystemTests/ErrnoTest.swift index 5f4551ba..767190ee 100644 --- a/Tests/SystemTests/ErrnoTest.swift +++ b/Tests/SystemTests/ErrnoTest.swift @@ -19,7 +19,7 @@ import System import WinSDK #endif -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) final class ErrnoTest: XCTestCase { func testConstants() { XCTAssert(EPERM == Errno.notPermitted.rawValue) diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 479e503f..e4609bb3 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -20,7 +20,7 @@ import Android import CSystem #endif -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) final class FileOperationsTest: XCTestCase { #if !os(WASI) // Would need to use _getConst funcs from CSystem func testSyscalls() { diff --git a/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift b/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift index e816bb5a..d72d59a2 100644 --- a/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift @@ -15,7 +15,7 @@ import XCTest @testable import System #endif -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) struct TestPathComponents: TestCase { var path: FilePath var expectedRoot: FilePath.Root? @@ -100,7 +100,7 @@ struct TestPathComponents: TestCase { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) final class FilePathComponentsTest: XCTestCase { func testAdHocRRC() { var path: FilePath = "/usr/local/bin" diff --git a/Tests/SystemTests/FilePathTests/FilePathDecodable.swift b/Tests/SystemTests/FilePathTests/FilePathDecodable.swift index 290bef2f..b88b0299 100644 --- a/Tests/SystemTests/FilePathTests/FilePathDecodable.swift +++ b/Tests/SystemTests/FilePathTests/FilePathDecodable.swift @@ -15,7 +15,7 @@ import XCTest @testable import System #endif -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) final class FilePathDecodableTest: XCTestCase { func testInvalidFilePath() { // _storage is a valid SystemString, but the invariants of FilePath are diff --git a/Tests/SystemTests/FilePathTests/FilePathExtras.swift b/Tests/SystemTests/FilePathTests/FilePathExtras.swift index 82f11373..ff4b1b37 100644 --- a/Tests/SystemTests/FilePathTests/FilePathExtras.swift +++ b/Tests/SystemTests/FilePathTests/FilePathExtras.swift @@ -6,7 +6,7 @@ #endif // Why can't I write this extension on `FilePath.ComponentView.SubSequence`? -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension Slice where Base == FilePath.ComponentView { internal var _storageSlice: SystemString.SubSequence { base._path._storage[self.startIndex._storage ..< self.endIndex._storage] @@ -15,7 +15,7 @@ extension Slice where Base == FilePath.ComponentView { // Proposed API that didn't make the cut, but we stil want to keep our testing for -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension FilePath { /// Returns `self` relative to `base`. /// This does not cosult the file system or resolve symlinks. diff --git a/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift b/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift index 766bdf8d..66e9105e 100644 --- a/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift @@ -68,7 +68,7 @@ extension ParsingTestCase { @testable import System #endif -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) final class FilePathParsingTest: XCTestCase { func testNormalization() { let unixPaths: Array = [ diff --git a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift index d23999a2..4e182556 100644 --- a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift @@ -146,7 +146,7 @@ extension SyntaxTestCase { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension SyntaxTestCase { func testComponents(_ path: FilePath, expected: [String]) { let expectedComponents = expected.map { FilePath.Component($0)! } @@ -342,7 +342,7 @@ private struct WindowsRootTestCase: TestCase { var line: UInt } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) extension WindowsRootTestCase { func runAllTests() { withWindowsPaths(enabled: true) { @@ -357,7 +357,7 @@ extension WindowsRootTestCase { } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) final class FilePathSyntaxTest: XCTestCase { func testPathSyntax() { let unixPaths: Array = [ diff --git a/Tests/SystemTests/FilePathTests/FilePathTest.swift b/Tests/SystemTests/FilePathTests/FilePathTest.swift index b50cc17e..ef71c850 100644 --- a/Tests/SystemTests/FilePathTests/FilePathTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathTest.swift @@ -15,7 +15,7 @@ import SystemPackage import System #endif -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) func filePathFromInvalidCodePointSequence(_ bytes: S) -> FilePath where S.Element == CInterop.PlatformUnicodeEncoding.CodeUnit { var array = Array(bytes) assert(array.last != 0, "already null terminated") @@ -28,7 +28,7 @@ func filePathFromInvalidCodePointSequence(_ bytes: S) -> FilePath w } } -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) final class FilePathTest: XCTestCase { struct TestPath { let filePath: FilePath diff --git a/Tests/SystemTests/FileTypesTest.swift b/Tests/SystemTests/FileTypesTest.swift index a818dfba..0ddcb0de 100644 --- a/Tests/SystemTests/FileTypesTest.swift +++ b/Tests/SystemTests/FileTypesTest.swift @@ -18,7 +18,7 @@ import System import Android #endif -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) final class FileDescriptorTest: XCTestCase { func testStandardDescriptors() { XCTAssertEqual(FileDescriptor.standardInput.rawValue, 0) @@ -76,7 +76,7 @@ final class FileDescriptorTest: XCTestCase { } -@available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) +@available(System 0.0.1, *) final class FilePermissionsTest: XCTestCase { func testPermissions() { diff --git a/Tests/SystemTests/MachPortTests.swift b/Tests/SystemTests/MachPortTests.swift index 073da9a5..fde04d8c 100644 --- a/Tests/SystemTests/MachPortTests.swift +++ b/Tests/SystemTests/MachPortTests.swift @@ -18,7 +18,7 @@ import SystemPackage import System #endif -@available(/*System 1.4.0: macOS 9999, iOS 9999, watchOS 9999, tvOS 9999*/iOS 8, *) +@available(System 1.4.0, *) final class MachPortTests: XCTestCase { func refCountForMachPortName(name:mach_port_name_t, kind:mach_port_right_t) -> mach_port_urefs_t { var refCount:mach_port_urefs_t = .max diff --git a/Tests/SystemTests/MockingTest.swift b/Tests/SystemTests/MockingTest.swift index 1f2c96da..38985c6b 100644 --- a/Tests/SystemTests/MockingTest.swift +++ b/Tests/SystemTests/MockingTest.swift @@ -15,7 +15,7 @@ import XCTest @testable import System #endif -@available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) +@available(System 0.0.2, *) final class MockingTest: XCTestCase { func testMocking() { XCTAssertFalse(mockingEnabled) diff --git a/Utilities/expand-availability.py b/Utilities/expand-availability.py deleted file mode 100755 index 4cb1a1be..00000000 --- a/Utilities/expand-availability.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 - -# This script can be used to automatically add/remove `@available` attributes to -# declarations in Swift sources in this package. -# -# In order for this to work, ABI-impacting declarations need to be annotated -# with special comments in the following format: -# -# @available(/*System 0.0.2*/iOS 8, *) -# public func greeting() -> String { -# "Hello" -# } -# -# (The iOS 8 availability is a dummy no-op declaration -- it only has to be -# there because `@available(*)` isn't valid syntax, and commenting out the -# entire `@available` attribute would interfere with parser tools for doc -# comments. `iOS 8` is the shortest version string that matches the minimum -# possible deployment target for Swift code, so we use that as our dummy -# availability version. `@available(iOS 8, *)` is functionally equivalent to not -# having an `@available` attribute at all.) -# -# The script adds full availability incantations to these comments. It can run -# in one of two modes: -# -# By default, `expand-availability.py` expands availability macros within the -# comments. This is useful during package development to cross-reference -# availability across `SystemPackage` and the ABI-stable `System` module that -# ships in Apple's OS releases: -# -# @available(/*System 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) -# public func greeting() -> String { -# "Hello" -# } -# -# `expand-availability.py --attributes` adds actual availability declarations. -# This is used by maintainers to build ABI stable releases of System on Apple's -# platforms: -# -# @available(/*System 0.0.2: */macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -# public func greeting() -> String { -# "Hello" -# } -# -# The script recognizes all three forms of these annotations and updates them on -# every run, so we can run the script to enable/disable attributes as needed. - -import os -import os.path -import fileinput -import re -import sys -import argparse - -versions = { - "System 0.0.1": "macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0", - "System 0.0.2": "macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0", - "System 1.1.0": "macOS 12.3, iOS 15.4, watchOS 8.5, tvOS 15.4", - "System 1.2.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999", - "System 1.3.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999", - "System 1.4.0": "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999", -} - -parser = argparse.ArgumentParser(description="Expand availability macros.") -parser.add_argument("--attributes", help="Add @available attributes", - action="store_true") -args = parser.parse_args() - -def swift_sources_in(path): - result = [] - for (dir, _, files) in os.walk(path): - for file in files: - extension = os.path.splitext(file)[1] - if extension == ".swift": - result.append(os.path.join(dir, file)) - return result - -# Old-style syntax: -# /*System 0.0.2*/ -# /*System 0.0.2, @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)*/ -# /*System 0.0.2*/@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -old_macro_pattern = re.compile( - r"/\*(System [^ *]+)(, @available\([^)]*\))?\*/(@available\([^)]*\))?") - -# New-style comments: -# @available(/*SwiftSystem 0.0.2*/macOS 10, *) -# @available(/*SwiftSystem 0.0.2: macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0*/iOS 8, *) -# @available(/*SwiftSystem 0.0.2*/macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -# -# These do not interfere with our tools' ability to find doc comments. -macro_pattern = re.compile( - r"@available\(/\*(System [^ *:]+)[^*/)]*\*/([^)]*)\*\)") - -def available_attribute(filename, lineno, symbolic_version): - expansion = versions[symbolic_version] - if expansion is None: - raise ValueError("{0}:{1}: error: Unknown System version '{0}'" - .format(fileinput.filename(), fileinput.lineno(), symbolic_version)) - if args.attributes: - attribute = "@available(/*{0}*/{1}, *)".format(symbolic_version, expansion) - else: - # Sadly `@available(*)` is not valid syntax, so we have to mention at - # least one actual platform here. - attribute = "@available(/*{0}: {1}*/iOS 8, *)".format(symbolic_version, expansion) - return attribute - - -sources = swift_sources_in("Sources") + swift_sources_in("Tests") -for line in fileinput.input(files=sources, inplace=True): - match = re.search(macro_pattern, line) - if match is None: - match = re.search(old_macro_pattern, line) - if match: - symbolic_version = match.group(1) - replacement = available_attribute( - fileinput.filename(), fileinput.lineno(), symbolic_version) - line = line[:match.start()] + replacement + line[match.end():] - print(line, end="") From e5198c2b2d062741dc6653b993ce31566049364d Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Wed, 30 Jul 2025 08:50:14 -0700 Subject: [PATCH 132/172] disable 16.0 and 16.1 --- .github/workflows/pull_request.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d4949eb1..35d50227 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -11,6 +11,13 @@ jobs: with: linux_os_versions: '["jammy", "focal"]' enable_macos_checks: true + # FIXME: https://github.com/swiftlang/github-workflows/pull/140 + # Xcode 16.0 and 16.1 are not actually available + macos_exclude_xcode_versions: | + [ + {"xcode_version": "16.0"}, + {"xcode_version": "16.1"}, + ] swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" build-abi-stable: @@ -22,12 +29,12 @@ jobs: enable_windows_checks: false # Only build macos_build_command: "xcrun swift build --build-tests" - # Only test against latest Xcode + # FIXME: https://github.com/swiftlang/github-workflows/pull/140 + # Xcode 16.0 and 16.1 are not actually available macos_exclude_xcode_versions: | [ {"xcode_version": "16.0"}, {"xcode_version": "16.1"}, - {"xcode_version": "16.2"}, ] # Enable availability to match ABI stable verion of system. swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI -Xbuild-tools-swiftc -DSYSTEM_ABI_STABLE" From f97a80426c77504e51cffc6189d2f23e0965c83e Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Wed, 30 Jul 2025 08:53:33 -0700 Subject: [PATCH 133/172] update readme --- Package.swift | 1 + README.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 74a70160..35059250 100644 --- a/Package.swift +++ b/Package.swift @@ -63,6 +63,7 @@ let availability: [Available] = [ Available("1.4.2", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), Available("1.5.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), Available("1.6.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + Available("1.6.1", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), ] let swiftSettingsAvailability = availability.map(\.swiftSetting) diff --git a/README.md b/README.md index 0089f681..0b75fd04 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ To use the `SystemPackage` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: ```swift -.package(url: "https://github.com/apple/swift-system", from: "1.6.0"), +.package(url: "https://github.com/apple/swift-system", from: "1.6.1"), ``` Finally, include `"SystemPackage"` as a dependency for your executable target: @@ -41,7 +41,7 @@ Finally, include `"SystemPackage"` as a dependency for your executable target: let package = Package( // name, platforms, products, etc. dependencies: [ - .package(url: "https://github.com/apple/swift-system", from: "1.6.0"), + .package(url: "https://github.com/apple/swift-system", from: "1.6.1"), // other dependencies ], targets: [ From c4fef8b59a68d5b773ddc1e8697815fe553280eb Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Wed, 30 Jul 2025 09:11:26 -0700 Subject: [PATCH 134/172] Disable non-escapable API by feature flag --- Sources/System/IORing/IOCompletion.swift | 6 +++--- Sources/System/IORing/IORequest.swift | 6 +++--- Sources/System/IORing/IORing.swift | 6 +++--- Sources/System/IORing/RawIORequest.swift | 6 +++--- Tests/SystemTests/IORequestTests.swift | 6 +++--- Tests/SystemTests/IORingTests.swift | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/System/IORing/IOCompletion.swift b/Sources/System/IORing/IOCompletion.swift index ea99cc70..d9e69050 100644 --- a/Sources/System/IORing/IOCompletion.swift +++ b/Sources/System/IORing/IOCompletion.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.2) +#if compiler(>=6.2) && $Lifetimes #if os(Linux) import CSystem @@ -67,5 +67,5 @@ public extension IORing.Completion { } } } -#endif -#endif +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index cc91e476..7e433f9f 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.2) +#if compiler(>=6.2) && $Lifetimes #if os(Linux) import CSystem @@ -493,5 +493,5 @@ extension IORing.Request { return request } } -#endif -#endif +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/IORing/IORing.swift b/Sources/System/IORing/IORing.swift index 3e54c789..16c85a0f 100644 --- a/Sources/System/IORing/IORing.swift +++ b/Sources/System/IORing/IORing.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.2) +#if compiler(>=6.2) && $Lifetimes #if os(Linux) import CSystem @@ -900,5 +900,5 @@ extension IORing.RegisteredBuffer { return unsafe _overrideLifetime(span, borrowing: self) } } -#endif -#endif +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Sources/System/IORing/RawIORequest.swift b/Sources/System/IORing/RawIORequest.swift index 4958a480..0bc40294 100644 --- a/Sources/System/IORing/RawIORequest.swift +++ b/Sources/System/IORing/RawIORequest.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.2) +#if compiler(>=6.2) && $Lifetimes #if os(Linux) import CSystem @@ -201,5 +201,5 @@ extension RawIORequest { } } } -#endif -#endif +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Tests/SystemTests/IORequestTests.swift b/Tests/SystemTests/IORequestTests.swift index 50728234..9246d394 100644 --- a/Tests/SystemTests/IORequestTests.swift +++ b/Tests/SystemTests/IORequestTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.2) +#if compiler(>=6.2) && $Lifetimes #if os(Linux) import XCTest @@ -30,5 +30,5 @@ final class IORequestTests: XCTestCase { XCTAssertEqual(sourceBytes, .init(repeating: 0, count: 64)) } } -#endif -#endif +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes diff --git a/Tests/SystemTests/IORingTests.swift b/Tests/SystemTests/IORingTests.swift index 8e0e8fa6..c5d54d41 100644 --- a/Tests/SystemTests/IORingTests.swift +++ b/Tests/SystemTests/IORingTests.swift @@ -1,4 +1,4 @@ -#if compiler(>=6.2) +#if compiler(>=6.2) && $Lifetimes #if os(Linux) import XCTest @@ -128,5 +128,5 @@ final class IORingTests: XCTestCase { rawBuffer.deallocate() } } -#endif -#endif +#endif // os(Linux) +#endif // compiler(>=6.2) && $Lifetimes From 2610d66a5274c40b42374f66ad63340f11403320 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Wed, 30 Jul 2025 14:28:58 -0700 Subject: [PATCH 135/172] This package requires Swift 5.9 Accordingly, these conditions would always be true --- Sources/System/MachPort.swift | 2 +- Tests/SystemTests/MachPortTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/System/MachPort.swift b/Sources/System/MachPort.swift index 8cc05096..b566895b 100644 --- a/Sources/System/MachPort.swift +++ b/Sources/System/MachPort.swift @@ -7,7 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ -#if swift(>=5.9) && SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN import Darwin.Mach diff --git a/Tests/SystemTests/MachPortTests.swift b/Tests/SystemTests/MachPortTests.swift index 073da9a5..6baf50c6 100644 --- a/Tests/SystemTests/MachPortTests.swift +++ b/Tests/SystemTests/MachPortTests.swift @@ -7,7 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ -#if swift(>=5.9) && SYSTEM_PACKAGE_DARWIN +#if SYSTEM_PACKAGE_DARWIN import XCTest import Darwin.Mach From 13ad3fe4aca421b8a18ff872724f916e4ff4d481 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Wed, 30 Jul 2025 14:29:12 -0700 Subject: [PATCH 136/172] Update copyright dates --- Sources/System/MachPort.swift | 2 +- Tests/SystemTests/MachPortTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/System/MachPort.swift b/Sources/System/MachPort.swift index b566895b..452fd5ae 100644 --- a/Sources/System/MachPort.swift +++ b/Sources/System/MachPort.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2022 Apple Inc. and the Swift System project authors + Copyright (c) 2022 - 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 diff --git a/Tests/SystemTests/MachPortTests.swift b/Tests/SystemTests/MachPortTests.swift index 6baf50c6..a1c85a4a 100644 --- a/Tests/SystemTests/MachPortTests.swift +++ b/Tests/SystemTests/MachPortTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift System open source project - Copyright (c) 2022 Apple Inc. and the Swift System project authors + Copyright (c) 2022 - 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 From 9ec41a667cf84d553edb40b44633b94e507ab532 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 30 Jul 2025 18:47:26 -0600 Subject: [PATCH 137/172] Support testing in release mode --- Tests/SystemTests/FileOperationsTest.swift | 6 ++++-- Tests/SystemTests/FilePathTests/FilePathParsingTest.swift | 2 ++ Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift | 2 ++ Tests/SystemTests/MockingTest.swift | 2 ++ Tests/SystemTests/TestingInfrastructure.swift | 6 ++++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 479e503f..dcffc132 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -22,7 +22,7 @@ import CSystem @available(/*System 0.0.1: macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0*/iOS 8, *) final class FileOperationsTest: XCTestCase { - #if !os(WASI) // Would need to use _getConst funcs from CSystem + #if ENABLE_MOCKING && !os(WASI) // Would need to use _getConst funcs from CSystem func testSyscalls() { let fd = FileDescriptor(rawValue: 1) @@ -91,7 +91,7 @@ final class FileOperationsTest: XCTestCase { for test in syscallTestCases { test.runAllTests() } } - #endif // !os(WASI) + #endif // ENABLE_MOCKING && !os(WASI) func testWriteFromEmptyBuffer() throws { #if os(Windows) @@ -215,6 +215,7 @@ final class FileOperationsTest: XCTestCase { } } + #if ENABLE_MOCKING func testGithubIssues() { // https://github.com/apple/swift-system/issues/26 #if os(WASI) @@ -233,6 +234,7 @@ final class FileOperationsTest: XCTestCase { } issue26.runAllTests() } + #endif // ENABLE_MOCKING func testResizeFile() throws { try withTemporaryFilePath(basename: "testResizeFile") { path in diff --git a/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift b/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift index 766bdf8d..b592c08b 100644 --- a/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathParsingTest.swift @@ -7,6 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ +#if ENABLE_MOCKING import XCTest #if SYSTEM_PACKAGE @@ -105,3 +106,4 @@ final class FilePathParsingTest: XCTestCase { } } } +#endif // ENABLE_MOCKING diff --git a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift index d23999a2..558a003d 100644 --- a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift @@ -7,6 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ +#if ENABLE_MOCKING import XCTest #if SYSTEM_PACKAGE @@ -1238,3 +1239,4 @@ final class FilePathSyntaxTest: XCTestCase { } } +#endif // ENABLE_MOCKING diff --git a/Tests/SystemTests/MockingTest.swift b/Tests/SystemTests/MockingTest.swift index 1f2c96da..a90125a9 100644 --- a/Tests/SystemTests/MockingTest.swift +++ b/Tests/SystemTests/MockingTest.swift @@ -7,6 +7,7 @@ See https://swift.org/LICENSE.txt for license information */ +#if ENABLE_MOCKING import XCTest #if SYSTEM_PACKAGE @@ -48,3 +49,4 @@ final class MockingTest: XCTestCase { XCTAssertFalse(mockingEnabled) } } +#endif // ENABLE_MOCKING diff --git a/Tests/SystemTests/TestingInfrastructure.swift b/Tests/SystemTests/TestingInfrastructure.swift index b8905fc4..c169a364 100644 --- a/Tests/SystemTests/TestingInfrastructure.swift +++ b/Tests/SystemTests/TestingInfrastructure.swift @@ -17,6 +17,7 @@ import XCTest internal struct Wildcard: Hashable {} +#if ENABLE_MOCKING extension Trace.Entry { /// This implements `==` with wildcard matching. /// (`Entry` cannot conform to `Equatable`/`Hashable` this way because @@ -33,6 +34,7 @@ extension Trace.Entry { return true } } +#endif // ENABLE_MOCKING // To aid debugging, force failures to fatal error internal var forceFatalFailures = false @@ -81,6 +83,7 @@ extension TestCase { fail(message) } } + #if ENABLE_MOCKING func expectMatch( _ expected: Trace.Entry?, _ actual: Trace.Entry?, _ message: String? = nil @@ -102,6 +105,7 @@ extension TestCase { fail(message) } } + #endif // ENABLE_MOCKING func expectNil( _ actual: T?, _ message: String? = nil @@ -142,6 +146,7 @@ extension TestCase { } +#if ENABLE_MOCKING internal struct MockTestCase: TestCase { var file: StaticString var line: UInt @@ -241,6 +246,7 @@ internal struct MockTestCase: TestCase { } } } +#endif // ENABLE_MOCKING internal func withWindowsPaths(enabled: Bool, _ body: () -> ()) { _withWindowsPaths(enabled: enabled, body) From 11d9d4a78e87c2de2af9dbb3fbd8fe3f460a1623 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Fri, 1 Aug 2025 14:51:27 -0600 Subject: [PATCH 138/172] [CI] Enable Linux static SDK and Wasm SDK builds --- .github/workflows/pull_request.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 35d50227..b72c36a4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,6 +19,8 @@ jobs: {"xcode_version": "16.1"}, ] swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" + enable_linux_static_sdk_build: true + enable_wasm_sdk_build: true build-abi-stable: name: Build ABI Stable From a5f13c77b809bd42844d7cf37fa39aa6e30e66bc Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Sun, 3 Aug 2025 10:59:47 -0600 Subject: [PATCH 139/172] Fix typedef errors on platforms with _ASM_GENERIC_INT headers --- Sources/CSystem/include/io_uring.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/CSystem/include/io_uring.h b/Sources/CSystem/include/io_uring.h index d8287b1a..ce618f3a 100644 --- a/Sources/CSystem/include/io_uring.h +++ b/Sources/CSystem/include/io_uring.h @@ -134,11 +134,13 @@ 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) +#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; typedef uint32_t __u32; typedef uint64_t __u64; typedef int32_t __s32; +#endif #ifndef __kernel_rwf_t typedef int __kernel_rwf_t; From 0ea6743c17f1e6d0f1cbb1c62753262ba64a3217 Mon Sep 17 00:00:00 2001 From: "LamTrinh.Dev" Date: Mon, 11 Aug 2025 00:56:26 +0700 Subject: [PATCH 140/172] Fix spelling errors in some comments. --- Sources/System/IORing/IORequest.swift | 2 +- Sources/System/UtilConsumers.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/System/IORing/IORequest.swift b/Sources/System/IORing/IORequest.swift index 7e433f9f..17be7927 100644 --- a/Sources/System/IORing/IORequest.swift +++ b/Sources/System/IORing/IORequest.swift @@ -320,7 +320,7 @@ extension IORing.Request { * ASYNC_CANCEL flags. * * IORING_ASYNC_CANCEL_ALL Cancel all requests that match the given key - * IORING_ASYNC_CANCEL_FD Key off 'fd' for cancelation rather than the + * 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 diff --git a/Sources/System/UtilConsumers.swift b/Sources/System/UtilConsumers.swift index 183f5f7d..6977543f 100644 --- a/Sources/System/UtilConsumers.swift +++ b/Sources/System/UtilConsumers.swift @@ -52,13 +52,13 @@ extension Slice where Element: Equatable { return self[...idx] } - // If `e` is present, eat up to first occurence of `e` + // If `e` is present, eat up to first occurrence of `e` internal mutating func _eatUntil(_ e: Element) -> Slice? { guard let idx = self.firstIndex(of: e) else { return nil } return _eatUntil(idx) } - // If `e` is present, eat up to and through first occurence of `e` + // If `e` is present, eat up to and through first occurrence of `e` internal mutating func _eatThrough(_ e: Element) -> Slice? { guard let idx = self.firstIndex(of: e) else { return nil } return _eatThrough(idx) From 0610d8f893d5eec935883802bb6f9eb31c2b845b Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Aug 2025 11:44:05 -0600 Subject: [PATCH 141/172] Stat proposal --- NNNN-system-stat.md | 890 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 NNNN-system-stat.md diff --git a/NNNN-system-stat.md b/NNNN-system-stat.md new file mode 100644 index 00000000..a44aa422 --- /dev/null +++ b/NNNN-system-stat.md @@ -0,0 +1,890 @@ +# Stat for Swift System + +* Proposal: [SE-NNNN](NNNN-system-stat.md) +* Authors: [Jonathan Flat](https://github.com/jrflat), [Michael Ilseman](https://github.com/milseman), [Rauhul Varma](https://github.com/rauhul) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [apple/swift-system#256](https://github.com/apple/swift-system/pull/256) +* Review: ([pitch](https://forums.swift.org/)) + +## Introduction + +This proposal introduces a Swift-native `Stat` type to the System library, providing comprehensive access to file metadata on Unix-like platforms through type-safe, platform-aware APIs that wrap the C `stat` types and system calls. + +## Motivation + +Currently, Swift developers who want to work with the file system's lowest level API can only do so through bridged C interfaces. These interfaces lack type safety and require writing non-idiomatic Swift, leading to errors and confusion. + +The goal of the `Stat` type is to provide a faithful and performant Swift wrapper around the underlying C system calls while adding type safety, platform abstraction, and improved discoverability/usability with clear naming. For more on the motivation behind System, see [https://www.swift.org/blog/swift-system](https://www.swift.org/blog/swift-system) + +## Proposed solution + +This proposal adds a `struct Stat` that is available on Unix-like platforms. See discussion on Windows-specific API in **Future Directions**. + +### `Stat` - File Metadata +A Swift wrapper around the C `stat` struct that provides type-safe access to file metadata: + +```swift +// Get file status from path String +let stat = try Stat("/path/to/file") + +// From FileDescriptor +let stat = try fd.stat() + +// From FilePath +let stat = try filePath.stat() + +// `followTargetSymlink: true` (default) behaves like `stat()` +// `followTargetSymlink: false` behaves like `lstat()` +let stat = try symlinkPath.stat(followTargetSymlink: false) + +// Supply flags and optional file descriptor to use the `fstatat()` variant +let stat = try Stat("path/to/file", relativeTo: fd, flags: .symlinkNoFollow) + +print("Size: \(stat.size) bytes") +print("Type: \(stat.type)") // .regular, .directory, .symbolicLink, etc. +print("Permissions: \(stat.permissions)") +print("Modified: \(stat.modificationTime)") + +// Platform-specific information when available +#if canImport(Darwin) || os(FreeBSD) +print("Creation time: \(stat.creationTime)") +#endif +``` + +### Error Handling + +All initializers throw the existing `Errno` type: + +```swift +do { + let stat = try Stat("/nonexistent/file") +} catch Errno.noSuchFileOrDirectory { + print("File not found") +} catch { + print("Other error: \(error)") +} +``` + +These initializers use a typed `throws(Errno)` and require Swift 6.0 or later. + +## Detailed design + +See the **Appendix** section at the end of this proposal for a table view of Swift API to C mappings. + +All API are marked `@_alwaysEmitIntoClient` for performance and back-dating of availability. + +### FileType + +This proposal introduces `FileType` and `FileMode` types to represent `mode_t` values from the C `stat` struct. The type and permissions of a `FileMode` can be modified for convenience, and `FileMode` handles the respective bit masking. + +```swift +/// A file type matching those contained in a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +public struct FileType: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw file-type bits from the C mode. + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed file type from the raw C value. + /// + /// - Note: `rawValue` should only contain the mode's file-type bits. Otherwise, + /// use `FileMode(rawValue:)` to get a strongly-typed `FileMode`, then + /// call `.type` to get the properly masked `FileType`. + public init(rawValue: CInterop.Mode) + + /// Directory + /// + /// The corresponding C constant is `S_IFDIR`. + public static var directory: FileType { get } + + /// Character special device + /// + /// The corresponding C constant is `S_IFCHR`. + public static var characterSpecial: FileType { get } + + /// Block special device + /// + /// The corresponding C constant is `S_IFBLK`. + public static var blockSpecial: FileType { get } + + /// Regular file + /// + /// The corresponding C constant is `S_IFREG`. + public static var regular: FileType { get } + + /// FIFO (or pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + public static var pipe: FileType { get } + + /// Symbolic link + /// + /// The corresponding C constant is `S_IFLNK`. + public static var symbolicLink: FileType { get } + + /// Socket + /// + /// The corresponding C constant is `S_IFSOCK`. + public static var socket: FileType { get } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Whiteout file + /// + /// The corresponding C constant is `S_IFWHT`. + public static var whiteout: FileType { get } + #endif +} +``` + +### FileMode +```swift +/// A strongly-typed file mode representing a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C mode. + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed `FileMode` from the raw C value. + public init(rawValue: CInterop.Mode) + + /// Creates a `FileMode` from the given file type and permissions. + /// + /// - Note: This initializer masks the inputs with their respective bit masks. + public init(type: FileType, permissions: FilePermissions) + + /// The file's type, from the mode's file-type bits. + /// + /// Setting this property will mask the `newValue` with the file-type bit mask `S_IFMT`. + public var type: FileType { get set } + + /// The file's permissions, from the mode's permission bits. + /// + /// Setting this property will mask the `newValue` with the permissions bit mask `0o7777`. + public var permissions: FilePermissions { get set } +} +``` + +### Supporting ID Types + +This proposal also uses new `DeviceID`, `UserID`, `GroupID`, and `Inode` types to represent the respective C data types found in `stat`. These are strongly-typed structs instead of `CInterop` typealiases to prevent ambiguity in future System implementations and to allow for added functionality. + +For example, with an implementation of `chown`, a developer might accidentally misplace user and group parameters with no warning if both were a typealias of the underlying `unsigned int`. Furthermore, a strongly-typed `DeviceID` would allow us to add functionality such as a `makedev` function, or `major` and `minor` getters. + +For now, we define the following for use in `Stat`. + +```swift +@frozen +public struct UserID: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.UserID + public init(rawValue: CInterop.UserID) +} + +@frozen +public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.GroupID + public init(rawValue: CInterop.GroupID) +} + +@frozen +public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.DeviceID + public init(rawValue: CInterop.DeviceID) +} + +@frozen +public struct Inode: RawRepresentable, Sendable, Hashable, Codable { + public var rawValue: CInterop.Inode + public init(rawValue: CInterop.Inode) +} +``` + +Each type stores a `CInterop` typealias to ensure an appropriate `rawValue` for the current platform. Added functionality is outside the scope of this proposal and will be included in a future proposal. + +### FileFlags + +A new `FileFlags` type represents file-specific flags found in a `stat` struct on Darwin, FreeBSD, and OpenBSD. This type would also be useful for an implementation of `chflags()`. + +```swift +/// File-specific flags found in the `st_flags` property of a `stat` struct +/// or used as input to `chflags()`. +/// +/// - Note: Only available on Darwin, FreeBSD, and OpenBSD. +@frozen +public struct FileFlags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + public let rawValue: CInterop.FileFlags + + /// Creates a strongly-typed `FileFlags` from the raw C value. + public init(rawValue: CInterop.FileFlags) + + // MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + + /// Do not dump the file during backups. + /// + /// The corresponding C constant is `UF_NODUMP`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var noDump: FileFlags { get } + + /// File may not be changed. + /// + /// The corresponding C constant is `UF_IMMUTABLE`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var userImmutable: FileFlags { get } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `UF_APPEND`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var userAppend: FileFlags { get } + + /// File has been archived. + /// + /// The corresponding C constant is `SF_ARCHIVED`. + /// - Note: This flag may only be changed by the superuser. + public static var archived: FileFlags { get } + + /// File may not be changed. + /// + /// The corresponding C constant is `SF_IMMUTABLE`. + /// - Note: This flag may only be changed by the superuser. + public static var systemImmutable: FileFlags { get } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `SF_APPEND`. + /// - Note: This flag may only be changed by the superuser. + public static var systemAppend: FileFlags { get } + + // MARK: Flags Available on Darwin and FreeBSD + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Directory is opaque when viewed through a union mount. + /// + /// The corresponding C constant is `UF_OPAQUE`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var opaque: FileFlags { get } + + /// File is compressed at the file system level. + /// + /// The corresponding C constant is `UF_COMPRESSED`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + public static var compressed: FileFlags { get } + + /// File is tracked for the purpose of document IDs. + /// + /// The corresponding C constant is `UF_TRACKED`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var tracked: FileFlags { get } + + /// File should not be displayed in a GUI. + /// + /// The corresponding C constant is `UF_HIDDEN`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var hidden: FileFlags { get } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + public static var restricted: FileFlags { get } + + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `SF_NOUNLINK`. + /// - Note: This flag may only be changed by the superuser. + public static var systemNoUnlink: FileFlags { get } + #endif + + // MARK: Flags Available on Darwin only + + #if SYSTEM_PACKAGE_DARWIN + /// File requires an entitlement for reading and writing. + /// + /// The corresponding C constant is `UF_DATAVAULT`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var dataVault: FileFlags { get } + + /// File is a firmlink. + /// + /// Firmlinks are used by macOS to create transparent links between + /// the read-only system volume and writable data volume. For example, + /// the `/Applications` folder on the system volume is a firmlink to + /// the `/Applications` folder on the data volume, allowing the user + /// to see both system- and user-installed applications in a single folder. + /// + /// The corresponding C constant is `SF_FIRMLINK`. + /// - Note: This flag may only be changed by the superuser. + public static var firmlink: FileFlags { get } + + /// File is a dataless placeholder (content is stored remotely). + /// + /// The system will attempt to materialize the file when accessed according to + /// the dataless file materialization policy of the accessing thread or process. + /// See `getiopolicy_np(3)`. + /// + /// The corresponding C constant is `SF_DATALESS`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + public static var dataless: FileFlags { get } + #endif + + // MARK: Flags Available on FreeBSD Only + + #if os(FreeBSD) + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `UF_NOUNLINK`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var userNoUnlink: FileFlags { get } + + /// File has the Windows offline attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_OFFLINE` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_OFFLINE`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var offline: FileFlags { get } + + /// File is read-only. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_READONLY` attribute. + /// + /// The corresponding C constant is `UF_READONLY`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var readOnly: FileFlags { get } + + /// File contains a Windows reparse point. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_REPARSE_POINT` attribute. + /// + /// The corresponding C constant is `UF_REPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var reparse: FileFlags { get } + + /// File is sparse. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SPARSE_FILE` attribute, + /// or to indicate a sparse file. + /// + /// The corresponding C constant is `UF_SPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var sparse: FileFlags { get } + + /// File has the Windows system attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SYSTEM` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_SYSTEM`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var system: FileFlags { get } + + /// File is a snapshot. + /// + /// The corresponding C constant is `SF_SNAPSHOT`. + /// - Note: This flag may only be changed by the superuser. + public static var snapshot: FileFlags { get } + #endif +} +``` + +### Stat + +`Stat` can be initialized from a `FilePath`, `UnsafePointer`, or `FileDescriptor`. This proposal also includes functions on `FileDescriptor` and `FilePath` for creating a `Stat` object, seen in the section below. + +The initializer accepting a `FileDescriptor` corresponds to `fstat()`. If the file descriptor points to a symlink, this will return information about the symlink itself. + +In the non-`FileDescriptor` case, one form of the initializer takes a `followTargetSymlink: Bool = true` parameter. The default `true` corresponds to `stat()` and will follow a symlink at the end of the path. Setting `followTargetSymlink: false` corresponds to `lstat()` and will return information about the symlink itself. + +The other form of the initializer receives a path, which can be optionally resolved against a given file descriptor, and a set of `Stat.Flags`. These APIs correspond to the `fstatat()` system call and use a default file descriptor of `AT_FDCWD` if one isn't supplied. + +```swift +/// A Swift wrapper of the C `stat` struct. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +public struct Stat: RawRepresentable, Sendable { + + /// The raw C `stat` struct. + public var rawValue: CInterop.Stat + + /// Creates a Swift `Stat` from the raw C struct. + public init(rawValue: CInterop.Stat) + + // MARK: Stat.Flags + + /// Flags representing those passed to `fstatat()`. + @frozen + public struct Flags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + public let rawValue: CInt + + /// Creates a strongly-typed `Stat.Flags` from raw C flags. + public init(rawValue: CInt) + + /// If the path ends with a symbolic link, return information about the link itself. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW`. + public static var symlinkNoFollow: Flags { get } + + #if SYSTEM_PACKAGE_DARWIN + /// If the path ends with a symbolic link, return information about the link itself. + /// If _any_ symbolic link is encountered during path resolution, return an error. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW_ANY`. + /// - Note: Only available on Darwin. + public static var symlinkNoFollowAny: Flags { get } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// If the path does not reside in the hierarchy beneath the starting directory, return an error. + /// + /// The corresponding C constant is `AT_RESOLVE_BENEATH`. + /// - Note: Only available on Darwin and FreeBSD. + public static var resolveBeneath: Flags { get } + #endif + + #if os(FreeBSD) || os(Linux) || os(Android) + /// If the path is an empty string (or `NULL` since Linux 6.11), + /// return information about the given file descriptor. + /// + /// The corresponding C constant is `AT_EMPTY_PATH`. + /// - Note: Only available on FreeBSD, Linux, and Android. + public static var emptyPath: Flags { get } + #endif + } + + // MARK: Initializers + + /// Creates a `Stat` struct from a `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + public init( + _ path: FilePath, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from an`UnsafePointer` path. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + public init( + _ path: UnsafePointer, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from a `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + public init( + _ fd: FileDescriptor, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from a `FilePath` and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + public init( + _ path: FilePath, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from a `FilePath` and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + public init( + _ path: FilePath, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + public init( + _ path: UnsafePointer, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + public init( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) + + // MARK: Properties + + /// ID of device containing file + /// + /// The corresponding C property is `st_dev`. + public var deviceID: DeviceID { get set } + + /// Inode number + /// + /// The corresponding C property is `st_ino`. + public var inode: Inode { get set } + + /// File mode + /// + /// The corresponding C property is `st_mode`. + public var mode: FileMode { get set } + + /// File type for the given mode + public var type: FileType { get set } + + /// File permissions for the given mode + public var permissions: FilePermissions { get set } + + /// Number of hard links + /// + /// The corresponding C property is `st_nlink`. + public var linkCount: Int { get set } + + /// User ID of owner + /// + /// The corresponding C property is `st_uid`. + public var userID: UserID { get set } + + /// Group ID of owner + /// + /// The corresponding C property is `st_gid`. + public var groupID: GroupID { get set } + + /// Device ID (if special file) + /// + /// For character or block special files, the returned `DeviceID` may have + /// meaningful `.major` and `.minor` values. For non-special files, this + /// property is usually meaningless and often set to 0. + /// + /// The corresponding C property is `st_rdev`. + public var specialDeviceID: DeviceID { get set } + + /// Total size, in bytes + /// + /// The corresponding C property is `st_size`. + public var size: Int64 { get set } + + /// Block size for filesystem I/O, in bytes + /// + /// The corresponding C property is `st_blksize`. + public var preferredIOBlockSize: Int { get set } + + /// Number of 512-byte blocks allocated + /// + /// The corresponding C property is `st_blocks`. + public var blocksAllocated: Int64 { get set } + + /// Total size allocated, in bytes + /// + /// - Note: Calculated as `512 * blocksAllocated`. + public var sizeAllocated: Int64 { get } + + /// Time of last access, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var accessTime: UTCClock.Instant { get set } + + /// Time of last modification, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var modificationTime: UTCClock.Instant { get set } + + /// Time of last status (inode) change, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var changeTime: UTCClock.Instant { get set } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var creationTime: UTCClock.Instant { get set } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// File flags + /// + /// The corresponding C property is `st_flags`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + public var flags: FileFlags { get set } + + /// File generation number + /// + /// The file generation number is used to distinguish between different files + /// that have used the same inode over time. + /// + /// The corresponding C property is `st_gen`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + public var generationNumber: Int { get set } + #endif +} + +// MARK: - Equatable and Hashable + +extension Stat: Equatable { + /// Compares the raw bytes of two `Stat` structs for equality. + public static func == (lhs: Self, rhs: Self) -> Bool +} + +extension Stat: Hashable { + /// Hashes the raw bytes of this `Stat` struct. + public func hash(into hasher: inout Hasher) +} +``` + +### FileDescriptor and FilePath Extensions + +```swift +extension FileDescriptor { + + /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + public func stat( + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat +} + +extension FilePath { + + /// Creates a `Stat` struct for the file referenced by this `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + public func stat( + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat + + /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + public func stat( + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat + + /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + public func stat( + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat +} +``` + +### CInterop Extensions + +This proposal extends the existing `CInterop` namespace with platform-appropriate typealiases for the underlying C types. These typealiases are used as the `rawValue` for their strongly-typed representations. + +```swift +extension CInterop { + public typealias Stat + public typealias Inode + public typealias UserID + public typealias GroupID + public typealias DeviceID + public typealias FileFlags +} +``` + +## Source compatibility + +This proposal is additive and source-compatible with existing code. + +## ABI compatibility + +This proposal is additive and ABI-compatible with existing code. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility. + +## Future directions + +To remain faithful to the underlying system calls, we don't anticipate extending `Stat`. However, the types introduced in this proposal could serve as the foundation of broader file system APIs in Swift. + +While this proposal does not include `Stat` on Windows, a separate proposal should provide Swift-native wrappers of idiomatic `GetFileInformation` functions with their associated types. + +A more general `FileInfo` API could then build on these OS-specific types to provide an ergonomic, cross-platform abstraction for file metadata. These future cross-platform APIs might be better implemented outside of System, such as in Foundation, the standard library, or somewhere in between. They could provide additional information or conveniences, such as reading and modifying extended attributes or setting file timestamps. + +In the future, more functionality could be added to types such as `DeviceID`. + +## Alternatives considered + +### `FileInfo` as the lowest-level type +An alternative approach could be to have a more general `FileInfo` type be the lowest level of abstraction provided by the System library. This type would then handle all the `stat` or Windows-specific struct storage and accessors. However, this alternative: + +- Is inconsistent with System's philosophy of providing low-level system abstractions. +- Introduces an even larger number of system-specific APIs on each type. +- Misses out on the familiarity of the `stat` name. Developers know what to look for and what to expect from this type. + +### Single combined type for both file and file system metadata +Combining `Stat` and `StatFS` (separate proposal) into a single type was considered but rejected because file and file system information serve different purposes and are typically needed in different contexts. Storing and/or initializing both `stat` and `statfs` structs unnecessarily reduces performance when one isn't needed. + +### Making `Stat` available on Windows +It's possible to make `Stat` available on Windows and use either the non-native `_stat` functions from CRT or populate the information via a separate `GetFileInformation` call. However, many of the `stat` fields are not- or less-applicable on Windows and are treated as such by `_stat`. For instance, `st_uid` is always zero on Windows, `st_ino` has no meaning in FAT, HPFS, or NTFS file systems, and `st_mode` can only specify a regular file, directory, or character special, with the executable bit depending entirely on the file's path extension. + +Rather than forcing Windows file metadata semantics into a cross-platform `Stat` type, we should instead create Windows-specific types that give developers full access to platform-native file metadata. Combined with a higher-level `FileInfo` type that _is_ cross-platform, this gives the best of both low-level and platform-agnostic APIs. + +### Only have `FilePath` and `FileDescriptor` extensions rather than initializers that accept these types +While having `.stat()` functions on `FilePath` and `FileDescriptor` is preferred for ergonomics and function chaining, this technique might lack the discoverability of having an initializer on `Stat` directly. This proposal therefore includes both the initializers and extensions. + +### Types for time properties + +`UTCClock.Instant` was chosen over alternatives such as `Duration` or a new `Timespec` type to provide a comparable instant in time rather than a duration since the Epoch. This would depend on lowering `UTCClock` to System or the standard library, which could be discussed in a separate pitch or proposal. + +### Type names + +`Stat` was chosen over alternatives like `FileStat` or `FileStatus` for its brevity and likeness to the "stat" system call. Unlike generic names such as `FileInfo` or `FileMetadata`, `Stat` emphasizes the platform-specific nature of this type. + +`Inode` was similarly chosen over alternatives like `FileIndex` or `FileID` to emphasize the platform-specific nature. `IndexNode` is a bit verbose, and despite its etymology, "inode" is now ubiquitous and understood as a single word, making the capitalization `Inode` preferable to `INode`. + + +## Acknowledgments + +These new APIs build on excellent types currently available in the System library. + +## Appendix + +### Swift API to C Mappings + +The following tables show the mapping between Swift APIs and their underlying C system calls across different operating systems: + +#### `Stat` Initializer Mappings + +The `retryOnInterrupt: Bool = true` parameter is omitted for clarity. + +| Swift API | Unix-like Platforms | +|-----------|---------------------| +| `Stat(_ path: FilePath, followTargetSymlink: true)` | `stat()` | +| `Stat(_ path: UnsafePointer, followTargetSymlink: true)` | `stat()` | +|| +| `Stat(_ path: FilePath, followTargetSymlink: false)` | `lstat()` | +| `Stat(_ path: UnsafePointer, followTargetSymlink: false)` | `lstat()` | +|| +| `Stat(_ path: FilePath, relativeTo: FileDescriptor, flags: Stat.Flags)` | `fstatat()` | +| `Stat(_ path: UnsafePointer, relativeTo: FileDescriptor, flags: Stat.Flags)` | `fstatat()` | +|| +| `Stat(_ fd: FileDescriptor)` | `fstat()` | +| `FileDescriptor.stat()` | `fstat()` | +|| +| `FilePath.stat(followTargetSymlink: true)` | `stat()` | +| `FilePath.stat(followTargetSymlink: false)` | `lstat()` | +| `FilePath.stat(relativeTo: FileDescriptor, flags: Stat.Flags)` | `fstatat()` | + +#### `Stat` Property Mappings + +`"` denotes the same property name across all operating systems. + +| Swift Property | Darwin | FreeBSD | OpenBSD | Linux | Android | WASI | +|----------------|--------|---------|---------|-------|---------|------| +| `deviceID` | `st_dev` | " | " | " | " | " | +| `inode` | `st_ino` | " | " | " | " | " | +| `mode` | `st_mode` | " | " | " | " | " | +| `linkCount` | `st_nlink` | " | " | " | " | " | +| `userID` | `st_uid` | " | " | " | " | " | +| `groupID` | `st_gid` | " | " | " | " | " | +| `specialDeviceID` | `st_rdev` | " | " | " | " | " | +| `size` | `st_size` | " | " | " | " | " | +| `preferredIOBlockSize` | `st_blksize` | " | " | " | " | " | +| `blocksAllocated` | `st_blocks` | " | " | " | " | " | +| `accessTime` | `st_atimespec` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | +| `modificationTime` | `st_mtimespec` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | +| `changeTime` | `st_ctimespec` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | +| `creationTime` | `st_birthtimespec` | `st_birthtim` | N/A | N/A | N/A | N/A | +| `flags` | `st_flags` | `st_flags` | `st_flags` | N/A | N/A | N/A | +| `generationNumber` | `st_gen` | `st_gen` | `st_gen` | N/A | N/A | N/A | + +#### `Stat.Flags` Mappings + +| Swift Flag | Darwin | FreeBSD | OpenBSD | Linux | Android | WASI | +|------------|--------|---------|---------|-------|---------|------| +| `symlinkNoFollow` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | `AT_SYMLINK_NOFOLLOW` | +| `symlinkNoFollowAny` | `AT_SYMLINK_NOFOLLOW_ANY` | N/A | N/A | N/A | N/A | N/A | +| `resolveBeneath` | `AT_RESOLVE_BENEATH` | `AT_RESOLVE_BENEATH` | N/A | N/A | N/A | N/A | +| `emptyPath` | N/A | `AT_EMPTY_PATH` | N/A | `AT_EMPTY_PATH` | `AT_EMPTY_PATH` | N/A | + +#### `FileFlags` Mappings + +**Note:** `FileFlags` is only available on Darwin, FreeBSD, and OpenBSD. + +| Swift Flag | Darwin | FreeBSD | OpenBSD | +|------------|--------|---------|---------| +| `noDump` | `UF_NODUMP` | `UF_NODUMP` | `UF_NODUMP` | +| `userImmutable` | `UF_IMMUTABLE` | `UF_IMMUTABLE` | `UF_IMMUTABLE` | +| `userAppend` | `UF_APPEND` | `UF_APPEND` | `UF_APPEND` | +| `archived` | `SF_ARCHIVED` | `SF_ARCHIVED` | `SF_ARCHIVED` | +| `systemImmutable` | `SF_IMMUTABLE` | `SF_IMMUTABLE` | `SF_IMMUTABLE` | +| `systemAppend` | `SF_APPEND` | `SF_APPEND` | `SF_APPEND` | +| `opaque` | `UF_OPAQUE` | `UF_OPAQUE` | N/A | +| `compressed` | `UF_COMPRESSED` | `UF_COMPRESSED` | N/A | +| `tracked` | `UF_TRACKED` | `UF_TRACKED` | N/A | +| `hidden` | `UF_HIDDEN` | `UF_HIDDEN` | N/A | +| `restricted` | `SF_RESTRICTED` | `SF_RESTRICTED` | N/A | +| `systemNoUnlink` | `SF_NOUNLINK` | `SF_NOUNLINK` | N/A | +| `dataVault` | `UF_DATAVAULT` | N/A | N/A | +| `firmlink` | `SF_FIRMLINK` | N/A | N/A | +| `dataless` | `SF_DATALESS` | N/A | N/A | +| `userNoUnlink` | N/A | `UF_NOUNLINK` | N/A | +| `offline` | N/A | `UF_OFFLINE` | N/A | +| `readOnly` | N/A | `UF_READONLY` | N/A | +| `reparse` | N/A | `UF_REPARSE` | N/A | +| `sparse` | N/A | `UF_SPARSE` | N/A | +| `system` | N/A | `UF_SYSTEM` | N/A | +| `snapshot` | N/A | `SF_SNAPSHOT` | N/A | From 0b3b691d0b292bc0037f79db0a2bb858b7d5d942 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Aug 2025 12:36:17 -0600 Subject: [PATCH 142/172] Update pitch link --- NNNN-system-stat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NNNN-system-stat.md b/NNNN-system-stat.md index a44aa422..adee11f1 100644 --- a/NNNN-system-stat.md +++ b/NNNN-system-stat.md @@ -5,7 +5,7 @@ * Review Manager: TBD * Status: **Awaiting review** * Implementation: [apple/swift-system#256](https://github.com/apple/swift-system/pull/256) -* Review: ([pitch](https://forums.swift.org/)) +* Review: ([pitch](https://forums.swift.org/t/pitch-stat-types-for-swift-system/81616)) ## Introduction From b2711a872a8f5ae979cc326f75d7548175376663 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Fri, 12 Sep 2025 18:57:46 -0700 Subject: [PATCH 143/172] Fix: FileDescriptor.duplicate(as:) returns an invalid file descriptor on Windows FileDescriptor.duplicate(as:) is backed by dup2 on Unix, and _dup2 on Windows. On Unix, dup2 returns its second argument. On Windows, _dup2 instead returns 0 on success and -1 on error. This results in the newly returned FileDescriptor object always containing a '0' file descriptor rather than the newly created file descriptor, in violation of the documented behavior. Account for the platform difference in the syscall wrapper to fix this. Closes #192 --- Sources/System/Internals/WindowsSyscallAdapters.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index 6437d16a..da5dfb8d 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -135,7 +135,11 @@ internal func dup(_ fd: Int32) -> Int32 { @inline(__always) internal func dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { - _dup2(fd, fd2) + // _dup2 returns 0 to indicate success. + if _dup2(fd, fd2) == 0 { + return fd2 + } + return -1 } @inline(__always) From b1618475ccf5d901bc91b9253a707630967b5173 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Sun, 3 Aug 2025 17:11:41 -0700 Subject: [PATCH 144/172] [1.6] Fix: FileDescriptor.duplicate(as:) returns an invalid file descriptor on Windows FileDescriptor.duplicate(as:) is backed by dup2 on Unix, and _dup2 on Windows. On Unix, dup2 returns its second argument. On Windows, _dup2 instead returns 0 on success and -1 on error. This results in the newly returned FileDescriptor object always containing a '0' file descriptor rather than the newly created file descriptor, in violation of the documented behavior. Account for the platform difference in the syscall wrapper to fix this. Closes #192 (cherry picked from commit b2711a872a8f5ae979cc326f75d7548175376663) --- Sources/System/Internals/WindowsSyscallAdapters.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/System/Internals/WindowsSyscallAdapters.swift b/Sources/System/Internals/WindowsSyscallAdapters.swift index 6437d16a..da5dfb8d 100644 --- a/Sources/System/Internals/WindowsSyscallAdapters.swift +++ b/Sources/System/Internals/WindowsSyscallAdapters.swift @@ -135,7 +135,11 @@ internal func dup(_ fd: Int32) -> Int32 { @inline(__always) internal func dup2(_ fd: Int32, _ fd2: Int32) -> Int32 { - _dup2(fd, fd2) + // _dup2 returns 0 to indicate success. + if _dup2(fd, fd2) == 0 { + return fd2 + } + return -1 } @inline(__always) From b6bd66889b16f64cd94d3270a6da5135476a599c Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Wed, 17 Sep 2025 12:33:32 -0700 Subject: [PATCH 145/172] [workflows] change ubuntus to test against --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b72c36a4..ec6f66bc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_os_versions: '["jammy", "focal"]' + linux_os_versions: '["noble", "jammy", "focal"]' enable_macos_checks: true # FIXME: https://github.com/swiftlang/github-workflows/pull/140 # Xcode 16.0 and 16.1 are not actually available From 227ebbbe2d082de79ae08217364ff9a3ff9bc89d Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Wed, 17 Sep 2025 15:40:27 -0700 Subject: [PATCH 146/172] [workflows] declare 2D exclusions --- .github/workflows/pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ec6f66bc..4c3a66c6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,6 +10,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_os_versions: '["noble", "jammy", "focal"]' + linux_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}, {"os_version": "noble", "swift_version": "5.9"}, {"os_version": "noble", "swift_version": "5.10"}]' enable_macos_checks: true # FIXME: https://github.com/swiftlang/github-workflows/pull/140 # Xcode 16.0 and 16.1 are not actually available @@ -20,7 +21,9 @@ jobs: ] swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" enable_linux_static_sdk_build: true + linux_static_sdk_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}]' enable_wasm_sdk_build: true + wasm_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}]' build-abi-stable: name: Build ABI Stable From 1c1ad1d3f486667658b28a8f3bd6ae334e6f11ec Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Wed, 17 Sep 2025 12:33:32 -0700 Subject: [PATCH 147/172] [workflows] change ubuntus to test against --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 35d50227..a5b2889c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - linux_os_versions: '["jammy", "focal"]' + linux_os_versions: '["noble", "jammy", "focal"]' enable_macos_checks: true # FIXME: https://github.com/swiftlang/github-workflows/pull/140 # Xcode 16.0 and 16.1 are not actually available From 18a52c47292e4c0dcced61dc47ef13b8fe857b81 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 18 Sep 2025 10:47:41 -0700 Subject: [PATCH 148/172] [workflows] declare 2D exclusions --- .github/workflows/pull_request.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a5b2889c..4c3a66c6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,6 +10,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: linux_os_versions: '["noble", "jammy", "focal"]' + linux_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}, {"os_version": "noble", "swift_version": "5.9"}, {"os_version": "noble", "swift_version": "5.10"}]' enable_macos_checks: true # FIXME: https://github.com/swiftlang/github-workflows/pull/140 # Xcode 16.0 and 16.1 are not actually available @@ -19,6 +20,10 @@ jobs: {"xcode_version": "16.1"}, ] swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" + enable_linux_static_sdk_build: true + linux_static_sdk_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}]' + enable_wasm_sdk_build: true + wasm_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}]' build-abi-stable: name: Build ABI Stable From 80587df07f614905e5406123bb592174c0ee5bd2 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 18 Sep 2025 16:58:21 -0700 Subject: [PATCH 149/172] Require Swift 6.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 35059250..37e259b9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 //===----------------------------------------------------------------------===// // // This source file is part of the Swift System open source project From 478da393fa0f8b2de63612f4bd076ab97e03daa6 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 18 Sep 2025 16:59:28 -0700 Subject: [PATCH 150/172] Use new names for Swift 6 mode --- Sources/System/Internals/Exports.swift | 2 +- Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift | 2 +- Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift | 2 +- Tests/SystemTests/UtilTests.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index d18102a2..c7d9944f 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -140,7 +140,7 @@ extension String { return #else - self.init(validatingUTF8: platformString) + self.init(validatingCString: platformString) #endif } diff --git a/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift b/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift index d72d59a2..675b5330 100644 --- a/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathComponentsTest.swift @@ -107,7 +107,7 @@ final class FilePathComponentsTest: XCTestCase { func expect( _ s: String, - _ file: StaticString = #file, + _ file: StaticString = #filePath, _ line: UInt = #line ) { if path == FilePath(s) { return } diff --git a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift index 6e031895..0fdfbc27 100644 --- a/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift +++ b/Tests/SystemTests/FilePathTests/FilePathSyntaxTest.swift @@ -850,7 +850,7 @@ final class FilePathSyntaxTest: XCTestCase { func expect( _ s: String, - _ file: StaticString = #file, + _ file: StaticString = #filePath, _ line: UInt = #line ) { if path == FilePath(s) { return } diff --git a/Tests/SystemTests/UtilTests.swift b/Tests/SystemTests/UtilTests.swift index bfd21341..e9523207 100644 --- a/Tests/SystemTests/UtilTests.swift +++ b/Tests/SystemTests/UtilTests.swift @@ -34,7 +34,7 @@ class UtilTests: XCTestCase { func testCStringArray() { func check( _ array: [String], - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { array._withCStringArray { carray in From 2ae0c650beceb41f8367ab6eddd9d9b1deb7dfe5 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 18 Sep 2025 17:01:26 -0700 Subject: [PATCH 151/172] Make shared state constant as intended. --- Tests/SystemTests/TestingInfrastructure.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SystemTests/TestingInfrastructure.swift b/Tests/SystemTests/TestingInfrastructure.swift index c169a364..12610ff9 100644 --- a/Tests/SystemTests/TestingInfrastructure.swift +++ b/Tests/SystemTests/TestingInfrastructure.swift @@ -37,7 +37,7 @@ extension Trace.Entry { #endif // ENABLE_MOCKING // To aid debugging, force failures to fatal error -internal var forceFatalFailures = false +internal let forceFatalFailures = false internal protocol TestCase { // TODO: want a source location stack, more fidelity, kinds of stack entries, etc From d8dd95176baf68c43edb3d2c8cbaa0c5d57db0d0 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 18 Sep 2025 17:26:43 -0700 Subject: [PATCH 152/172] adjust pull-request workflow --- .github/workflows/pull_request.yml | 44 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4c3a66c6..cd2bef9e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,21 +9,39 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: + swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" + enable_linux_checks: true linux_os_versions: '["noble", "jammy", "focal"]' - linux_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}, {"os_version": "noble", "swift_version": "5.9"}, {"os_version": "noble", "swift_version": "5.10"}]' + linux_exclude_swift_versions: | + [ + {"swift_version": "5.9"}, + {"swift_version": "5.10"}, + {"os_version": "focal", "swift_version": "nightly-6.2"}, + {"os_version": "focal", "swift_version": "6.2"}, + {"os_version": "focal", "swift_version": "nightly-main"}, + ] enable_macos_checks: true - # FIXME: https://github.com/swiftlang/github-workflows/pull/140 - # Xcode 16.0 and 16.1 are not actually available - macos_exclude_xcode_versions: | + macos_exclude_xcode_versions: '[]' + enable_windows_checks: true + windows_exclude_swift_versions: | [ - {"xcode_version": "16.0"}, - {"xcode_version": "16.1"}, + {"swift_version": "5.9"}, + {"swift_version": "5.10"} ] - swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI" enable_linux_static_sdk_build: true - linux_static_sdk_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}]' + linux_static_sdk_exclude_swift_versions: | + [ + {"os_version": "focal", "swift_version": "nightly-6.2"}, + {"os_version": "focal", "swift_version": "6.2"}, + {"os_version": "focal", "swift_version": "nightly-main"}, + ] enable_wasm_sdk_build: true - wasm_exclude_swift_versions: '[{"os_version": "focal", "swift_version": "nightly-main"}, {"os_version": "focal", "swift_version": "nightly-6.2"}, {"os_version": "focal", "swift_version": "6.2"}]' + wasm_exclude_swift_versions: | + [ + {"os_version": "focal", "swift_version": "nightly-6.2"}, + {"os_version": "focal", "swift_version": "6.2"}, + {"os_version": "focal", "swift_version": "nightly-main"}, + ] build-abi-stable: name: Build ABI Stable @@ -34,13 +52,7 @@ jobs: enable_windows_checks: false # Only build macos_build_command: "xcrun swift build --build-tests" - # FIXME: https://github.com/swiftlang/github-workflows/pull/140 - # Xcode 16.0 and 16.1 are not actually available - macos_exclude_xcode_versions: | - [ - {"xcode_version": "16.0"}, - {"xcode_version": "16.1"}, - ] + macos_exclude_xcode_versions: '[]' # Enable availability to match ABI stable verion of system. swift_flags: "-Xbuild-tools-swiftc -DSYSTEM_CI -Xbuild-tools-swiftc -DSYSTEM_ABI_STABLE" From bc4e6e3bea6dc420e9ee071b5309a0765d5fe117 Mon Sep 17 00:00:00 2001 From: Guillaume Lessard Date: Thu, 18 Sep 2025 17:37:49 -0700 Subject: [PATCH 153/172] Use Swift 5 language mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MachPort` is fine with Swift 6.1 and up, but does not compile with Swift 6.0’s Swift 6 language mode. `import CSystem` is a problem in the Linux version (inconsistently imported as implementation-only.) The Wasm implementation seems not concurrency-safe. --- Package.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 37e259b9..11a43f61 100644 --- a/Package.swift +++ b/Package.swift @@ -139,5 +139,6 @@ let package = Package( exclude: testsToExclude, cSettings: cSettings, swiftSettings: swiftSettings), - ]) - + ], + swiftLanguageVersions: [.v5] +) From 70f197f03691659bf3e4086056a3ac034a43401f Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 29 Sep 2025 11:19:36 -0600 Subject: [PATCH 154/172] Address feedback (v3) --- NNNN-system-stat.md | 202 ++++++++++++++++++++++++++++++++------------ 1 file changed, 147 insertions(+), 55 deletions(-) diff --git a/NNNN-system-stat.md b/NNNN-system-stat.md index adee11f1..f7d8f2cc 100644 --- a/NNNN-system-stat.md +++ b/NNNN-system-stat.md @@ -1,12 +1,18 @@ # Stat for Swift System -* Proposal: [SE-NNNN](NNNN-system-stat.md) +* Proposal: [SYS-0006](NNNN-system-stat.md) * Authors: [Jonathan Flat](https://github.com/jrflat), [Michael Ilseman](https://github.com/milseman), [Rauhul Varma](https://github.com/rauhul) * Review Manager: TBD * Status: **Awaiting review** * Implementation: [apple/swift-system#256](https://github.com/apple/swift-system/pull/256) * Review: ([pitch](https://forums.swift.org/t/pitch-stat-types-for-swift-system/81616)) +#### Revision history + +* **v1** Initial version +* **v2** Moved `UTCClock.Instant` properties to **Future Directions** and exposed C `timespec` properties. Expanded **Alternatives Considered** for `Stat` name and time properties. +* **v3** Add `init(_:)` to wrapper types, clarify `FileType(rawValue:)` behavior with `S_IFMT`, rename `.pipe` to `.fifo`, mention `ALLPERMS` instead of `0o7777`, explain "is"-less flag names in **Alternatives Considered**, fix conditionals for FreeBSD flags, clarify that `.type` and `.permissions` depend on `.mode`, clarify that size property behaviors are file system-dependent. + ## Introduction This proposal introduces a Swift-native `Stat` type to the System library, providing comprehensive access to file metadata on Unix-like platforms through type-safe, platform-aware APIs that wrap the C `stat` types and system calls. @@ -42,6 +48,7 @@ let stat = try symlinkPath.stat(followTargetSymlink: false) let stat = try Stat("path/to/file", relativeTo: fd, flags: .symlinkNoFollow) print("Size: \(stat.size) bytes") +print("Size allocated: \(stat.sizeAllocated) bytes") print("Type: \(stat.type)") // .regular, .directory, .symbolicLink, etc. print("Permissions: \(stat.permissions)") print("Modified: \(stat.modificationTime)") @@ -88,11 +95,17 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { /// The raw file-type bits from the C mode. public var rawValue: CInterop.Mode - /// Creates a strongly-typed file type from the raw C value. - /// - /// - Note: `rawValue` should only contain the mode's file-type bits. Otherwise, - /// use `FileMode(rawValue:)` to get a strongly-typed `FileMode`, then - /// call `.type` to get the properly masked `FileType`. + /// Creates a strongly-typed file type from the raw C `mode_t`. + /// + /// - Note: This initializer stores the `rawValue` directly and **does not** + /// mask the value with `S_IFMT`. If the supplied `rawValue` contains bits + /// outside of the `S_IFMT` mask, the resulting `FileType` will not compare + /// equal to constants like `.directory` and `.symbolicLink`, which may + /// be unexpected. + /// + /// If you're unsure whether the `mode_t` contains bits outside of `S_IFMT`, + /// you can use `FileMode(rawValue:)` instead to get a strongly-typed + /// `FileMode`, then call `.type` to get the properly masked `FileType`. public init(rawValue: CInterop.Mode) /// Directory @@ -115,10 +128,10 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { /// The corresponding C constant is `S_IFREG`. public static var regular: FileType { get } - /// FIFO (or pipe) + /// FIFO (or named pipe) /// /// The corresponding C constant is `S_IFIFO`. - public static var pipe: FileType { get } + public static var fifo: FileType { get } /// Symbolic link /// @@ -152,6 +165,9 @@ public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// Creates a strongly-typed `FileMode` from the raw C value. public init(rawValue: CInterop.Mode) + + /// Creates a strongly-typed `FileMode` from the raw C value. + public init(_ rawValue: CInterop.Mode) /// Creates a `FileMode` from the given file type and permissions. /// @@ -165,7 +181,7 @@ public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// The file's permissions, from the mode's permission bits. /// - /// Setting this property will mask the `newValue` with the permissions bit mask `0o7777`. + /// Setting this property will mask the `newValue` with the permissions bit mask `ALLPERMS`. public var permissions: FilePermissions { get set } } ``` @@ -183,24 +199,28 @@ For now, we define the following for use in `Stat`. public struct UserID: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.UserID public init(rawValue: CInterop.UserID) + public init(_ rawValue: CInterop.UserID) } @frozen public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.GroupID public init(rawValue: CInterop.GroupID) + public init(_ rawValue: CInterop.GroupID) } @frozen public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.DeviceID public init(rawValue: CInterop.DeviceID) + public init(_ rawValue: CInterop.DeviceID) } @frozen public struct Inode: RawRepresentable, Sendable, Hashable, Codable { public var rawValue: CInterop.Inode public init(rawValue: CInterop.Inode) + public init(_ rawValue: CInterop.Inode) } ``` @@ -271,30 +291,12 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { /// - Note: This flag may be changed by the file owner or superuser. public static var opaque: FileFlags { get } - /// File is compressed at the file system level. - /// - /// The corresponding C constant is `UF_COMPRESSED`. - /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. - public static var compressed: FileFlags { get } - - /// File is tracked for the purpose of document IDs. - /// - /// The corresponding C constant is `UF_TRACKED`. - /// - Note: This flag may be changed by the file owner or superuser. - public static var tracked: FileFlags { get } - /// File should not be displayed in a GUI. /// /// The corresponding C constant is `UF_HIDDEN`. /// - Note: This flag may be changed by the file owner or superuser. public static var hidden: FileFlags { get } - /// File requires an entitlement for writing. - /// - /// The corresponding C constant is `SF_RESTRICTED`. - /// - Note: This flag may only be changed by the superuser. - public static var restricted: FileFlags { get } - /// File may not be removed or renamed. /// /// The corresponding C constant is `SF_NOUNLINK`. @@ -305,11 +307,29 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { // MARK: Flags Available on Darwin only #if SYSTEM_PACKAGE_DARWIN + /// File is compressed at the file system level. + /// + /// The corresponding C constant is `UF_COMPRESSED`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + public static var compressed: FileFlags { get } + + /// File is tracked for the purpose of document IDs. + /// + /// The corresponding C constant is `UF_TRACKED`. + /// - Note: This flag may be changed by the file owner or superuser. + public static var tracked: FileFlags { get } + /// File requires an entitlement for reading and writing. /// /// The corresponding C constant is `UF_DATAVAULT`. /// - Note: This flag may be changed by the file owner or superuser. public static var dataVault: FileFlags { get } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + public static var restricted: FileFlags { get } /// File is a firmlink. /// @@ -449,6 +469,7 @@ public struct Stat: RawRepresentable, Sendable { /// /// The corresponding C constant is `AT_RESOLVE_BENEATH`. /// - Note: Only available on Darwin and FreeBSD. + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) public static var resolveBeneath: Flags { get } #endif @@ -478,7 +499,7 @@ public struct Stat: RawRepresentable, Sendable { retryOnInterrupt: Bool = true ) throws(Errno) - /// Creates a `Stat` struct from an`UnsafePointer` path. + /// Creates a `Stat` struct from an `UnsafePointer` path. /// /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. @@ -568,9 +589,15 @@ public struct Stat: RawRepresentable, Sendable { public var mode: FileMode { get set } /// File type for the given mode + /// + /// - Note: This property is equivalent to `mode.type`. Modifying this + /// property will update the underlying `st_mode` accordingly. public var type: FileType { get set } /// File permissions for the given mode + /// + /// - Note: This property is equivalent to `mode.permissions`. Modifying + /// this property will update the underlying `st_mode` accordingly. public var permissions: FilePermissions { get set } /// Number of hard links @@ -599,6 +626,12 @@ public struct Stat: RawRepresentable, Sendable { /// Total size, in bytes /// + /// The semantics of this property are tied to the underlying C `st_size` field, + /// which can have file system-dependent behavior. For example, this property + /// can return different values for a file's data fork and resource fork, and some + /// file systems report logical size rather than actual disk usage for compressed + /// or cloned files. + /// /// The corresponding C property is `st_size`. public var size: Int64 { get set } @@ -609,39 +642,46 @@ public struct Stat: RawRepresentable, Sendable { /// Number of 512-byte blocks allocated /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file system-dependent behavior. + /// /// The corresponding C property is `st_blocks`. public var blocksAllocated: Int64 { get set } /// Total size allocated, in bytes /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file system-dependent behavior. + /// /// - Note: Calculated as `512 * blocksAllocated`. public var sizeAllocated: Int64 { get } - - /// Time of last access, given as a `UTCClock.Instant` + + // NOTE: "st_" property names are used for the `timespec` properties so + // we can reserve `accessTime`, `modificationTime`, etc. for potential + // `UTCClock.Instant` properties in the future. + // See Future Directions for more info. + + /// Time of last access, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var accessTime: UTCClock.Instant { get set } - - /// Time of last modification, given as a `UTCClock.Instant` + public var st_atim: timespec { get set } + + /// Time of last modification, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var modificationTime: UTCClock.Instant { get set } - - /// Time of last status (inode) change, given as a `UTCClock.Instant` + public var st_mtim: timespec { get set } + + /// Time of last status (inode) change, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var changeTime: UTCClock.Instant { get set } + public var st_ctim: timespec { get set } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - /// Time of file creation, given as a `UTCClock.Instant` + /// Time of file creation, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). /// - Note: Only available on Darwin and FreeBSD. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var creationTime: UTCClock.Instant { get set } + public var st_birthtim: timespec { get set } #endif #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) @@ -703,7 +743,7 @@ extension FilePath { retryOnInterrupt: Bool = true ) throws(Errno) -> Stat - /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`. + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`. /// /// If `path` is relative, it is resolved against the current working directory. /// @@ -713,7 +753,7 @@ extension FilePath { retryOnInterrupt: Bool = true ) throws(Errno) -> Stat - /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`, + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`, /// including a `FileDescriptor` to resolve a relative path. /// /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. @@ -761,10 +801,43 @@ To remain faithful to the underlying system calls, we don't anticipate extending While this proposal does not include `Stat` on Windows, a separate proposal should provide Swift-native wrappers of idiomatic `GetFileInformation` functions with their associated types. -A more general `FileInfo` API could then build on these OS-specific types to provide an ergonomic, cross-platform abstraction for file metadata. These future cross-platform APIs might be better implemented outside of System, such as in Foundation, the standard library, or somewhere in between. They could provide additional information or conveniences, such as reading and modifying extended attributes or setting file timestamps. +A more general `FileInfo` API could then build on these OS-specific types to provide an ergonomic, cross-platform abstraction for file metadata. These future cross-platform APIs might be better implemented outside of System, such as in Foundation, the standard library, or somewhere in between. They could provide additional information or convenience features, such as reading and modifying extended attributes or setting file timestamps. In the future, more functionality could be added to types such as `DeviceID`. +### Using `UTCClock.Instant` for time properties + +When the `UTCClock` proposal and code destination is finalized, we could use the `UTCClock.Instant` type for `Stat` time properties: + +```swift +extension Stat { + /// Time of last access, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). + public var accessTime: UTCClock.Instant { get set } + + /// Time of last modification, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). + public var modificationTime: UTCClock.Instant { get set } + + /// Time of last status (inode) change, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). + public var changeTime: UTCClock.Instant { get set } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a `UTCClock.Instant` + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + public var creationTime: UTCClock.Instant { get set } + #endif +} +``` + +We would reserve the more ergonomic `accessTime`, `modificationTime`, etc. names for these future extensions. + ## Alternatives considered ### `FileInfo` as the lowest-level type @@ -787,14 +860,33 @@ While having `.stat()` functions on `FilePath` and `FileDescriptor` is preferred ### Types for time properties -`UTCClock.Instant` was chosen over alternatives such as `Duration` or a new `Timespec` type to provide a comparable instant in time rather than a duration since the Epoch. This would depend on lowering `UTCClock` to System or the standard library, which could be discussed in a separate pitch or proposal. +`UTCClock.Instant` would ideally be chosen over alternatives such as `Duration` or a new `Timespec` type to provide a comparable instant in time rather than a duration since the Epoch. However, this would depend on lowering `UTCClock` to System or the standard library and depends on that separate proposal. + +Exposing a `timespec` property directly also has benefits; it's faithful to the underlying system's type and already has conversion support to/from `Duration` in the standard library. + +Given that `timespec` is not particularly crufty and already has public API supporting its conversions, we decided to expose the raw `timespec` for now under the original C property names (`st_atim`, `st_mtim`, etc.) and reserve more ergonomic names for future extensions. ### Type names `Stat` was chosen over alternatives like `FileStat` or `FileStatus` for its brevity and likeness to the "stat" system call. Unlike generic names such as `FileInfo` or `FileMetadata`, `Stat` emphasizes the platform-specific nature of this type. -`Inode` was similarly chosen over alternatives like `FileIndex` or `FileID` to emphasize the platform-specific nature. `IndexNode` is a bit verbose, and despite its etymology, "inode" is now ubiquitous and understood as a single word, making the capitalization `Inode` preferable to `INode`. +`Stat` and (possible future) `StatFS` names were chosen over `FileStat` and `FileSystemStat`, or a namespaced `File.Stat` and `FileSystem.Stat`, because `Stat` is recognized more as its own concept rather than shorthand for "status." Thus, using `FileSystem.Stat` or `FileSystemStat` for `statfs` in the future might lead to confusion. Also, precedence from other languages' `stat` APIs that use a "file system" namespace might add to this confusion: + +``` +Rust: fs::metadata() -> fs::Metadata +Python: os.stat() -> os.stat_result +Go: os.Stat() -> fs.FileInfo +``` + +`Inode` was chosen over alternatives like `FileIndex` or `FileID` to emphasize the platform-specific nature. `IndexNode` is a bit verbose, and despite its etymology, "inode" is now ubiquitous and understood as a single word, making the capitalization `Inode` preferable to `INode`. + +### `FileFlags` naming conventions + +`FileFlags` property names such as `hidden` and `compressed` could alternatively use an "is" prefix commonly seen in boolean properties to form `.isHidden` and `.isCompressed`. However, we chose to omit the "is" prefix for the following reasons: +- The "is"-less flag names are succinct and closely aligned with the underlying C constants they represent. +- `OptionSet` property names often use an adjective ("hidden") rather than a predicate ("is hidden") when describing a single subject, such as a file. This is likely because "is" does not add to the flow of `flags.contains(.isHidden)` like it does for a direct boolean property, such as `file.isHidden`. +- For both `OptionSet` APIs that describe a single subject and those that describe a collection of elements, there's precedence to omit the "is". Examples of single-subject `OptionSet` APIs include `UIControl.State`, which uses `.highlighted` and `.disabled` rather than `.isHighlighted` and `.isDisabled`, and `FilePermissions`, which uses `.ownerRead` rather than `.isOwnerReadable`. Examples of multi-subject `OptionSet` APIs include `Edge.Set`, which uses `.top` and `.bottom`, and `ShippingOptions` from the `OptionSet` documentation, which uses `.nextDay`, `.priority`, etc. ## Acknowledgments @@ -844,10 +936,10 @@ The `retryOnInterrupt: Bool = true` parameter is omitted for clarity. | `size` | `st_size` | " | " | " | " | " | | `preferredIOBlockSize` | `st_blksize` | " | " | " | " | " | | `blocksAllocated` | `st_blocks` | " | " | " | " | " | -| `accessTime` | `st_atimespec` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | -| `modificationTime` | `st_mtimespec` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | -| `changeTime` | `st_ctimespec` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | -| `creationTime` | `st_birthtimespec` | `st_birthtim` | N/A | N/A | N/A | N/A | +| `st_atim` | `st_atimespec` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | `st_atim` | +| `st_mtim` | `st_mtimespec` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | `st_mtim` | +| `st_ctim` | `st_ctimespec` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | `st_ctim` | +| `st_birthtim` | `st_birthtimespec` | `st_birthtim` | N/A | N/A | N/A | N/A | | `flags` | `st_flags` | `st_flags` | `st_flags` | N/A | N/A | N/A | | `generationNumber` | `st_gen` | `st_gen` | `st_gen` | N/A | N/A | N/A | @@ -873,12 +965,12 @@ The `retryOnInterrupt: Bool = true` parameter is omitted for clarity. | `systemImmutable` | `SF_IMMUTABLE` | `SF_IMMUTABLE` | `SF_IMMUTABLE` | | `systemAppend` | `SF_APPEND` | `SF_APPEND` | `SF_APPEND` | | `opaque` | `UF_OPAQUE` | `UF_OPAQUE` | N/A | -| `compressed` | `UF_COMPRESSED` | `UF_COMPRESSED` | N/A | -| `tracked` | `UF_TRACKED` | `UF_TRACKED` | N/A | | `hidden` | `UF_HIDDEN` | `UF_HIDDEN` | N/A | -| `restricted` | `SF_RESTRICTED` | `SF_RESTRICTED` | N/A | | `systemNoUnlink` | `SF_NOUNLINK` | `SF_NOUNLINK` | N/A | +| `compressed` | `UF_COMPRESSED` | N/A | N/A | +| `tracked` | `UF_TRACKED` | N/A | N/A | | `dataVault` | `UF_DATAVAULT` | N/A | N/A | +| `restricted` | `SF_RESTRICTED` | N/A | N/A | | `firmlink` | `SF_FIRMLINK` | N/A | N/A | | `dataless` | `SF_DATALESS` | N/A | N/A | | `userNoUnlink` | N/A | `UF_NOUNLINK` | N/A | From 2f4c8c216c59c6a1932f7465e81321525cd45e0e Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 29 Sep 2025 11:26:15 -0600 Subject: [PATCH 155/172] Create Proposals directory --- NNNN-system-stat.md => Proposals/0006-system-stat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename NNNN-system-stat.md => Proposals/0006-system-stat.md (99%) diff --git a/NNNN-system-stat.md b/Proposals/0006-system-stat.md similarity index 99% rename from NNNN-system-stat.md rename to Proposals/0006-system-stat.md index f7d8f2cc..39c1bd3a 100644 --- a/NNNN-system-stat.md +++ b/Proposals/0006-system-stat.md @@ -1,6 +1,6 @@ # Stat for Swift System -* Proposal: [SYS-0006](NNNN-system-stat.md) +* Proposal: [SYS-0006](0006-system-stat.md) * Authors: [Jonathan Flat](https://github.com/jrflat), [Michael Ilseman](https://github.com/milseman), [Rauhul Varma](https://github.com/rauhul) * Review Manager: TBD * Status: **Awaiting review** From 57d515e1d0671a252415e3631f6ac24a55093345 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Aug 2025 12:09:37 -0600 Subject: [PATCH 156/172] Stat implementation --- Package.swift | 1 + Sources/System/FileSystem/FileFlags.swift | 254 +++++++ Sources/System/FileSystem/FileMode.swift | 54 ++ Sources/System/FileSystem/FileType.swift | 102 +++ Sources/System/FileSystem/Identifiers.swift | 89 +++ Sources/System/FileSystem/Stat.swift | 700 ++++++++++++++++++++ Sources/System/Internals/CInterop.swift | 10 +- Sources/System/Internals/Constants.swift | 152 ++++- Sources/System/Internals/Exports.swift | 40 +- Tests/SystemTests/FileModeTests.swift | 134 ++++ Tests/SystemTests/StatTests.swift | 408 ++++++++++++ 11 files changed, 1935 insertions(+), 9 deletions(-) create mode 100644 Sources/System/FileSystem/FileFlags.swift create mode 100644 Sources/System/FileSystem/FileMode.swift create mode 100644 Sources/System/FileSystem/FileType.swift create mode 100644 Sources/System/FileSystem/Identifiers.swift create mode 100644 Sources/System/FileSystem/Stat.swift create mode 100644 Tests/SystemTests/FileModeTests.swift create mode 100644 Tests/SystemTests/StatTests.swift diff --git a/Package.swift b/Package.swift index 11a43f61..8aba3315 100644 --- a/Package.swift +++ b/Package.swift @@ -87,6 +87,7 @@ let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ let cSettings: [CSetting] = [ .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), + .define("_GNU_SOURCE", .when(platforms: [.linux])), ] #if SYSTEM_ABI_STABLE diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift new file mode 100644 index 00000000..f89cbc15 --- /dev/null +++ b/Sources/System/FileSystem/FileFlags.swift @@ -0,0 +1,254 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +// |------------------------| +// | Swift API to C Mapping | +// |------------------------------------------------------------------| +// | FileFlags | Darwin | FreeBSD | OpenBSD | +// |------------------|---------------|---------------|---------------| +// | noDump | UF_NODUMP | UF_NODUMP | UF_NODUMP | +// | userImmutable | UF_IMMUTABLE | UF_IMMUTABLE | UF_IMMUTABLE | +// | userAppend | UF_APPEND | UF_APPEND | UF_APPEND | +// | archived | SF_ARCHIVED | SF_ARCHIVED | SF_ARCHIVED | +// | systemImmutable | SF_IMMUTABLE | SF_IMMUTABLE | SF_IMMUTABLE | +// | systemAppend | SF_APPEND | SF_APPEND | SF_APPEND | +// | opaque | UF_OPAQUE | UF_OPAQUE | N/A | +// | compressed | UF_COMPRESSED | UF_COMPRESSED | N/A | +// | tracked | UF_TRACKED | UF_TRACKED | N/A | +// | hidden | UF_HIDDEN | UF_HIDDEN | N/A | +// | restricted | SF_RESTRICTED | SF_RESTRICTED | N/A | +// | systemNoUnlink | SF_NOUNLINK | SF_NOUNLINK | N/A | +// | dataVault | UF_DATAVAULT | N/A | N/A | +// | firmlink | SF_FIRMLINK | N/A | N/A | +// | dataless | SF_DATALESS | N/A | N/A | +// | userNoUnlink | N/A | UF_NOUNLINK | N/A | +// | offline | N/A | UF_OFFLINE | N/A | +// | readOnly | N/A | UF_READONLY | N/A | +// | reparse | N/A | UF_REPARSE | N/A | +// | sparse | N/A | UF_SPARSE | N/A | +// | system | N/A | UF_SYSTEM | N/A | +// | snapshot | N/A | SF_SNAPSHOT | N/A | +// |------------------|---------------|---------------|---------------| + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +// @available(System X.Y.Z, *) +extension CInterop { + public typealias FileFlags = UInt32 +} + +/// File-specific flags found in the `st_flags` property of a `stat` struct +/// or used as input to `chflags()`. +/// +/// - Note: Only available on Darwin, FreeBSD, and OpenBSD. +@frozen +// @available(System X.Y.Z, *) +public struct FileFlags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInterop.FileFlags + + /// Creates a strongly-typed `FileFlags` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileFlags) { self.rawValue = rawValue } + + // MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + + /// Do not dump the file during backups. + /// + /// The corresponding C constant is `UF_NODUMP`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var noDump: FileFlags { FileFlags(rawValue: _UF_NODUMP) } + + /// File may not be changed. + /// + /// The corresponding C constant is `UF_IMMUTABLE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userImmutable: FileFlags { FileFlags(rawValue: _UF_IMMUTABLE) } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `UF_APPEND`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userAppend: FileFlags { FileFlags(rawValue: _UF_APPEND) } + + /// File has been archived. + /// + /// The corresponding C constant is `SF_ARCHIVED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var archived: FileFlags { FileFlags(rawValue: _SF_ARCHIVED) } + + /// File may not be changed. + /// + /// The corresponding C constant is `SF_IMMUTABLE`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemImmutable: FileFlags { FileFlags(rawValue: _SF_IMMUTABLE) } + + /// Writes to the file may only append. + /// + /// The corresponding C constant is `SF_APPEND`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemAppend: FileFlags { FileFlags(rawValue: _SF_APPEND) } + + // MARK: Flags Available on Darwin and FreeBSD + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Directory is opaque when viewed through a union mount. + /// + /// The corresponding C constant is `UF_OPAQUE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var opaque: FileFlags { FileFlags(rawValue: _UF_OPAQUE) } + + /// File is compressed at the file system level. + /// + /// The corresponding C constant is `UF_COMPRESSED`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var compressed: FileFlags { FileFlags(rawValue: _UF_COMPRESSED) } + + /// File is tracked for the purpose of document IDs. + /// + /// The corresponding C constant is `UF_TRACKED`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var tracked: FileFlags { FileFlags(rawValue: _UF_TRACKED) } + + /// File should not be displayed in a GUI. + /// + /// The corresponding C constant is `UF_HIDDEN`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var hidden: FileFlags { FileFlags(rawValue: _UF_HIDDEN) } + + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var restricted: FileFlags { FileFlags(rawValue: _SF_RESTRICTED) } + + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `SF_NOUNLINK`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var systemNoUnlink: FileFlags { FileFlags(rawValue: _SF_NOUNLINK) } + #endif + + // MARK: Flags Available on Darwin only + + #if SYSTEM_PACKAGE_DARWIN + /// File requires an entitlement for reading and writing. + /// + /// The corresponding C constant is `UF_DATAVAULT`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var dataVault: FileFlags { FileFlags(rawValue: _UF_DATAVAULT) } + + /// File is a firmlink. + /// + /// Firmlinks are used by macOS to create transparent links between + /// the read-only system volume and writable data volume. For example, + /// the `/Applications` folder on the system volume is a firmlink to + /// the `/Applications` folder on the data volume, allowing the user + /// to see both system- and user-installed applications in a single folder. + /// + /// The corresponding C constant is `SF_FIRMLINK`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var firmlink: FileFlags { FileFlags(rawValue: _SF_FIRMLINK) } + + /// File is a dataless placeholder (content is stored remotely). + /// + /// The system will attempt to materialize the file when accessed according to + /// the dataless file materialization policy of the accessing thread or process. + /// See `getiopolicy_np(3)`. + /// + /// The corresponding C constant is `SF_DATALESS`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var dataless: FileFlags { FileFlags(rawValue: _SF_DATALESS) } + #endif + + // MARK: Flags Available on FreeBSD Only + + #if os(FreeBSD) + /// File may not be removed or renamed. + /// + /// The corresponding C constant is `UF_NOUNLINK`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var userNoUnlink: FileFlags { FileFlags(rawValue: _UF_NOUNLINK) } + + /// File has the Windows offline attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_OFFLINE` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_OFFLINE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var offline: FileFlags { FileFlags(rawValue: _UF_OFFLINE) } + + /// File is read-only. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_READONLY` attribute. + /// + /// The corresponding C constant is `UF_READONLY`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var readOnly: FileFlags { FileFlags(rawValue: _UF_READONLY) } + + /// File contains a Windows reparse point. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_REPARSE_POINT` attribute. + /// + /// The corresponding C constant is `UF_REPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var reparse: FileFlags { FileFlags(rawValue: _UF_REPARSE) } + + /// File is sparse. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SPARSE_FILE` attribute, + /// or to indicate a sparse file. + /// + /// The corresponding C constant is `UF_SPARSE`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var sparse: FileFlags { FileFlags(rawValue: _UF_SPARSE) } + + /// File has the Windows system attribute. + /// + /// File systems may use this flag for compatibility with the Windows `FILE_ATTRIBUTE_SYSTEM` attribute, + /// but otherwise provide no special handling when it's set. + /// + /// The corresponding C constant is `UF_SYSTEM`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var system: FileFlags { FileFlags(rawValue: _UF_SYSTEM) } + + /// File is a snapshot. + /// + /// The corresponding C constant is `SF_SNAPSHOT`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var snapshot: FileFlags { FileFlags(rawValue: _SF_SNAPSHOT) } + #endif +} +#endif diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift new file mode 100644 index 00000000..14ae30ea --- /dev/null +++ b/Sources/System/FileSystem/FileMode.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 !os(Windows) +/// A strongly-typed file mode representing a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +// @available(System X.Y.Z, *) +public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C mode. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed `FileMode` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } + + /// Creates a `FileMode` from the given file type and permissions. + /// + /// - Note: This initializer masks the inputs with their respective bit masks. + @_alwaysEmitIntoClient + public init(type: FileType, permissions: FilePermissions) { + self.rawValue = (type.rawValue & _MODE_FILETYPE_MASK) | (permissions.rawValue & _MODE_PERMISSIONS_MASK) + } + + /// The file's type, from the mode's file-type bits. + /// + /// Setting this property will mask the `newValue` with the file-type bit mask `S_IFMT`. + @_alwaysEmitIntoClient + public var type: FileType { + get { FileType(rawValue: rawValue & _MODE_FILETYPE_MASK) } + set { rawValue = (rawValue & ~_MODE_FILETYPE_MASK) | (newValue.rawValue & _MODE_FILETYPE_MASK) } + } + + /// The file's permissions, from the mode's permission bits. + /// + /// Setting this property will mask the `newValue` with the permissions bit mask `0o7777`. + @_alwaysEmitIntoClient + public var permissions: FilePermissions { + get { FilePermissions(rawValue: rawValue & _MODE_PERMISSIONS_MASK) } + set { rawValue = (rawValue & ~_MODE_PERMISSIONS_MASK) | (newValue.rawValue & _MODE_PERMISSIONS_MASK) } + } +} +#endif diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift new file mode 100644 index 00000000..91718880 --- /dev/null +++ b/Sources/System/FileSystem/FileType.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 +// +//===----------------------------------------------------------------------===// + +// |------------------------| +// | Swift API to C Mapping | +// |----------------------------------------| +// | FileType | Unix-like Platforms | +// |------------------|---------------------| +// | directory | S_IFDIR | +// | characterSpecial | S_IFCHR | +// | blockSpecial | S_IFBLK | +// | regular | S_IFREG | +// | pipe | S_IFIFO | +// | symbolicLink | S_IFLNK | +// | socket | S_IFSOCK | +// |------------------|---------------------| +// +// |------------------------------------------------------------------| +// | FileType | Darwin | FreeBSD | Other Unix-like Platforms | +// |------------------|---------|---------|---------------------------| +// | whiteout | S_IFWHT | S_IFWHT | N/A | +// |------------------|---------|---------|---------------------------| + +#if !os(Windows) +/// A file type matching those contained in a C `mode_t`. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +// @available(System X.Y.Z, *) +public struct FileType: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw file-type bits from the C mode. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Mode + + /// Creates a strongly-typed file type from the raw C value. + /// + /// - Note: `rawValue` should only contain the mode's file-type bits. Otherwise, + /// use `FileMode(rawValue:)` to get a strongly-typed `FileMode`, then + /// call `.type` to get the properly masked `FileType`. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } + + /// Directory + /// + /// The corresponding C constant is `S_IFDIR`. + @_alwaysEmitIntoClient + public static var directory: FileType { FileType(rawValue: _S_IFDIR) } + + /// Character special device + /// + /// The corresponding C constant is `S_IFCHR`. + @_alwaysEmitIntoClient + public static var characterSpecial: FileType { FileType(rawValue: _S_IFCHR) } + + /// Block special device + /// + /// The corresponding C constant is `S_IFBLK`. + @_alwaysEmitIntoClient + public static var blockSpecial: FileType { FileType(rawValue: _S_IFBLK) } + + /// Regular file + /// + /// The corresponding C constant is `S_IFREG`. + @_alwaysEmitIntoClient + public static var regular: FileType { FileType(rawValue: _S_IFREG) } + + /// FIFO (or pipe) + /// + /// The corresponding C constant is `S_IFIFO`. + @_alwaysEmitIntoClient + public static var pipe: FileType { FileType(rawValue: _S_IFIFO) } + + /// Symbolic link + /// + /// The corresponding C constant is `S_IFLNK`. + @_alwaysEmitIntoClient + public static var symbolicLink: FileType { FileType(rawValue: _S_IFLNK) } + + /// Socket + /// + /// The corresponding C constant is `S_IFSOCK`. + @_alwaysEmitIntoClient + public static var socket: FileType { FileType(rawValue: _S_IFSOCK) } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Whiteout file + /// + /// The corresponding C constant is `S_IFWHT`. + @_alwaysEmitIntoClient + public static var whiteout: FileType { FileType(rawValue: _S_IFWHT) } + #endif +} +#endif diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift new file mode 100644 index 00000000..df9a01ae --- /dev/null +++ b/Sources/System/FileSystem/Identifiers.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 !os(Windows) +/// A Swift wrapper of the C `uid_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct UserID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `uid_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.UserID + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.UserID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `gid_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `gid_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.GroupID + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.GroupID) { self.rawValue = rawValue } +} + +/// A Swift wrapper of the C `dev_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `dev_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.DeviceID + + /// Creates a strongly-typed `DeviceID` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.DeviceID) { self.rawValue = rawValue } + + + /// Creates a `DeviceID` from the given major and minor device numbers. + /// + /// The corresponding C function is `makedev()`. + @_alwaysEmitIntoClient + public static func make(major: CUnsignedInt, minor: CUnsignedInt) -> DeviceID { + DeviceID(rawValue: system_makedev(major, minor)) + } + + /// The major device number + /// + /// The corresponding C function is `major()`. + @_alwaysEmitIntoClient + public var major: CInt { system_major(rawValue) } + + /// The minor device number + /// + /// The corresponding C function is `minor()`. + @_alwaysEmitIntoClient + public var minor: CInt { system_minor(rawValue) } +} + +/// A Swift wrapper of the C `ino_t` type. +@frozen +// @available(System X.Y.Z, *) +public struct Inode: RawRepresentable, Sendable, Hashable, Codable { + + /// The raw C `ino_t`. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Inode + + /// Creates a strongly-typed `Inode` from the raw C value. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Inode) { self.rawValue = rawValue } +} +#endif // !os(Windows) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift new file mode 100644 index 00000000..76f37b0a --- /dev/null +++ b/Sources/System/FileSystem/Stat.swift @@ -0,0 +1,700 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 !os(Windows) + +// Must import here to use C stat properties in @_alwaysEmitIntoClient APIs. +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import CSystem +import Glibc +#elseif canImport(Musl) +import CSystem +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +import CSystem +import Android +#else +#error("Unsupported Platform") +#endif + +// MARK: - Stat + +/// A Swift wrapper of the C `stat` struct. +/// +/// - Note: Only available on Unix-like platforms. +@frozen +// @available(System X.Y.Z, *) +public struct Stat: RawRepresentable, Sendable { + + /// The raw C `stat` struct. + @_alwaysEmitIntoClient + public var rawValue: CInterop.Stat + + /// Creates a Swift `Stat` from the raw C struct. + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Stat) { self.rawValue = rawValue } + + // MARK: Stat.Flags + + /// Flags representing those passed to `fstatat()`. + @frozen + public struct Flags: OptionSet, Sendable, Hashable, Codable { + + /// The raw C flags. + @_alwaysEmitIntoClient + public let rawValue: CInt + + /// Creates a strongly-typed `Stat.Flags` from raw C flags. + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// If the path ends with a symbolic link, return information about the link itself. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW`. + @_alwaysEmitIntoClient + public static var symlinkNoFollow: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW) } + + #if SYSTEM_PACKAGE_DARWIN + /// If the path ends with a symbolic link, return information about the link itself. + /// If _any_ symbolic link is encountered during path resolution, return an error. + /// + /// The corresponding C constant is `AT_SYMLINK_NOFOLLOW_ANY`. + /// - Note: Only available on Darwin. + @_alwaysEmitIntoClient + public static var symlinkNoFollowAny: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW_ANY) } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// If the path does not reside in the hierarchy beneath the starting directory, return an error. + /// + /// The corresponding C constant is `AT_RESOLVE_BENEATH`. + /// - Note: Only available on Darwin and FreeBSD. + @_alwaysEmitIntoClient + public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } + #endif + + #if os(FreeBSD) || os(Linux) || os(Android) + /// If the path is an empty string (or `NULL` since Linux 6.11), + /// return information about the given file descriptor. + /// + /// The corresponding C constant is `AT_EMPTY_PATH`. + /// - Note: Only available on FreeBSD, Linux, and Android. + @_alwaysEmitIntoClient + public static var emptyPath: Flags { Flags(rawValue: _AT_EMPTY_PATH) } + #endif + } + + // MARK: Initializers + + /// Creates a `Stat` struct from a `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._stat( + $0, + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from an`UnsafePointer` path. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._stat( + path, + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _stat( + _ ptr: UnsafePointer, + followTargetSymlink: Bool, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + if followTargetSymlink { + system_stat(ptr, &result) + } else { + system_lstat(ptr, &result) + } + }.map { result } + } + + /// Creates a `Stat` struct from a `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + @_alwaysEmitIntoClient + public init( + _ fd: FileDescriptor, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstat( + fd, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstat( + _ fd: FileDescriptor, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstat(fd.rawValue, &result) + }.map { result } + } + + /// Creates a `Stat` struct from a `FilePath` and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._fstatat( + $0, + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from a `FilePath` and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: FilePath, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try path.withPlatformString { + Self._fstatat( + $0, + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ) + }.get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatat( + path, + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// Creates a `Stat` struct from an `UnsafePointer` path and `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public init( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) { + self.rawValue = try Self._fstatat( + path, + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal static func _fstatat( + _ path: UnsafePointer, + relativeTo fd: FileDescriptor.RawValue, + flags: Stat.Flags, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstatat(fd, path, &result, flags.rawValue) + }.map { result } + } + + + // MARK: Properties + + /// ID of device containing file + /// + /// The corresponding C property is `st_dev`. + @_alwaysEmitIntoClient + public var deviceID: DeviceID { + get { DeviceID(rawValue: rawValue.st_dev) } + set { rawValue.st_dev = newValue.rawValue } + } + + /// Inode number + /// + /// The corresponding C property is `st_ino`. + @_alwaysEmitIntoClient + public var inode: Inode { + get { Inode(rawValue: rawValue.st_ino) } + set { rawValue.st_ino = newValue.rawValue } + } + + /// File mode + /// + /// The corresponding C property is `st_mode`. + @_alwaysEmitIntoClient + public var mode: FileMode { + get { FileMode(rawValue: rawValue.st_mode) } + set { rawValue.st_mode = newValue.rawValue } + } + + /// File type for the given mode + @_alwaysEmitIntoClient + public var type: FileType { + get { mode.type } + set { + var newMode = mode + newMode.type = newValue + mode = newMode + } + } + + /// File permissions for the given mode + @_alwaysEmitIntoClient + public var permissions: FilePermissions { + get { mode.permissions } + set { + var newMode = mode + newMode.permissions = newValue + mode = newMode + } + } + + /// Number of hard links + /// + /// The corresponding C property is `st_nlink`. + @_alwaysEmitIntoClient + public var linkCount: Int { + get { Int(rawValue.st_nlink) } + set { rawValue.st_nlink = numericCast(newValue) } + } + + /// User ID of owner + /// + /// The corresponding C property is `st_uid`. + @_alwaysEmitIntoClient + public var userID: UserID { + get { UserID(rawValue: rawValue.st_uid) } + set { rawValue.st_uid = newValue.rawValue } + } + + /// Group ID of owner + /// + /// The corresponding C property is `st_gid`. + @_alwaysEmitIntoClient + public var groupID: GroupID { + get { GroupID(rawValue: rawValue.st_gid) } + set { rawValue.st_gid = newValue.rawValue } + } + + /// Device ID (if special file) + /// + /// For character or block special files, the returned `DeviceID` may have + /// meaningful `.major` and `.minor` values. For non-special files, this + /// property is usually meaningless and often set to 0. + /// + /// The corresponding C property is `st_rdev`. + @_alwaysEmitIntoClient + public var specialDeviceID: DeviceID { + get { DeviceID(rawValue: rawValue.st_rdev) } + set { rawValue.st_rdev = newValue.rawValue } + } + + /// Total size, in bytes + /// + /// The corresponding C property is `st_size`. + @_alwaysEmitIntoClient + public var size: Int64 { + get { Int64(rawValue.st_size) } + set { rawValue.st_size = numericCast(newValue) } + } + + /// Block size for filesystem I/O, in bytes + /// + /// The corresponding C property is `st_blksize`. + @_alwaysEmitIntoClient + public var preferredIOBlockSize: Int { + get { Int(rawValue.st_blksize) } + set { rawValue.st_blksize = numericCast(newValue) } + } + + /// Number of 512-byte blocks allocated + /// + /// The corresponding C property is `st_blocks`. + @_alwaysEmitIntoClient + public var blocksAllocated: Int64 { + get { Int64(rawValue.st_blocks) } + set { rawValue.st_blocks = numericCast(newValue) } + } + + /// Total size allocated, in bytes + /// + /// - Note: Calculated as `512 * blocksAllocated`. + @_alwaysEmitIntoClient + public var sizeAllocated: Int64 { + 512 * blocksAllocated + } + + // TODO: jflat - Change time properties to UTCClock.Instant when possible. + + /// Time of last access, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var accessTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_atimespec + #else + let timespec = rawValue.st_atim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_atimespec = timespec + #else + rawValue.st_atim = timespec + #endif + } + } + + /// Time of last modification, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var modificationTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_mtimespec + #else + let timespec = rawValue.st_mtim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_mtimespec = timespec + #else + rawValue.st_mtim = timespec + #endif + } + } + + /// Time of last status (inode) change, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var changeTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_ctimespec + #else + let timespec = rawValue.st_ctim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_ctimespec = timespec + #else + rawValue.st_ctim = timespec + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + /// Time of file creation, given as a `Duration` since the Epoch + /// + /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). + /// - Note: Only available on Darwin and FreeBSD. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public var creationTime: Duration { + get { + #if SYSTEM_PACKAGE_DARWIN + let timespec = rawValue.st_birthtimespec + #else + let timespec = rawValue.st_birthtim + #endif + return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) + } + set { + let (seconds, attoseconds) = newValue.components + let timespec = timespec( + tv_sec: numericCast(seconds), + tv_nsec: numericCast(attoseconds / 1_000_000_000) + ) + #if SYSTEM_PACKAGE_DARWIN + rawValue.st_birthtimespec = timespec + #else + rawValue.st_birthtim = timespec + #endif + } + } + #endif + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + /// File flags + /// + /// The corresponding C property is `st_flags`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + @_alwaysEmitIntoClient + public var flags: FileFlags { + get { FileFlags(rawValue: rawValue.st_flags) } + set { rawValue.st_flags = newValue.rawValue } + } + + /// File generation number + /// + /// The file generation number is used to distinguish between different files + /// that have used the same inode over time. + /// + /// The corresponding C property is `st_gen`. + /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. + @_alwaysEmitIntoClient + public var generationNumber: Int { + get { Int(rawValue.st_gen) } + set { rawValue.st_gen = numericCast(newValue)} + } + #endif +} + +// MARK: - Equatable and Hashable + +extension Stat: Equatable { + @_alwaysEmitIntoClient + /// Compares the raw bytes of two `Stat` structs for equality. + public static func == (lhs: Self, rhs: Self) -> Bool { + return withUnsafeBytes(of: lhs.rawValue) { lhsBytes in + withUnsafeBytes(of: rhs.rawValue) { rhsBytes in + lhsBytes.elementsEqual(rhsBytes) + } + } + } +} + +extension Stat: Hashable { + @_alwaysEmitIntoClient + /// Hashes the raw bytes of this `Stat` struct. + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: rawValue) { bytes in + hasher.combine(bytes: bytes) + } + } +} + +// MARK: - CustomStringConvertible and CustomDebugStringConvertible + +// TODO: jflat + +// MARK: - FileDescriptor Extensions + +// @available(System X.Y.Z, *) +extension FileDescriptor { + + /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. + /// + /// The corresponding C function is `fstat()`. + @_alwaysEmitIntoClient + public func stat( + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _fstat( + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _fstat( + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstat(self.rawValue, &result) + }.map { Stat(rawValue: result) } + } +} + +// MARK: - FilePath Extensions + +// @available(System X.Y.Z, *) +extension FilePath { + + /// Creates a `Stat` struct for the file referenced by this `FilePath`. + /// + /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. + /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. + /// If `followTargetSymlink` is set to `false`, this initializer behaves like `lstat()` and + /// returns information about the symlink itself. + /// + /// The corresponding C function is `stat()` or `lstat()` as described above. + @_alwaysEmitIntoClient + public func stat( + followTargetSymlink: Bool = true, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _stat( + followTargetSymlink: followTargetSymlink, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _stat( + followTargetSymlink: Bool, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return withPlatformString { ptr in + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + if followTargetSymlink { + system_stat(ptr, &result) + } else { + system_lstat(ptr, &result) + } + }.map { Stat(rawValue: result) } + } + } + + /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`. + /// + /// If `path` is relative, it is resolved against the current working directory. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public func stat( + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _fstatat( + relativeTo: _AT_FDCWD, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`, + /// including a `FileDescriptor` to resolve a relative path. + /// + /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. + /// If `path` is relative, it is resolved against the directory given by `fd`. + /// + /// The corresponding C function is `fstatat()`. + @_alwaysEmitIntoClient + public func stat( + relativeTo fd: FileDescriptor, + flags: Stat.Flags, + retryOnInterrupt: Bool = true + ) throws(Errno) -> Stat { + try _fstatat( + relativeTo: fd.rawValue, + flags: flags, + retryOnInterrupt: retryOnInterrupt + ).get() + } + + @usableFromInline + internal func _fstatat( + relativeTo fd: FileDescriptor.RawValue, + flags: Stat.Flags, + retryOnInterrupt: Bool + ) -> Result { + var result = CInterop.Stat() + return withPlatformString { ptr in + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fstatat(fd, ptr, &result, flags.rawValue) + }.map { Stat(rawValue: result) } + } + } +} + +#endif // !os(Windows) diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index b6de1233..7a35b09c 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -5,7 +5,7 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ #if SYSTEM_PACKAGE_DARWIN import Darwin @@ -78,4 +78,12 @@ public enum CInterop { /// on API. public typealias PlatformUnicodeEncoding = UTF8 #endif + + #if !os(Windows) + public typealias Stat = stat + public typealias DeviceID = dev_t + public typealias Inode = ino_t + public typealias UserID = uid_t + public typealias GroupID = gid_t + #endif } diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index d8cbdcbd..8805ffad 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -5,7 +5,7 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ // For platform constants redefined in Swift. We define them here so that // they can be used anywhere without imports and without confusion to @@ -17,6 +17,7 @@ import Darwin import CSystem import ucrt #elseif canImport(Glibc) +import CSystem import Glibc #elseif canImport(Musl) import CSystem @@ -438,7 +439,7 @@ internal var _ENOSR: CInt { ENOSR } @_alwaysEmitIntoClient internal var _ENOSTR: CInt { ENOSTR } -#endif +#endif #endif @_alwaysEmitIntoClient @@ -639,3 +640,150 @@ internal var _SEEK_HOLE: CInt { SEEK_HOLE } @_alwaysEmitIntoClient internal var _SEEK_DATA: CInt { SEEK_DATA } #endif + +// MARK: - File System + +#if !os(Windows) + +@_alwaysEmitIntoClient +internal var _AT_FDCWD: CInt { AT_FDCWD } + +// MARK: - fstatat Flags + +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_FOLLOW } + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } +#endif + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } +#endif + +#if os(FreeBSD) || os(Linux) || os(Android) +@_alwaysEmitIntoClient +internal var _AT_EMPTY_PATH: CInt { AT_EMPTY_PATH } +#endif + +// MARK: - File Mode / File Type + +@_alwaysEmitIntoClient +internal var _MODE_FILETYPE_MASK: CInterop.Mode { S_IFMT } + +@_alwaysEmitIntoClient +internal var _MODE_PERMISSIONS_MASK: CInterop.Mode { 0o7777 } + +@_alwaysEmitIntoClient +internal var _S_IFDIR: CInterop.Mode { S_IFDIR } + +@_alwaysEmitIntoClient +internal var _S_IFCHR: CInterop.Mode { S_IFCHR } + +@_alwaysEmitIntoClient +internal var _S_IFBLK: CInterop.Mode { S_IFBLK } + +@_alwaysEmitIntoClient +internal var _S_IFREG: CInterop.Mode { S_IFREG } + +@_alwaysEmitIntoClient +internal var _S_IFIFO: CInterop.Mode { S_IFIFO } + +@_alwaysEmitIntoClient +internal var _S_IFLNK: CInterop.Mode { S_IFLNK } + +@_alwaysEmitIntoClient +internal var _S_IFSOCK: CInterop.Mode { S_IFSOCK } + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _S_IFWHT: CInterop.Mode { S_IFWHT } +#endif + +// MARK: - stat/chflags File Flags + +// MARK: Flags Available on Darwin, FreeBSD, and OpenBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) +@_alwaysEmitIntoClient +internal var _UF_NODUMP: CInterop.FileFlags { UInt32(bitPattern: UF_NODUMP) } + +@_alwaysEmitIntoClient +internal var _UF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: UF_IMMUTABLE) } + +@_alwaysEmitIntoClient +internal var _UF_APPEND: CInterop.FileFlags { UInt32(bitPattern: UF_APPEND) } + +@_alwaysEmitIntoClient +internal var _SF_ARCHIVED: CInterop.FileFlags { UInt32(bitPattern: SF_ARCHIVED) } + +@_alwaysEmitIntoClient +internal var _SF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: SF_IMMUTABLE) } + +@_alwaysEmitIntoClient +internal var _SF_APPEND: CInterop.FileFlags { UInt32(bitPattern: SF_APPEND) } +#endif + +// MARK: Flags Available on Darwin and FreeBSD + +#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +@_alwaysEmitIntoClient +internal var _UF_OPAQUE: CInterop.FileFlags { UInt32(bitPattern: UF_OPAQUE) } + +@_alwaysEmitIntoClient +internal var _UF_COMPRESSED: CInterop.FileFlags { UInt32(bitPattern: UF_COMPRESSED) } + +@_alwaysEmitIntoClient +internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } + +@_alwaysEmitIntoClient +internal var _UF_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } + +@_alwaysEmitIntoClient +internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } + +@_alwaysEmitIntoClient +internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) } +#endif + +// MARK: Flags Available on Darwin Only + +#if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _UF_DATAVAULT: CInterop.FileFlags { UInt32(bitPattern: UF_DATAVAULT) } + +@_alwaysEmitIntoClient +internal var _SF_FIRMLINK: CInterop.FileFlags { UInt32(bitPattern: SF_FIRMLINK) } + +@_alwaysEmitIntoClient +internal var _SF_DATALESS: CInterop.FileFlags { UInt32(bitPattern: SF_DATALESS) } +#endif + +// MARK: Flags Available on FreeBSD Only + +#if os(FreeBSD) +@_alwaysEmitIntoClient +internal var _UF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: UF_NOUNLINK) } + +@_alwaysEmitIntoClient +internal var _UF_OFFLINE: CInterop.FileFlags { UInt32(bitPattern: UF_OFFLINE) } + +@_alwaysEmitIntoClient +internal var _UF_READONLY: CInterop.FileFlags { UInt32(bitPattern: UF_READONLY) } + +@_alwaysEmitIntoClient +internal var _UF_REPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_REPARSE) } + +@_alwaysEmitIntoClient +internal var _UF_SPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_SPARSE) } + +@_alwaysEmitIntoClient +internal var _UF_SYSTEM: CInterop.FileFlags { UInt32(bitPattern: UF_SYSTEM) } + +@_alwaysEmitIntoClient +internal var _SF_SNAPSHOT: CInterop.FileFlags { UInt32(bitPattern: SF_SNAPSHOT) } +#endif + +#endif // !os(Windows) diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index c7d9944f..025aefae 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -5,7 +5,7 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ // Internal wrappers and typedefs which help reduce #if littering in System's // code base. @@ -90,6 +90,34 @@ internal func system_strlen(_ s: UnsafeMutablePointer) -> Int { strlen(s) } +#if !os(Windows) +internal func system_stat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { + stat(p, &s) +} +internal func system_lstat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { + lstat(p, &s) +} +internal func system_fstat(_ fd: CInt, _ s: inout CInterop.Stat) -> Int32 { + fstat(fd, &s) +} +internal func system_fstatat(_ fd: CInt, _ p: UnsafePointer, _ s: inout CInterop.Stat, _ flags: CInt) -> Int32 { + fstatat(fd, p, &s, flags) +} + +@usableFromInline +internal func system_major(_ dev: CInterop.DeviceID) -> CInt { + numericCast((dev >> 24) & 0xff) +} +@usableFromInline +internal func system_minor(_ dev: CInterop.DeviceID) -> CInt { + numericCast(dev & 0xffffff) +} +@usableFromInline +internal func system_makedev(_ maj: CUnsignedInt, _ min: CUnsignedInt) -> CInterop.DeviceID { + CInterop.DeviceID((maj << 24) | min) +} +#endif + // Convention: `system_platform_foo` is a // platform-representation-abstracted wrapper around `foo`-like functionality. // Type and layout differences such as the `char` vs `wchar` are abstracted. @@ -167,20 +195,20 @@ internal typealias _PlatformTLSKey = DWORD #elseif os(WASI) && (swift(<6.1) || !_runtime(_multithreaded)) // Mock TLS storage for single-threaded WASI internal final class _PlatformTLSKey { - fileprivate init() {} + fileprivate init() {} } private final class TLSStorage: @unchecked Sendable { - var storage = [ObjectIdentifier: UnsafeMutableRawPointer]() + var storage = [ObjectIdentifier: UnsafeMutableRawPointer]() } private let sharedTLSStorage = TLSStorage() func pthread_setspecific(_ key: _PlatformTLSKey, _ p: UnsafeMutableRawPointer?) -> Int { - sharedTLSStorage.storage[ObjectIdentifier(key)] = p - return 0 + sharedTLSStorage.storage[ObjectIdentifier(key)] = p + return 0 } func pthread_getspecific(_ key: _PlatformTLSKey) -> UnsafeMutableRawPointer? { - sharedTLSStorage.storage[ObjectIdentifier(key)] + sharedTLSStorage.storage[ObjectIdentifier(key)] } #else internal typealias _PlatformTLSKey = pthread_key_t diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift new file mode 100644 index 00000000..bc302a71 --- /dev/null +++ b/Tests/SystemTests/FileModeTests.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@Suite("FileMode") +private struct FileModeTests { + + @Test func basics() async throws { + var mode = FileMode(rawValue: S_IFREG | 0o644) // Regular file, rw-r--r-- + #expect(mode.type == .regular) + #expect(mode.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + mode.type = .directory // Directory, rw-r--r-- + #expect(mode.type == .directory) + #expect(mode.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + mode.permissions.insert([.ownerExecute, .groupExecute, .otherExecute]) // Directory, rwxr-xr-x + #expect(mode.type == .directory) + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + + mode.type = .symbolicLink // Symbolic link, rwxr-xr-x + #expect(mode.type == .symbolicLink) + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + + let mode1 = FileMode(rawValue: S_IFLNK | 0o755) // Symbolic link, rwxr-xr-x + let mode2 = FileMode(type: .symbolicLink, permissions: [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute]) + #expect(mode == mode1) + #expect(mode1 == mode2) + + mode.permissions.remove([.otherReadExecute]) // Symbolic link, rwxr-x--- + #expect(mode.permissions == [.ownerReadWriteExecute, .groupReadExecute]) + #expect(mode != mode1) + #expect(mode != mode2) + #expect(mode.type == mode1.type) + #expect(mode.type == mode2.type) + } + + @Test func invalidInput() async throws { + // No permissions, all other bits set + var invalidMode = FileMode(rawValue: ~0o7777) + #expect(invalidMode.permissions.isEmpty) + #expect(invalidMode.type != .directory) + #expect(invalidMode.type != .characterSpecial) + #expect(invalidMode.type != .blockSpecial) + #expect(invalidMode.type != .regular) + #expect(invalidMode.type != .pipe) + #expect(invalidMode.type != .symbolicLink) + #expect(invalidMode.type != .socket) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(invalidMode.type != .whiteout) + #endif + + // All file-type bits set + invalidMode = FileMode(rawValue: S_IFMT) + #expect(invalidMode.type != .directory) + #expect(invalidMode.type != .characterSpecial) + #expect(invalidMode.type != .blockSpecial) + #expect(invalidMode.type != .regular) + #expect(invalidMode.type != .pipe) + #expect(invalidMode.type != .symbolicLink) + #expect(invalidMode.type != .socket) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(invalidMode.type != .whiteout) + #endif + + // FileMode(type:permissions:) masks its inputs so + // they don't accidentally modify the other bits. + let emptyPermissions = FileMode(type: FileType(rawValue: ~0), permissions: []) + #expect(emptyPermissions.permissions.isEmpty) + #expect(emptyPermissions.type == FileType(rawValue: S_IFMT)) + #expect(emptyPermissions == invalidMode) + + let regularFile = FileMode(type: .regular, permissions: FilePermissions(rawValue: ~0)) + #expect(regularFile.type == .regular) + #expect(regularFile.permissions == FilePermissions(rawValue: 0o7777)) + #expect(regularFile.permissions == [ + .ownerReadWriteExecute, + .groupReadWriteExecute, + .otherReadWriteExecute, + .setUserID, .setGroupID, .saveText + ]) + + // Setting properties should not modify the other bits, either. + var mode = FileMode(rawValue: 0) + mode.type = FileType(rawValue: ~0) + #expect(mode.type == FileType(rawValue: S_IFMT)) + #expect(mode.permissions.isEmpty) + + mode.type.rawValue = 0 + #expect(mode.type == FileType(rawValue: 0)) + #expect(mode.permissions.isEmpty) + + mode.permissions = FilePermissions(rawValue: ~0) + #expect(mode.permissions == FilePermissions(rawValue: 0o7777)) + #expect(mode.type == FileType(rawValue: 0)) + + mode.permissions = [] + #expect(mode.permissions.isEmpty) + #expect(mode.type == FileType(rawValue: 0)) + } + +} +#endif diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift new file mode 100644 index 00000000..af93f6ea --- /dev/null +++ b/Tests/SystemTests/StatTests.swift @@ -0,0 +1,408 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift System open source project +// +// Copyright (c) 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 !os(Windows) + +import Testing + +#if SYSTEM_PACKAGE_DARWIN +import Darwin +#elseif canImport(Glibc) +import CSystem +import Glibc +#elseif canImport(Musl) +import CSystem +import Musl +#elseif canImport(WASILibc) +import CSystem +import WASILibc +#elseif canImport(Android) +import Android +#else +#error("Unsupported Platform") +#endif + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +@Suite("Stat") +private struct StatTests { + + @Test func basics() async throws { + try withTemporaryFilePath(basename: "Stat_basics") { tempDir in + let dirStatFromFilePath = try tempDir.stat() + #expect(dirStatFromFilePath.type == .directory) + + let dirFD = try FileDescriptor.open(tempDir, .readOnly) + defer { + try? dirFD.close() + } + let dirStatFromFD = try dirFD.stat() + #expect(dirStatFromFD.type == .directory) + + let dirStatFromCString = try tempDir.withPlatformString { try Stat($0) } + #expect(dirStatFromCString.type == .directory) + + #expect(dirStatFromFilePath == dirStatFromFD) + #expect(dirStatFromFD == dirStatFromCString) + + let tempFile = tempDir.appending("test.txt") + let fileFD = try FileDescriptor.open(tempFile, .readWrite, options: .create, permissions: [.ownerReadWrite, .groupRead, .otherRead]) + defer { + try? fileFD.close() + } + try fileFD.writeAll("Hello, world!".utf8) + + let fileStatFromFD = try fileFD.stat() + #expect(fileStatFromFD.type == .regular) + #expect(fileStatFromFD.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + #expect(fileStatFromFD.size == "Hello, world!".utf8.count) + + let fileStatFromFilePath = try tempFile.stat() + #expect(fileStatFromFilePath.type == .regular) + + let fileStatFromCString = try tempFile.withPlatformString { try Stat($0) } + #expect(fileStatFromCString.type == .regular) + + #expect(fileStatFromFD == fileStatFromFilePath) + #expect(fileStatFromFilePath == fileStatFromCString) + } + } + + @Test + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func followSymlinkInits() async throws { + try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in + let targetFilePath = tempDir.appending("target.txt") + let symlinkPath = tempDir.appending("symlink") + let targetFD = try FileDescriptor.open(targetFilePath, .readWrite, options: .create, permissions: .ownerReadWrite) + defer { + try? targetFD.close() + } + try targetFD.writeAll(Array(repeating: UInt8(ascii: "A"), count: 1025)) + + try targetFilePath.withPlatformString { targetPtr in + try symlinkPath.withPlatformString { symlinkPtr in + try #require(symlink(targetPtr, symlinkPtr) == 0, "\(Errno.current)") + } + } + + #if !os(WASI) // Can't open an fd to a symlink on WASI (no O_PATH) + #if SYSTEM_PACKAGE_DARWIN + let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: .symlink) + #else + // Need O_PATH | O_NOFOLLOW to open the symlink directly + let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: [.path, .noFollow]) + #endif + defer { + try? symlinkFD.close() + } + #endif // !os(WASI) + + let targetStat = try targetFilePath.stat() + let originalTargetAccessTime = targetStat.accessTime + + let symlinkStat = try symlinkPath.stat(followTargetSymlink: false) + let originalSymlinkAccessTime = symlinkStat.accessTime + + #expect(targetStat != symlinkStat) + #expect(targetStat.type == .regular) + #expect(symlinkStat.type == .symbolicLink) + #expect(symlinkStat.size < targetStat.size) + #expect(symlinkStat.sizeAllocated < targetStat.sizeAllocated) + + // Set each .accessTime back to its original value for comparison + + // FileDescriptor Extensions + + var stat = try targetFD.stat() + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + #if !os(WASI) + stat = try symlinkFD.stat() + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // Initializing Stat with FileDescriptor + + stat = try Stat(targetFD) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + #if !os(WASI) + stat = try Stat(symlinkFD) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + #endif + + // FilePath Extensions + + stat = try symlinkPath.stat(followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try symlinkPath.stat(followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with UnsafePointer + + try symlinkPath.withPlatformString { pathPtr in + stat = try Stat(pathPtr, followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(pathPtr, followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + + // Initializing Stat with FilePath + + stat = try Stat(symlinkPath, followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath, followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + + // Initializing Stat with String + + stat = try Stat(symlinkPath.string, followTargetSymlink: true) + stat.accessTime = originalTargetAccessTime + #expect(stat == targetStat) + + stat = try Stat(symlinkPath.string, followTargetSymlink: false) + stat.accessTime = originalSymlinkAccessTime + #expect(stat == symlinkStat) + } + } + + @Test func permissions() async throws { + try withTemporaryFilePath(basename: "Stat_permissions") { tempDir in + let testFile = tempDir.appending("test.txt") + let fd = try FileDescriptor.open(testFile, .writeOnly, options: .create, permissions: [.ownerReadWrite, .groupRead, .otherRead]) + try fd.close() + + let stat = try testFile.stat() + #expect(stat.type == .regular) + #expect(stat.permissions == [.ownerReadWrite, .groupRead, .otherRead]) + + var newMode = stat.mode + newMode.permissions.insert(.ownerExecute) + try testFile.withPlatformString { pathPtr in + try #require(chmod(pathPtr, newMode.permissions.rawValue) == 0, "\(Errno.current)") + } + + let updatedStat = try testFile.stat() + #expect(updatedStat.permissions == newMode.permissions) + + newMode.permissions.remove(.ownerWriteExecute) + try testFile.withPlatformString { pathPtr in + try #require(chmod(pathPtr, newMode.permissions.rawValue) == 0, "\(Errno.current)") + } + + let readOnlyStat = try testFile.stat() + #expect(readOnlyStat.permissions == newMode.permissions) + } + } + + @Test + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func times() async throws { + let startTime = Int64(time(nil)) + try #require(startTime >= 0, "\(Errno.current)") + let start: Duration = .seconds(startTime - 1) // A little wiggle room + try withTemporaryFilePath(basename: "Stat_times") { tempDir in + var dirStat = try tempDir.stat() + let dirAccessTime0 = dirStat.accessTime + let dirModificationTime0 = dirStat.modificationTime + let dirChangeTime0 = dirStat.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime0 = dirStat.creationTime + #endif + + #expect(dirAccessTime0 >= start) + #expect(dirAccessTime0 < start + .seconds(5)) + #expect(dirModificationTime0 >= start) + #expect(dirModificationTime0 < start + .seconds(5)) + #expect(dirChangeTime0 >= start) + #expect(dirChangeTime0 < start + .seconds(5)) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime0 >= start) + #expect(dirCreationTime0 < start + .seconds(5)) + #endif + + // Fails intermittently if less than 5ms + usleep(10000) + + let file1 = tempDir.appending("test1.txt") + let fd1 = try FileDescriptor.open(file1, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd1.close() + } + + dirStat = try tempDir.stat() + let dirAccessTime1 = dirStat.accessTime + let dirModificationTime1 = dirStat.modificationTime + let dirChangeTime1 = dirStat.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let dirCreationTime1 = dirStat.creationTime + #endif + + // Creating a file updates directory modification and change time. + // Access time may not be updated depending on mount options like NOATIME. + + #expect(dirModificationTime1 > dirModificationTime0) + #expect(dirChangeTime1 > dirChangeTime0) + #expect(dirAccessTime1 >= dirAccessTime0) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirCreationTime1 == dirCreationTime0) + #endif + + usleep(10000) + + // Changing permissions only updates directory change time + + try tempDir.withPlatformString { pathPtr in + var newMode = dirStat.mode + // tempDir only starts with .ownerReadWriteExecute + newMode.permissions.insert(.groupReadWriteExecute) + try #require(chmod(pathPtr, newMode.rawValue) == 0, "\(Errno.current)") + } + + dirStat = try tempDir.stat() + let dirChangeTime2 = dirStat.changeTime + #expect(dirChangeTime2 > dirChangeTime1) + #expect(dirStat.accessTime == dirAccessTime1) + #expect(dirStat.modificationTime == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.creationTime == dirCreationTime1) + #endif + + var stat1 = try file1.stat() + let file1AccessTime1 = stat1.accessTime + let file1ModificationTime1 = stat1.modificationTime + let file1ChangeTime1 = stat1.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime1 = stat1.creationTime + #endif + + usleep(10000) + + try fd1.writeAll("Hello, world!".utf8) + stat1 = try file1.stat() + let file1AccessTime2 = stat1.accessTime + let file1ModificationTime2 = stat1.modificationTime + let file1ChangeTime2 = stat1.changeTime + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + let file1CreationTime2 = stat1.creationTime + #endif + + #expect(file1AccessTime2 >= file1AccessTime1) + #expect(file1ModificationTime2 > file1ModificationTime1) + #expect(file1ChangeTime2 > file1ChangeTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(file1CreationTime2 == file1CreationTime1) + #endif + + // Changing file metadata or content does not update directory times + + dirStat = try tempDir.stat() + #expect(dirStat.changeTime == dirChangeTime2) + #expect(dirStat.accessTime == dirAccessTime1) + #expect(dirStat.modificationTime == dirModificationTime1) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(dirStat.creationTime == dirCreationTime1) + #endif + + usleep(10000) + + let file2 = tempDir.appending("test2.txt") + let fd2 = try FileDescriptor.open(file2, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd2.close() + } + + let stat2 = try file2.stat() + #expect(stat2.accessTime > file1AccessTime2) + #expect(stat2.modificationTime > file1ModificationTime2) + #expect(stat2.changeTime > file1ChangeTime2) + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #expect(stat2.creationTime > file1CreationTime2) + #endif + } + } + + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + @Test func flags() async throws { + try withTemporaryFilePath(basename: "Stat_flags") { tempDir in + let filePath = tempDir.appending("test.txt") + let fd = try FileDescriptor.open(filePath, .writeOnly, options: .create, permissions: .ownerReadWrite) + defer { + try? fd.close() + } + var stat = try fd.stat() + var flags = stat.flags + + #if SYSTEM_PACKAGE_DARWIN + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend, + .opaque, .tracked, .hidden, + /* .dataVault (throws EPERM when testing) */ + ] + #elseif os(FreeBSD) + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend, + .opaque, .tracked, .hidden, + .userNoUnlink, + .offline, + .readOnly, + .reparse, + .sparse, + .system + ] + #else // os(OpenBSD) + let userSettableFlags: FileFlags = [ + .noDump, .userImmutable, .userAppend + ] + #endif + + flags.insert(userSettableFlags) + try #require(fchflags(fd.rawValue, flags.rawValue) == 0, "\(Errno.current)") + + stat = try fd.stat() + #expect(stat.flags == flags) + + flags.remove(userSettableFlags) + try #require(fchflags(fd.rawValue, flags.rawValue) == 0, "\(Errno.current)") + + stat = try fd.stat() + #expect(stat.flags == flags) + } + } + #endif + +} + +#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) +private extension FileDescriptor.OpenOptions { + static var path: Self { Self(rawValue: O_PATH) } +} +#endif + +#endif From a07696ace3ea8ce5448543543943a2ec8e84ba04 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 26 Aug 2025 17:23:41 -0600 Subject: [PATCH 157/172] Fix AT_RESOLVE_BENEATH availability --- Sources/System/FileSystem/Stat.swift | 3 ++- Sources/System/Internals/Constants.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 76f37b0a..4e857273 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -76,12 +76,13 @@ public struct Stat: RawRepresentable, Sendable { public static var symlinkNoFollowAny: Flags { Flags(rawValue: _AT_SYMLINK_NOFOLLOW_ANY) } #endif - #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) + #if canImport(Darwin, _version: 346) || os(FreeBSD) /// If the path does not reside in the hierarchy beneath the starting directory, return an error. /// /// The corresponding C constant is `AT_RESOLVE_BENEATH`. /// - Note: Only available on Darwin and FreeBSD. @_alwaysEmitIntoClient + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } #endif diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 8805ffad..37b7da33 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -658,7 +658,7 @@ internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_FOLLOW } internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } #endif -#if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +#if canImport(Darwin, _version: 346) || os(FreeBSD) @_alwaysEmitIntoClient internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } #endif From 905f9f842dc6567cc29bee7470f8769918b84bde Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 26 Aug 2025 17:27:22 -0600 Subject: [PATCH 158/172] Expose C timespec properties until UTCClock can be used --- Sources/System/FileSystem/Stat.swift | 135 ++++++++++++++++----------- Tests/SystemTests/StatTests.swift | 120 ++++++++++++++---------- 2 files changed, 150 insertions(+), 105 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 4e857273..9f2e5503 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -410,119 +410,146 @@ public struct Stat: RawRepresentable, Sendable { 512 * blocksAllocated } - // TODO: jflat - Change time properties to UTCClock.Instant when possible. - - /// Time of last access, given as a `Duration` since the Epoch + /// Time of last access, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var accessTime: Duration { + @_alwaysEmitIntoClient + public var st_atim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_atimespec + rawValue.st_atimespec #else - let timespec = rawValue.st_atim + rawValue.st_atim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_atimespec = timespec + rawValue.st_atimespec = newValue #else - rawValue.st_atim = timespec + rawValue.st_atim = newValue #endif } } - /// Time of last modification, given as a `Duration` since the Epoch + /// Time of last modification, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var modificationTime: Duration { + @_alwaysEmitIntoClient + public var st_mtim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_mtimespec + rawValue.st_mtimespec #else - let timespec = rawValue.st_mtim + rawValue.st_mtim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_mtimespec = timespec + rawValue.st_mtimespec = newValue #else - rawValue.st_mtim = timespec + rawValue.st_mtim = newValue #endif } } - /// Time of last status (inode) change, given as a `Duration` since the Epoch + /// Time of last status (inode) change, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var changeTime: Duration { + @_alwaysEmitIntoClient + public var st_ctim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_ctimespec + rawValue.st_ctimespec #else - let timespec = rawValue.st_ctim + rawValue.st_ctim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_ctimespec = timespec + rawValue.st_ctimespec = newValue #else - rawValue.st_ctim = timespec + rawValue.st_ctim = newValue #endif } } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - /// Time of file creation, given as a `Duration` since the Epoch + /// Time of file creation, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). /// - Note: Only available on Darwin and FreeBSD. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public var creationTime: Duration { + @_alwaysEmitIntoClient + public var st_birthtim: timespec { get { #if SYSTEM_PACKAGE_DARWIN - let timespec = rawValue.st_birthtimespec + rawValue.st_birthtimespec #else - let timespec = rawValue.st_birthtim + rawValue.st_birthtim #endif - return .seconds(timespec.tv_sec) + .nanoseconds(timespec.tv_nsec) } set { - let (seconds, attoseconds) = newValue.components - let timespec = timespec( - tv_sec: numericCast(seconds), - tv_nsec: numericCast(attoseconds / 1_000_000_000) - ) #if SYSTEM_PACKAGE_DARWIN - rawValue.st_birthtimespec = timespec + rawValue.st_birthtimespec = newValue #else - rawValue.st_birthtim = timespec + rawValue.st_birthtim = newValue #endif } } #endif + // TODO: jflat - Change time properties to UTCClock.Instant when possible. + +// /// Time of last access, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). +// public var accessTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_atim)) +// } +// set { +// st_atim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// +// /// Time of last modification, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). +// public var modificationTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_mtim)) +// } +// set { +// st_mtim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// +// /// Time of last status (inode) change, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). +// public var changeTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_ctim)) +// } +// set { +// st_ctim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// +// #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) +// /// Time of file creation, given as a `UTCClock.Instant` +// /// +// /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). +// /// - Note: Only available on Darwin and FreeBSD. +// public var creationTime: UTCClock.Instant { +// get { +// UTCClock.systemEpoch.advanced(by: Duration(st_birthtim)) +// } +// set { +// st_birthtim = timespec(UTCClock.systemEpoch.duration(to: newValue)) +// } +// } +// #endif + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) /// File flags /// diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index af93f6ea..58c34413 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -111,10 +111,10 @@ private struct StatTests { #endif // !os(WASI) let targetStat = try targetFilePath.stat() - let originalTargetAccessTime = targetStat.accessTime + let originalTargetAccessTime = targetStat.st_atim let symlinkStat = try symlinkPath.stat(followTargetSymlink: false) - let originalSymlinkAccessTime = symlinkStat.accessTime + let originalSymlinkAccessTime = symlinkStat.st_atim #expect(targetStat != symlinkStat) #expect(targetStat.type == .regular) @@ -122,72 +122,72 @@ private struct StatTests { #expect(symlinkStat.size < targetStat.size) #expect(symlinkStat.sizeAllocated < targetStat.sizeAllocated) - // Set each .accessTime back to its original value for comparison + // Set each .st_atim back to its original value for comparison // FileDescriptor Extensions var stat = try targetFD.stat() - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) #if !os(WASI) stat = try symlinkFD.stat() - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) #endif // Initializing Stat with FileDescriptor stat = try Stat(targetFD) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) #if !os(WASI) stat = try Stat(symlinkFD) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) #endif // FilePath Extensions stat = try symlinkPath.stat(followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try symlinkPath.stat(followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) // Initializing Stat with UnsafePointer try symlinkPath.withPlatformString { pathPtr in stat = try Stat(pathPtr, followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try Stat(pathPtr, followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) } // Initializing Stat with FilePath stat = try Stat(symlinkPath, followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try Stat(symlinkPath, followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) // Initializing Stat with String stat = try Stat(symlinkPath.string, followTargetSymlink: true) - stat.accessTime = originalTargetAccessTime + stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) stat = try Stat(symlinkPath.string, followTargetSymlink: false) - stat.accessTime = originalSymlinkAccessTime + stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) } } @@ -222,29 +222,30 @@ private struct StatTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func times() async throws { - let startTime = Int64(time(nil)) - try #require(startTime >= 0, "\(Errno.current)") - let start: Duration = .seconds(startTime - 1) // A little wiggle room + var start = timespec() + try #require(clock_gettime(CLOCK_REALTIME, &start) == 0, "\(Errno.current)") + start.tv_sec -= 1 // A little wiggle room try withTemporaryFilePath(basename: "Stat_times") { tempDir in var dirStat = try tempDir.stat() - let dirAccessTime0 = dirStat.accessTime - let dirModificationTime0 = dirStat.modificationTime - let dirChangeTime0 = dirStat.changeTime + let dirAccessTime0 = dirStat.st_atim + let dirModificationTime0 = dirStat.st_mtim + let dirChangeTime0 = dirStat.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let dirCreationTime0 = dirStat.creationTime + let dirCreationTime0 = dirStat.st_birthtim #endif + var startUpperBound = start + startUpperBound.tv_sec += 5 #expect(dirAccessTime0 >= start) - #expect(dirAccessTime0 < start + .seconds(5)) + #expect(dirAccessTime0 < startUpperBound) #expect(dirModificationTime0 >= start) - #expect(dirModificationTime0 < start + .seconds(5)) + #expect(dirModificationTime0 < startUpperBound) #expect(dirChangeTime0 >= start) - #expect(dirChangeTime0 < start + .seconds(5)) + #expect(dirChangeTime0 < startUpperBound) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) #expect(dirCreationTime0 >= start) - #expect(dirCreationTime0 < start + .seconds(5)) + #expect(dirCreationTime0 < startUpperBound) #endif // Fails intermittently if less than 5ms @@ -257,11 +258,11 @@ private struct StatTests { } dirStat = try tempDir.stat() - let dirAccessTime1 = dirStat.accessTime - let dirModificationTime1 = dirStat.modificationTime - let dirChangeTime1 = dirStat.changeTime + let dirAccessTime1 = dirStat.st_atim + let dirModificationTime1 = dirStat.st_mtim + let dirChangeTime1 = dirStat.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let dirCreationTime1 = dirStat.creationTime + let dirCreationTime1 = dirStat.st_birthtim #endif // Creating a file updates directory modification and change time. @@ -286,31 +287,31 @@ private struct StatTests { } dirStat = try tempDir.stat() - let dirChangeTime2 = dirStat.changeTime + let dirChangeTime2 = dirStat.st_ctim #expect(dirChangeTime2 > dirChangeTime1) - #expect(dirStat.accessTime == dirAccessTime1) - #expect(dirStat.modificationTime == dirModificationTime1) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - #expect(dirStat.creationTime == dirCreationTime1) + #expect(dirStat.st_birthtim == dirCreationTime1) #endif var stat1 = try file1.stat() - let file1AccessTime1 = stat1.accessTime - let file1ModificationTime1 = stat1.modificationTime - let file1ChangeTime1 = stat1.changeTime + let file1AccessTime1 = stat1.st_atim + let file1ModificationTime1 = stat1.st_mtim + let file1ChangeTime1 = stat1.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let file1CreationTime1 = stat1.creationTime + let file1CreationTime1 = stat1.st_birthtim #endif usleep(10000) try fd1.writeAll("Hello, world!".utf8) stat1 = try file1.stat() - let file1AccessTime2 = stat1.accessTime - let file1ModificationTime2 = stat1.modificationTime - let file1ChangeTime2 = stat1.changeTime + let file1AccessTime2 = stat1.st_atim + let file1ModificationTime2 = stat1.st_mtim + let file1ChangeTime2 = stat1.st_ctim #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - let file1CreationTime2 = stat1.creationTime + let file1CreationTime2 = stat1.st_birthtim #endif #expect(file1AccessTime2 >= file1AccessTime1) @@ -323,11 +324,11 @@ private struct StatTests { // Changing file metadata or content does not update directory times dirStat = try tempDir.stat() - #expect(dirStat.changeTime == dirChangeTime2) - #expect(dirStat.accessTime == dirAccessTime1) - #expect(dirStat.modificationTime == dirModificationTime1) + #expect(dirStat.st_ctim == dirChangeTime2) + #expect(dirStat.st_atim == dirAccessTime1) + #expect(dirStat.st_mtim == dirModificationTime1) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - #expect(dirStat.creationTime == dirCreationTime1) + #expect(dirStat.st_birthtim == dirCreationTime1) #endif usleep(10000) @@ -339,11 +340,11 @@ private struct StatTests { } let stat2 = try file2.stat() - #expect(stat2.accessTime > file1AccessTime2) - #expect(stat2.modificationTime > file1ModificationTime2) - #expect(stat2.changeTime > file1ChangeTime2) + #expect(stat2.st_atim > file1AccessTime2) + #expect(stat2.st_mtim > file1ModificationTime2) + #expect(stat2.st_ctim > file1ChangeTime2) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) - #expect(stat2.creationTime > file1CreationTime2) + #expect(stat2.st_birthtim > file1CreationTime2) #endif } } @@ -405,4 +406,21 @@ private extension FileDescriptor.OpenOptions { } #endif +// Comparison operators for timespec until UTCClock.Instant properties are available +private func >= (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) >= (rhs.tv_sec, rhs.tv_nsec) +} + +private func < (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) < (rhs.tv_sec, rhs.tv_nsec) +} + +private func > (lhs: timespec, rhs: timespec) -> Bool { + (lhs.tv_sec, lhs.tv_nsec) > (rhs.tv_sec, rhs.tv_nsec) +} + +private func == (lhs: timespec, rhs: timespec) -> Bool { + lhs.tv_sec == rhs.tv_sec && lhs.tv_nsec == rhs.tv_nsec +} + #endif From 2f5b12b854f5003a7b7950c00e79a15bbb5f7349 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 29 Sep 2025 11:59:27 -0600 Subject: [PATCH 159/172] Updates for proposal v3 --- Sources/System/FileSystem/FileFlags.swift | 48 +++++++++---------- Sources/System/FileSystem/FileMode.swift | 2 +- Sources/System/FileSystem/FileType.swift | 20 +++++--- Sources/System/FileSystem/Identifiers.swift | 53 ++++++++++++++------- Sources/System/FileSystem/Stat.swift | 34 ++++++++++--- Sources/System/Internals/Constants.swift | 18 +++---- Tests/SystemTests/FileModeTests.swift | 4 +- 7 files changed, 111 insertions(+), 68 deletions(-) diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift index f89cbc15..fd714cb8 100644 --- a/Sources/System/FileSystem/FileFlags.swift +++ b/Sources/System/FileSystem/FileFlags.swift @@ -21,12 +21,12 @@ // | systemImmutable | SF_IMMUTABLE | SF_IMMUTABLE | SF_IMMUTABLE | // | systemAppend | SF_APPEND | SF_APPEND | SF_APPEND | // | opaque | UF_OPAQUE | UF_OPAQUE | N/A | -// | compressed | UF_COMPRESSED | UF_COMPRESSED | N/A | -// | tracked | UF_TRACKED | UF_TRACKED | N/A | // | hidden | UF_HIDDEN | UF_HIDDEN | N/A | -// | restricted | SF_RESTRICTED | SF_RESTRICTED | N/A | // | systemNoUnlink | SF_NOUNLINK | SF_NOUNLINK | N/A | +// | compressed | UF_COMPRESSED | N/A | N/A | +// | tracked | UF_TRACKED | N/A | N/A | // | dataVault | UF_DATAVAULT | N/A | N/A | +// | restricted | SF_RESTRICTED | N/A | N/A | // | firmlink | SF_FIRMLINK | N/A | N/A | // | dataless | SF_DATALESS | N/A | N/A | // | userNoUnlink | N/A | UF_NOUNLINK | N/A | @@ -114,20 +114,6 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var opaque: FileFlags { FileFlags(rawValue: _UF_OPAQUE) } - /// File is compressed at the file system level. - /// - /// The corresponding C constant is `UF_COMPRESSED`. - /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. - @_alwaysEmitIntoClient - public static var compressed: FileFlags { FileFlags(rawValue: _UF_COMPRESSED) } - - /// File is tracked for the purpose of document IDs. - /// - /// The corresponding C constant is `UF_TRACKED`. - /// - Note: This flag may be changed by the file owner or superuser. - @_alwaysEmitIntoClient - public static var tracked: FileFlags { FileFlags(rawValue: _UF_TRACKED) } - /// File should not be displayed in a GUI. /// /// The corresponding C constant is `UF_HIDDEN`. @@ -135,13 +121,6 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var hidden: FileFlags { FileFlags(rawValue: _UF_HIDDEN) } - /// File requires an entitlement for writing. - /// - /// The corresponding C constant is `SF_RESTRICTED`. - /// - Note: This flag may only be changed by the superuser. - @_alwaysEmitIntoClient - public static var restricted: FileFlags { FileFlags(rawValue: _SF_RESTRICTED) } - /// File may not be removed or renamed. /// /// The corresponding C constant is `SF_NOUNLINK`. @@ -153,6 +132,20 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { // MARK: Flags Available on Darwin only #if SYSTEM_PACKAGE_DARWIN + /// File is compressed at the file system level. + /// + /// The corresponding C constant is `UF_COMPRESSED`. + /// - Note: This flag is read-only. Attempting to change it will result in undefined behavior. + @_alwaysEmitIntoClient + public static var compressed: FileFlags { FileFlags(rawValue: _UF_COMPRESSED) } + + /// File is tracked for the purpose of document IDs. + /// + /// The corresponding C constant is `UF_TRACKED`. + /// - Note: This flag may be changed by the file owner or superuser. + @_alwaysEmitIntoClient + public static var tracked: FileFlags { FileFlags(rawValue: _UF_TRACKED) } + /// File requires an entitlement for reading and writing. /// /// The corresponding C constant is `UF_DATAVAULT`. @@ -160,6 +153,13 @@ public struct FileFlags: OptionSet, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var dataVault: FileFlags { FileFlags(rawValue: _UF_DATAVAULT) } + /// File requires an entitlement for writing. + /// + /// The corresponding C constant is `SF_RESTRICTED`. + /// - Note: This flag may only be changed by the superuser. + @_alwaysEmitIntoClient + public static var restricted: FileFlags { FileFlags(rawValue: _SF_RESTRICTED) } + /// File is a firmlink. /// /// Firmlinks are used by macOS to create transparent links between diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift index 14ae30ea..9a099476 100644 --- a/Sources/System/FileSystem/FileMode.swift +++ b/Sources/System/FileSystem/FileMode.swift @@ -44,7 +44,7 @@ public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// The file's permissions, from the mode's permission bits. /// - /// Setting this property will mask the `newValue` with the permissions bit mask `0o7777`. + /// Setting this property will mask the `newValue` with the permissions bit mask `ALLPERMS`. @_alwaysEmitIntoClient public var permissions: FilePermissions { get { FilePermissions(rawValue: rawValue & _MODE_PERMISSIONS_MASK) } diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift index 91718880..638b1472 100644 --- a/Sources/System/FileSystem/FileType.swift +++ b/Sources/System/FileSystem/FileType.swift @@ -18,7 +18,7 @@ // | characterSpecial | S_IFCHR | // | blockSpecial | S_IFBLK | // | regular | S_IFREG | -// | pipe | S_IFIFO | +// | fifo | S_IFIFO | // | symbolicLink | S_IFLNK | // | socket | S_IFSOCK | // |------------------|---------------------| @@ -41,11 +41,17 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public var rawValue: CInterop.Mode - /// Creates a strongly-typed file type from the raw C value. + /// Creates a strongly-typed file type from the raw C `mode_t`. /// - /// - Note: `rawValue` should only contain the mode's file-type bits. Otherwise, - /// use `FileMode(rawValue:)` to get a strongly-typed `FileMode`, then - /// call `.type` to get the properly masked `FileType`. + /// - Note: This initializer stores the `rawValue` directly and **does not** + /// mask the value with `S_IFMT`. If the supplied `rawValue` contains bits + /// outside of the `S_IFMT` mask, the resulting `FileType` will not compare + /// equal to constants like `.directory` and `.symbolicLink`, which may + /// be unexpected. + /// + /// If you're unsure whether the `mode_t` contains bits outside of `S_IFMT`, + /// you can use `FileMode(rawValue:)` instead to get a strongly-typed + /// `FileMode`, then call `.type` to get the properly masked `FileType`. @_alwaysEmitIntoClient public init(rawValue: CInterop.Mode) { self.rawValue = rawValue } @@ -73,11 +79,11 @@ public struct FileType: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public static var regular: FileType { FileType(rawValue: _S_IFREG) } - /// FIFO (or pipe) + /// FIFO (or named pipe) /// /// The corresponding C constant is `S_IFIFO`. @_alwaysEmitIntoClient - public static var pipe: FileType { FileType(rawValue: _S_IFIFO) } + public static var fifo: FileType { FileType(rawValue: _S_IFIFO) } /// Symbolic link /// diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift index df9a01ae..7620b601 100644 --- a/Sources/System/FileSystem/Identifiers.swift +++ b/Sources/System/FileSystem/Identifiers.swift @@ -19,9 +19,13 @@ public struct UserID: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public var rawValue: CInterop.UserID - /// Creates a strongly-typed `GroupID` from the raw C value. + /// Creates a strongly-typed `UserID` from the raw C value. @_alwaysEmitIntoClient public init(rawValue: CInterop.UserID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `UserID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.UserID) { self.rawValue = rawValue } } /// A Swift wrapper of the C `gid_t` type. @@ -36,6 +40,10 @@ public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { /// Creates a strongly-typed `GroupID` from the raw C value. @_alwaysEmitIntoClient public init(rawValue: CInterop.GroupID) { self.rawValue = rawValue } + + /// Creates a strongly-typed `GroupID` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.GroupID) { self.rawValue = rawValue } } /// A Swift wrapper of the C `dev_t` type. @@ -51,26 +59,31 @@ public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { @_alwaysEmitIntoClient public init(rawValue: CInterop.DeviceID) { self.rawValue = rawValue } - - /// Creates a `DeviceID` from the given major and minor device numbers. - /// - /// The corresponding C function is `makedev()`. + /// Creates a strongly-typed `DeviceID` from the raw C value. @_alwaysEmitIntoClient - public static func make(major: CUnsignedInt, minor: CUnsignedInt) -> DeviceID { - DeviceID(rawValue: system_makedev(major, minor)) - } + public init(_ rawValue: CInterop.DeviceID) { self.rawValue = rawValue } - /// The major device number - /// - /// The corresponding C function is `major()`. - @_alwaysEmitIntoClient - public var major: CInt { system_major(rawValue) } + // TODO: API review for ID wrapper functionality - /// The minor device number - /// - /// The corresponding C function is `minor()`. - @_alwaysEmitIntoClient - public var minor: CInt { system_minor(rawValue) } +// /// Creates a `DeviceID` from the given major and minor device numbers. +// /// +// /// The corresponding C function is `makedev()`. +// @_alwaysEmitIntoClient +// private static func make(major: CUnsignedInt, minor: CUnsignedInt) -> DeviceID { +// DeviceID(rawValue: system_makedev(major, minor)) +// } +// +// /// The major device number +// /// +// /// The corresponding C function is `major()`. +// @_alwaysEmitIntoClient +// private var major: CInt { system_major(rawValue) } +// +// /// The minor device number +// /// +// /// The corresponding C function is `minor()`. +// @_alwaysEmitIntoClient +// private var minor: CInt { system_minor(rawValue) } } /// A Swift wrapper of the C `ino_t` type. @@ -85,5 +98,9 @@ public struct Inode: RawRepresentable, Sendable, Hashable, Codable { /// Creates a strongly-typed `Inode` from the raw C value. @_alwaysEmitIntoClient public init(rawValue: CInterop.Inode) { self.rawValue = rawValue } + + /// Creates a strongly-typed `Inode` from the raw C value. + @_alwaysEmitIntoClient + public init(_ rawValue: CInterop.Inode) { self.rawValue = rawValue } } #endif // !os(Windows) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 9f2e5503..6bc78ab3 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -122,7 +122,7 @@ public struct Stat: RawRepresentable, Sendable { }.get() } - /// Creates a `Stat` struct from an`UnsafePointer` path. + /// Creates a `Stat` struct from an `UnsafePointer` path. /// /// `followTargetSymlink` determines the behavior if `path` ends with a symbolic link. /// By default, `followTargetSymlink` is `true` and this initializer behaves like `stat()`. @@ -314,6 +314,9 @@ public struct Stat: RawRepresentable, Sendable { } /// File type for the given mode + /// + /// - Note: This property is equivalent to `mode.type`. Modifying this + /// property will update the underlying `st_mode` accordingly. @_alwaysEmitIntoClient public var type: FileType { get { mode.type } @@ -325,6 +328,9 @@ public struct Stat: RawRepresentable, Sendable { } /// File permissions for the given mode + /// + /// - Note: This property is equivalent to `mode.permissions`. Modifying + /// this property will update the underlying `st_mode` accordingly. @_alwaysEmitIntoClient public var permissions: FilePermissions { get { mode.permissions } @@ -365,7 +371,7 @@ public struct Stat: RawRepresentable, Sendable { /// Device ID (if special file) /// /// For character or block special files, the returned `DeviceID` may have - /// meaningful `.major` and `.minor` values. For non-special files, this + /// meaningful major and minor values. For non-special files, this /// property is usually meaningless and often set to 0. /// /// The corresponding C property is `st_rdev`. @@ -377,6 +383,12 @@ public struct Stat: RawRepresentable, Sendable { /// Total size, in bytes /// + /// The semantics of this property are tied to the underlying C `st_size` field, + /// which can have file system-dependent behavior. For example, this property + /// can return different values for a file's data fork and resource fork, and some + /// file systems report logical size rather than actual disk usage for compressed + /// or cloned files. + /// /// The corresponding C property is `st_size`. @_alwaysEmitIntoClient public var size: Int64 { @@ -395,6 +407,9 @@ public struct Stat: RawRepresentable, Sendable { /// Number of 512-byte blocks allocated /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file system-dependent behavior. + /// /// The corresponding C property is `st_blocks`. @_alwaysEmitIntoClient public var blocksAllocated: Int64 { @@ -404,12 +419,19 @@ public struct Stat: RawRepresentable, Sendable { /// Total size allocated, in bytes /// + /// The semantics of this property are tied to the underlying C `st_blocks` field, + /// which can have file system-dependent behavior. + /// /// - Note: Calculated as `512 * blocksAllocated`. @_alwaysEmitIntoClient public var sizeAllocated: Int64 { 512 * blocksAllocated } + // NOTE: "st_" property names are used for the `timespec` properties so + // we can reserve `accessTime`, `modificationTime`, etc. for potential + // `UTCClock.Instant` properties in the future. + /// Time of last access, given as a C `timespec` since the Epoch. /// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). @@ -497,7 +519,7 @@ public struct Stat: RawRepresentable, Sendable { } #endif - // TODO: jflat - Change time properties to UTCClock.Instant when possible. + // TODO: Investigate changing time properties to UTCClock.Instant once available. // /// Time of last access, given as a `UTCClock.Instant` // /// @@ -602,8 +624,6 @@ extension Stat: Hashable { // MARK: - CustomStringConvertible and CustomDebugStringConvertible -// TODO: jflat - // MARK: - FileDescriptor Extensions // @available(System X.Y.Z, *) @@ -673,7 +693,7 @@ extension FilePath { } } - /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`. + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`. /// /// If `path` is relative, it is resolved against the current working directory. /// @@ -690,7 +710,7 @@ extension FilePath { ).get() } - /// Creates a `Stat` struct for the file referenced by this`FilePath` using the given `Flags`, + /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`, /// including a `FileDescriptor` to resolve a relative path. /// /// If `path` is absolute (starts with a forward slash), then `fd` is ignored. diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 37b7da33..c4fda718 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -732,18 +732,9 @@ internal var _SF_APPEND: CInterop.FileFlags { UInt32(bitPattern: SF_APPEND) } @_alwaysEmitIntoClient internal var _UF_OPAQUE: CInterop.FileFlags { UInt32(bitPattern: UF_OPAQUE) } -@_alwaysEmitIntoClient -internal var _UF_COMPRESSED: CInterop.FileFlags { UInt32(bitPattern: UF_COMPRESSED) } - -@_alwaysEmitIntoClient -internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } - @_alwaysEmitIntoClient internal var _UF_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } -@_alwaysEmitIntoClient -internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } - @_alwaysEmitIntoClient internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) } #endif @@ -751,9 +742,18 @@ internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) // MARK: Flags Available on Darwin Only #if SYSTEM_PACKAGE_DARWIN +@_alwaysEmitIntoClient +internal var _UF_COMPRESSED: CInterop.FileFlags { UInt32(bitPattern: UF_COMPRESSED) } + +@_alwaysEmitIntoClient +internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } + @_alwaysEmitIntoClient internal var _UF_DATAVAULT: CInterop.FileFlags { UInt32(bitPattern: UF_DATAVAULT) } +@_alwaysEmitIntoClient +internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } + @_alwaysEmitIntoClient internal var _SF_FIRMLINK: CInterop.FileFlags { UInt32(bitPattern: SF_FIRMLINK) } diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift index bc302a71..ca010335 100644 --- a/Tests/SystemTests/FileModeTests.swift +++ b/Tests/SystemTests/FileModeTests.swift @@ -74,7 +74,7 @@ private struct FileModeTests { #expect(invalidMode.type != .characterSpecial) #expect(invalidMode.type != .blockSpecial) #expect(invalidMode.type != .regular) - #expect(invalidMode.type != .pipe) + #expect(invalidMode.type != .fifo) #expect(invalidMode.type != .symbolicLink) #expect(invalidMode.type != .socket) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @@ -87,7 +87,7 @@ private struct FileModeTests { #expect(invalidMode.type != .characterSpecial) #expect(invalidMode.type != .blockSpecial) #expect(invalidMode.type != .regular) - #expect(invalidMode.type != .pipe) + #expect(invalidMode.type != .fifo) #expect(invalidMode.type != .symbolicLink) #expect(invalidMode.type != .socket) #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) From 98712f67f23f3b411e81dba13d42ba3355956872 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 6 Oct 2025 13:18:14 -0600 Subject: [PATCH 160/172] Remove _GNU_SOURCE define and AT_EMPTY_PATH for now --- Package.swift | 1 - Sources/System/FileSystem/Stat.swift | 19 +++++++++-------- Sources/System/Internals/Constants.swift | 9 ++++---- Tests/SystemTests/StatTests.swift | 26 +++++++++++------------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 8aba3315..11a43f61 100644 --- a/Package.swift +++ b/Package.swift @@ -87,7 +87,6 @@ let swiftSettings = swiftSettingsAvailability + swiftSettingsCI + [ let cSettings: [CSetting] = [ .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), - .define("_GNU_SOURCE", .when(platforms: [.linux])), ] #if SYSTEM_ABI_STABLE diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 6bc78ab3..31243e68 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -86,15 +86,16 @@ public struct Stat: RawRepresentable, Sendable { public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } #endif - #if os(FreeBSD) || os(Linux) || os(Android) - /// If the path is an empty string (or `NULL` since Linux 6.11), - /// return information about the given file descriptor. - /// - /// The corresponding C constant is `AT_EMPTY_PATH`. - /// - Note: Only available on FreeBSD, Linux, and Android. - @_alwaysEmitIntoClient - public static var emptyPath: Flags { Flags(rawValue: _AT_EMPTY_PATH) } - #endif + // TODO: Re-enable when _GNU_SOURCE can be defined. +// #if os(FreeBSD) || os(Linux) || os(Android) +// /// If the path is an empty string (or `NULL` since Linux 6.11), +// /// return information about the given file descriptor. +// /// +// /// The corresponding C constant is `AT_EMPTY_PATH`. +// /// - Note: Only available on FreeBSD, Linux, and Android. +// @_alwaysEmitIntoClient +// public static var emptyPath: Flags { Flags(rawValue: _AT_EMPTY_PATH) } +// #endif } // MARK: Initializers diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index c4fda718..3d4b7efd 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -663,10 +663,11 @@ internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } #endif -#if os(FreeBSD) || os(Linux) || os(Android) -@_alwaysEmitIntoClient -internal var _AT_EMPTY_PATH: CInt { AT_EMPTY_PATH } -#endif +// TODO: Re-enable when _GNU_SOURCE can be defined. +//#if os(FreeBSD) || os(Linux) || os(Android) +//@_alwaysEmitIntoClient +//internal var _AT_EMPTY_PATH: CInt { AT_EMPTY_PATH } +//#endif // MARK: - File Mode / File Type diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index 58c34413..4fefe7cf 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -81,7 +81,6 @@ private struct StatTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func followSymlinkInits() async throws { try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in let targetFilePath = tempDir.appending("target.txt") @@ -98,17 +97,15 @@ private struct StatTests { } } - #if !os(WASI) // Can't open an fd to a symlink on WASI (no O_PATH) + // Can't open an fd to a symlink on WASI (no O_PATH) + // On non-Darwin, we need O_PATH | O_NOFOLLOW to open the symlink + // directly, but O_PATH requires _GNU_SOURCE be defined (TODO). #if SYSTEM_PACKAGE_DARWIN let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: .symlink) - #else - // Need O_PATH | O_NOFOLLOW to open the symlink directly - let symlinkFD = try FileDescriptor.open(symlinkPath, .readOnly, options: [.path, .noFollow]) - #endif defer { try? symlinkFD.close() } - #endif // !os(WASI) + #endif let targetStat = try targetFilePath.stat() let originalTargetAccessTime = targetStat.st_atim @@ -130,7 +127,7 @@ private struct StatTests { stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) - #if !os(WASI) + #if SYSTEM_PACKAGE_DARWIN stat = try symlinkFD.stat() stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) @@ -142,7 +139,7 @@ private struct StatTests { stat.st_atim = originalTargetAccessTime #expect(stat == targetStat) - #if !os(WASI) + #if SYSTEM_PACKAGE_DARWIN stat = try Stat(symlinkFD) stat.st_atim = originalSymlinkAccessTime #expect(stat == symlinkStat) @@ -400,11 +397,12 @@ private struct StatTests { } -#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) -private extension FileDescriptor.OpenOptions { - static var path: Self { Self(rawValue: O_PATH) } -} -#endif +// TODO: Re-enable for testing when _GNU_SOURCE can be defined. +//#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) +//private extension FileDescriptor.OpenOptions { +// static var path: Self { Self(rawValue: O_PATH) } +//} +//#endif // Comparison operators for timespec until UTCClock.Instant properties are available private func >= (lhs: timespec, rhs: timespec) -> Bool { From 67a2fd2ca1c5035036993e1baa9165897a4cbae2 Mon Sep 17 00:00:00 2001 From: Melissa Kilby Date: Fri, 17 Oct 2025 16:28:56 -0700 Subject: [PATCH 161/172] chore: restrict GitHub workflow permissions - future-proof Signed-off-by: Melissa Kilby --- .github/workflows/pull_request.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cd2bef9e..49f6dc9b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,5 +1,8 @@ name: Pull request +permissions: + contents: read + on: pull_request: types: [opened, reopened, synchronize] From 501bb7d052f5b41a6bbb0dad1db611f5b527cafc Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 28 Oct 2025 22:30:47 -0600 Subject: [PATCH 162/172] Remove commented-out code for future directions --- Sources/System/FileSystem/Identifiers.swift | 22 ------- Sources/System/FileSystem/Stat.swift | 64 --------------------- Sources/System/Internals/Exports.swift | 13 ----- 3 files changed, 99 deletions(-) diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift index 7620b601..ca2835ea 100644 --- a/Sources/System/FileSystem/Identifiers.swift +++ b/Sources/System/FileSystem/Identifiers.swift @@ -62,28 +62,6 @@ public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { /// Creates a strongly-typed `DeviceID` from the raw C value. @_alwaysEmitIntoClient public init(_ rawValue: CInterop.DeviceID) { self.rawValue = rawValue } - - // TODO: API review for ID wrapper functionality - -// /// Creates a `DeviceID` from the given major and minor device numbers. -// /// -// /// The corresponding C function is `makedev()`. -// @_alwaysEmitIntoClient -// private static func make(major: CUnsignedInt, minor: CUnsignedInt) -> DeviceID { -// DeviceID(rawValue: system_makedev(major, minor)) -// } -// -// /// The major device number -// /// -// /// The corresponding C function is `major()`. -// @_alwaysEmitIntoClient -// private var major: CInt { system_major(rawValue) } -// -// /// The minor device number -// /// -// /// The corresponding C function is `minor()`. -// @_alwaysEmitIntoClient -// private var minor: CInt { system_minor(rawValue) } } /// A Swift wrapper of the C `ino_t` type. diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 31243e68..f31fa5d7 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -85,17 +85,6 @@ public struct Stat: RawRepresentable, Sendable { @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } #endif - - // TODO: Re-enable when _GNU_SOURCE can be defined. -// #if os(FreeBSD) || os(Linux) || os(Android) -// /// If the path is an empty string (or `NULL` since Linux 6.11), -// /// return information about the given file descriptor. -// /// -// /// The corresponding C constant is `AT_EMPTY_PATH`. -// /// - Note: Only available on FreeBSD, Linux, and Android. -// @_alwaysEmitIntoClient -// public static var emptyPath: Flags { Flags(rawValue: _AT_EMPTY_PATH) } -// #endif } // MARK: Initializers @@ -520,59 +509,6 @@ public struct Stat: RawRepresentable, Sendable { } #endif - // TODO: Investigate changing time properties to UTCClock.Instant once available. - -// /// Time of last access, given as a `UTCClock.Instant` -// /// -// /// The corresponding C property is `st_atim` (or `st_atimespec` on Darwin). -// public var accessTime: UTCClock.Instant { -// get { -// UTCClock.systemEpoch.advanced(by: Duration(st_atim)) -// } -// set { -// st_atim = timespec(UTCClock.systemEpoch.duration(to: newValue)) -// } -// } -// -// /// Time of last modification, given as a `UTCClock.Instant` -// /// -// /// The corresponding C property is `st_mtim` (or `st_mtimespec` on Darwin). -// public var modificationTime: UTCClock.Instant { -// get { -// UTCClock.systemEpoch.advanced(by: Duration(st_mtim)) -// } -// set { -// st_mtim = timespec(UTCClock.systemEpoch.duration(to: newValue)) -// } -// } -// -// /// Time of last status (inode) change, given as a `UTCClock.Instant` -// /// -// /// The corresponding C property is `st_ctim` (or `st_ctimespec` on Darwin). -// public var changeTime: UTCClock.Instant { -// get { -// UTCClock.systemEpoch.advanced(by: Duration(st_ctim)) -// } -// set { -// st_ctim = timespec(UTCClock.systemEpoch.duration(to: newValue)) -// } -// } -// -// #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) -// /// Time of file creation, given as a `UTCClock.Instant` -// /// -// /// The corresponding C property is `st_birthtim` (or `st_birthtimespec` on Darwin). -// /// - Note: Only available on Darwin and FreeBSD. -// public var creationTime: UTCClock.Instant { -// get { -// UTCClock.systemEpoch.advanced(by: Duration(st_birthtim)) -// } -// set { -// st_birthtim = timespec(UTCClock.systemEpoch.duration(to: newValue)) -// } -// } -// #endif - #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) /// File flags /// diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index 025aefae..58d0db80 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -103,19 +103,6 @@ internal func system_fstat(_ fd: CInt, _ s: inout CInterop.Stat) -> Int32 { internal func system_fstatat(_ fd: CInt, _ p: UnsafePointer, _ s: inout CInterop.Stat, _ flags: CInt) -> Int32 { fstatat(fd, p, &s, flags) } - -@usableFromInline -internal func system_major(_ dev: CInterop.DeviceID) -> CInt { - numericCast((dev >> 24) & 0xff) -} -@usableFromInline -internal func system_minor(_ dev: CInterop.DeviceID) -> CInt { - numericCast(dev & 0xffffff) -} -@usableFromInline -internal func system_makedev(_ maj: CUnsignedInt, _ min: CUnsignedInt) -> CInterop.DeviceID { - CInterop.DeviceID((maj << 24) | min) -} #endif // Convention: `system_platform_foo` is a From 90dba1dc68343627ddd80a22dd7a9ed56cd7f72d Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 28 Oct 2025 22:31:34 -0600 Subject: [PATCH 163/172] AT_SYMLINK_FOLLOW -> AT_SYMLINK_NOFOLLOW --- Sources/System/Internals/Constants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 3d4b7efd..1d2030cf 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -651,7 +651,7 @@ internal var _AT_FDCWD: CInt { AT_FDCWD } // MARK: - fstatat Flags @_alwaysEmitIntoClient -internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_FOLLOW } +internal var _AT_SYMLINK_NOFOLLOW: CInt { AT_SYMLINK_NOFOLLOW } #if SYSTEM_PACKAGE_DARWIN @_alwaysEmitIntoClient From eee01456718b82983443be29523fca641827f9a2 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 28 Oct 2025 22:52:34 -0600 Subject: [PATCH 164/172] Standardize on "file system" --- Sources/System/Errno.swift | 2 +- Sources/System/FileSystem/Stat.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/System/Errno.swift b/Sources/System/Errno.swift index 43b46af5..94eb102d 100644 --- a/Sources/System/Errno.swift +++ b/Sources/System/Errno.swift @@ -961,7 +961,7 @@ public struct Errno: RawRepresentable, Error, Hashable, Codable { /// Stale NFS file handle. /// - /// You attempted access an open file on an NFS filesystem, + /// You attempted access an open file on an NFS file system, /// which is now unavailable as referenced by the given file descriptor. /// This may indicate that the file was deleted on the NFS server /// or that some other catastrophic event occurred. diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index f31fa5d7..e1d221bf 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -374,7 +374,7 @@ public struct Stat: RawRepresentable, Sendable { /// Total size, in bytes /// /// The semantics of this property are tied to the underlying C `st_size` field, - /// which can have file system-dependent behavior. For example, this property + /// which can have file-system–dependent behavior. For example, this property /// can return different values for a file's data fork and resource fork, and some /// file systems report logical size rather than actual disk usage for compressed /// or cloned files. @@ -386,7 +386,7 @@ public struct Stat: RawRepresentable, Sendable { set { rawValue.st_size = numericCast(newValue) } } - /// Block size for filesystem I/O, in bytes + /// Block size for file system I/O, in bytes /// /// The corresponding C property is `st_blksize`. @_alwaysEmitIntoClient @@ -398,7 +398,7 @@ public struct Stat: RawRepresentable, Sendable { /// Number of 512-byte blocks allocated /// /// The semantics of this property are tied to the underlying C `st_blocks` field, - /// which can have file system-dependent behavior. + /// which can have file-system–dependent behavior. /// /// The corresponding C property is `st_blocks`. @_alwaysEmitIntoClient @@ -410,7 +410,7 @@ public struct Stat: RawRepresentable, Sendable { /// Total size allocated, in bytes /// /// The semantics of this property are tied to the underlying C `st_blocks` field, - /// which can have file system-dependent behavior. + /// which can have file-system–dependent behavior. /// /// - Note: Calculated as `512 * blocksAllocated`. @_alwaysEmitIntoClient From 40e1deb663e499309739adb6a2acf193ebd7aa0e Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 29 Oct 2025 11:47:32 -0600 Subject: [PATCH 165/172] Consolidate internal Stat functions --- Sources/System/FileSystem/Stat.swift | 88 ++++++---------------------- 1 file changed, 17 insertions(+), 71 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index e1d221bf..f8bdb602 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -103,7 +103,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try path.withPlatformString { + self = try path.withPlatformString { Self._stat( $0, followTargetSymlink: followTargetSymlink, @@ -126,7 +126,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._stat( + self = try Self._stat( path, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt @@ -138,7 +138,7 @@ public struct Stat: RawRepresentable, Sendable { _ ptr: UnsafePointer, followTargetSymlink: Bool, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { if followTargetSymlink { @@ -146,7 +146,7 @@ public struct Stat: RawRepresentable, Sendable { } else { system_lstat(ptr, &result) } - }.map { result } + }.map { Stat(rawValue: result) } } /// Creates a `Stat` struct from a `FileDescriptor`. @@ -157,7 +157,7 @@ public struct Stat: RawRepresentable, Sendable { _ fd: FileDescriptor, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._fstat( + self = try Self._fstat( fd, retryOnInterrupt: retryOnInterrupt ).get() @@ -167,11 +167,11 @@ public struct Stat: RawRepresentable, Sendable { internal static func _fstat( _ fd: FileDescriptor, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstat(fd.rawValue, &result) - }.map { result } + }.map { Stat(rawValue: result) } } /// Creates a `Stat` struct from a `FilePath` and `Flags`. @@ -185,7 +185,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try path.withPlatformString { + self = try path.withPlatformString { Self._fstatat( $0, relativeTo: _AT_FDCWD, @@ -209,7 +209,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try path.withPlatformString { + self = try path.withPlatformString { Self._fstatat( $0, relativeTo: fd.rawValue, @@ -230,7 +230,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._fstatat( + self = try Self._fstatat( path, relativeTo: _AT_FDCWD, flags: flags, @@ -252,7 +252,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self.rawValue = try Self._fstatat( + self = try Self._fstatat( path, relativeTo: fd.rawValue, flags: flags, @@ -266,11 +266,11 @@ public struct Stat: RawRepresentable, Sendable { relativeTo fd: FileDescriptor.RawValue, flags: Stat.Flags, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstatat(fd, path, &result, flags.rawValue) - }.map { result } + }.map { Stat(rawValue: result) } } @@ -573,19 +573,7 @@ extension FileDescriptor { public func stat( retryOnInterrupt: Bool = true ) throws(Errno) -> Stat { - try _fstat( - retryOnInterrupt: retryOnInterrupt - ).get() - } - - @usableFromInline - internal func _fstat( - retryOnInterrupt: Bool - ) -> Result { - var result = CInterop.Stat() - return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_fstat(self.rawValue, &result) - }.map { Stat(rawValue: result) } + try Stat(self, retryOnInterrupt: retryOnInterrupt) } } @@ -607,27 +595,7 @@ extension FilePath { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) -> Stat { - try _stat( - followTargetSymlink: followTargetSymlink, - retryOnInterrupt: retryOnInterrupt - ).get() - } - - @usableFromInline - internal func _stat( - followTargetSymlink: Bool, - retryOnInterrupt: Bool - ) -> Result { - var result = CInterop.Stat() - return withPlatformString { ptr in - nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - if followTargetSymlink { - system_stat(ptr, &result) - } else { - system_lstat(ptr, &result) - } - }.map { Stat(rawValue: result) } - } + try Stat(self, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt) } /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`. @@ -640,11 +608,7 @@ extension FilePath { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) -> Stat { - try _fstatat( - relativeTo: _AT_FDCWD, - flags: flags, - retryOnInterrupt: retryOnInterrupt - ).get() + try Stat(self, flags: flags, retryOnInterrupt: retryOnInterrupt) } /// Creates a `Stat` struct for the file referenced by this `FilePath` using the given `Flags`, @@ -660,25 +624,7 @@ extension FilePath { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) -> Stat { - try _fstatat( - relativeTo: fd.rawValue, - flags: flags, - retryOnInterrupt: retryOnInterrupt - ).get() - } - - @usableFromInline - internal func _fstatat( - relativeTo fd: FileDescriptor.RawValue, - flags: Stat.Flags, - retryOnInterrupt: Bool - ) -> Result { - var result = CInterop.Stat() - return withPlatformString { ptr in - nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_fstatat(fd, ptr, &result, flags.rawValue) - }.map { Stat(rawValue: result) } - } + try Stat(self, relativeTo: fd, flags: flags, retryOnInterrupt: retryOnInterrupt) } } From 4911642226096fed703dee029786abda0ef28ba5 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 29 Oct 2025 11:57:30 -0600 Subject: [PATCH 166/172] Add availability, fix CInterop availability issue on old Darwin platforms --- Package.swift | 2 + Sources/System/FileSystem/FileFlags.swift | 4 +- Sources/System/FileSystem/FileMode.swift | 2 +- Sources/System/FileSystem/FileType.swift | 2 +- Sources/System/FileSystem/Identifiers.swift | 8 +-- Sources/System/FileSystem/Stat.swift | 9 +-- Sources/System/Internals/Constants.swift | 64 ++++++++++----------- Tests/SystemTests/FileModeTests.swift | 2 + Tests/SystemTests/StatTests.swift | 11 ++-- 9 files changed, 56 insertions(+), 48 deletions(-) diff --git a/Package.swift b/Package.swift index 11a43f61..c7621c05 100644 --- a/Package.swift +++ b/Package.swift @@ -64,6 +64,8 @@ let availability: [Available] = [ Available("1.5.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), Available("1.6.0", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), Available("1.6.1", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), + + Available("99", "macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999"), ] let swiftSettingsAvailability = availability.map(\.swiftSetting) diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift index fd714cb8..8fb54938 100644 --- a/Sources/System/FileSystem/FileFlags.swift +++ b/Sources/System/FileSystem/FileFlags.swift @@ -39,7 +39,7 @@ // |------------------|---------------|---------------|---------------| #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) -// @available(System X.Y.Z, *) +@available(System 99, *) extension CInterop { public typealias FileFlags = UInt32 } @@ -49,7 +49,7 @@ extension CInterop { /// /// - Note: Only available on Darwin, FreeBSD, and OpenBSD. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct FileFlags: OptionSet, Sendable, Hashable, Codable { /// The raw C flags. diff --git a/Sources/System/FileSystem/FileMode.swift b/Sources/System/FileSystem/FileMode.swift index 9a099476..91329f1a 100644 --- a/Sources/System/FileSystem/FileMode.swift +++ b/Sources/System/FileSystem/FileMode.swift @@ -14,7 +14,7 @@ /// /// - Note: Only available on Unix-like platforms. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct FileMode: RawRepresentable, Sendable, Hashable, Codable { /// The raw C mode. diff --git a/Sources/System/FileSystem/FileType.swift b/Sources/System/FileSystem/FileType.swift index 638b1472..42134522 100644 --- a/Sources/System/FileSystem/FileType.swift +++ b/Sources/System/FileSystem/FileType.swift @@ -34,7 +34,7 @@ /// /// - Note: Only available on Unix-like platforms. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct FileType: RawRepresentable, Sendable, Hashable, Codable { /// The raw file-type bits from the C mode. diff --git a/Sources/System/FileSystem/Identifiers.swift b/Sources/System/FileSystem/Identifiers.swift index ca2835ea..b8f90141 100644 --- a/Sources/System/FileSystem/Identifiers.swift +++ b/Sources/System/FileSystem/Identifiers.swift @@ -12,7 +12,7 @@ #if !os(Windows) /// A Swift wrapper of the C `uid_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct UserID: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `uid_t`. @@ -30,7 +30,7 @@ public struct UserID: RawRepresentable, Sendable, Hashable, Codable { /// A Swift wrapper of the C `gid_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `gid_t`. @@ -48,7 +48,7 @@ public struct GroupID: RawRepresentable, Sendable, Hashable, Codable { /// A Swift wrapper of the C `dev_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `dev_t`. @@ -66,7 +66,7 @@ public struct DeviceID: RawRepresentable, Sendable, Hashable, Codable { /// A Swift wrapper of the C `ino_t` type. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct Inode: RawRepresentable, Sendable, Hashable, Codable { /// The raw C `ino_t`. diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index f8bdb602..98b7dc19 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -35,7 +35,7 @@ import Android /// /// - Note: Only available on Unix-like platforms. @frozen -// @available(System X.Y.Z, *) +@available(System 99, *) public struct Stat: RawRepresentable, Sendable { /// The raw C `stat` struct. @@ -82,7 +82,6 @@ public struct Stat: RawRepresentable, Sendable { /// The corresponding C constant is `AT_RESOLVE_BENEATH`. /// - Note: Only available on Darwin and FreeBSD. @_alwaysEmitIntoClient - @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *) public static var resolveBeneath: Flags { Flags(rawValue: _AT_RESOLVE_BENEATH) } #endif } @@ -537,6 +536,7 @@ public struct Stat: RawRepresentable, Sendable { // MARK: - Equatable and Hashable +@available(System 99, *) extension Stat: Equatable { @_alwaysEmitIntoClient /// Compares the raw bytes of two `Stat` structs for equality. @@ -549,6 +549,7 @@ extension Stat: Equatable { } } +@available(System 99, *) extension Stat: Hashable { @_alwaysEmitIntoClient /// Hashes the raw bytes of this `Stat` struct. @@ -563,7 +564,7 @@ extension Stat: Hashable { // MARK: - FileDescriptor Extensions -// @available(System X.Y.Z, *) +@available(System 99, *) extension FileDescriptor { /// Creates a `Stat` struct for the file referenced by this `FileDescriptor`. @@ -579,7 +580,7 @@ extension FileDescriptor { // MARK: - FilePath Extensions -// @available(System X.Y.Z, *) +@available(System 99, *) extension FilePath { /// Creates a `Stat` struct for the file referenced by this `FilePath`. diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 1d2030cf..8740d81e 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -672,35 +672,35 @@ internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } // MARK: - File Mode / File Type @_alwaysEmitIntoClient -internal var _MODE_FILETYPE_MASK: CInterop.Mode { S_IFMT } +internal var _MODE_FILETYPE_MASK: mode_t { S_IFMT } @_alwaysEmitIntoClient -internal var _MODE_PERMISSIONS_MASK: CInterop.Mode { 0o7777 } +internal var _MODE_PERMISSIONS_MASK: mode_t { 0o7777 } @_alwaysEmitIntoClient -internal var _S_IFDIR: CInterop.Mode { S_IFDIR } +internal var _S_IFDIR: mode_t { S_IFDIR } @_alwaysEmitIntoClient -internal var _S_IFCHR: CInterop.Mode { S_IFCHR } +internal var _S_IFCHR: mode_t { S_IFCHR } @_alwaysEmitIntoClient -internal var _S_IFBLK: CInterop.Mode { S_IFBLK } +internal var _S_IFBLK: mode_t { S_IFBLK } @_alwaysEmitIntoClient -internal var _S_IFREG: CInterop.Mode { S_IFREG } +internal var _S_IFREG: mode_t { S_IFREG } @_alwaysEmitIntoClient -internal var _S_IFIFO: CInterop.Mode { S_IFIFO } +internal var _S_IFIFO: mode_t { S_IFIFO } @_alwaysEmitIntoClient -internal var _S_IFLNK: CInterop.Mode { S_IFLNK } +internal var _S_IFLNK: mode_t { S_IFLNK } @_alwaysEmitIntoClient -internal var _S_IFSOCK: CInterop.Mode { S_IFSOCK } +internal var _S_IFSOCK: mode_t { S_IFSOCK } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient -internal var _S_IFWHT: CInterop.Mode { S_IFWHT } +internal var _S_IFWHT: mode_t { S_IFWHT } #endif // MARK: - stat/chflags File Flags @@ -709,82 +709,82 @@ internal var _S_IFWHT: CInterop.Mode { S_IFWHT } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) @_alwaysEmitIntoClient -internal var _UF_NODUMP: CInterop.FileFlags { UInt32(bitPattern: UF_NODUMP) } +internal var _UF_NODUMP: UInt32 { UInt32(bitPattern: UF_NODUMP) } @_alwaysEmitIntoClient -internal var _UF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: UF_IMMUTABLE) } +internal var _UF_IMMUTABLE: UInt32 { UInt32(bitPattern: UF_IMMUTABLE) } @_alwaysEmitIntoClient -internal var _UF_APPEND: CInterop.FileFlags { UInt32(bitPattern: UF_APPEND) } +internal var _UF_APPEND: UInt32 { UInt32(bitPattern: UF_APPEND) } @_alwaysEmitIntoClient -internal var _SF_ARCHIVED: CInterop.FileFlags { UInt32(bitPattern: SF_ARCHIVED) } +internal var _SF_ARCHIVED: UInt32 { UInt32(bitPattern: SF_ARCHIVED) } @_alwaysEmitIntoClient -internal var _SF_IMMUTABLE: CInterop.FileFlags { UInt32(bitPattern: SF_IMMUTABLE) } +internal var _SF_IMMUTABLE: UInt32 { UInt32(bitPattern: SF_IMMUTABLE) } @_alwaysEmitIntoClient -internal var _SF_APPEND: CInterop.FileFlags { UInt32(bitPattern: SF_APPEND) } +internal var _SF_APPEND: UInt32 { UInt32(bitPattern: SF_APPEND) } #endif // MARK: Flags Available on Darwin and FreeBSD #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) @_alwaysEmitIntoClient -internal var _UF_OPAQUE: CInterop.FileFlags { UInt32(bitPattern: UF_OPAQUE) } +internal var _UF_OPAQUE: UInt32 { UInt32(bitPattern: UF_OPAQUE) } @_alwaysEmitIntoClient -internal var _UF_HIDDEN: CInterop.FileFlags { UInt32(bitPattern: UF_HIDDEN) } +internal var _UF_HIDDEN: UInt32 { UInt32(bitPattern: UF_HIDDEN) } @_alwaysEmitIntoClient -internal var _SF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: SF_NOUNLINK) } +internal var _SF_NOUNLINK: UInt32 { UInt32(bitPattern: SF_NOUNLINK) } #endif // MARK: Flags Available on Darwin Only #if SYSTEM_PACKAGE_DARWIN @_alwaysEmitIntoClient -internal var _UF_COMPRESSED: CInterop.FileFlags { UInt32(bitPattern: UF_COMPRESSED) } +internal var _UF_COMPRESSED: UInt32 { UInt32(bitPattern: UF_COMPRESSED) } @_alwaysEmitIntoClient -internal var _UF_TRACKED: CInterop.FileFlags { UInt32(bitPattern: UF_TRACKED) } +internal var _UF_TRACKED: UInt32 { UInt32(bitPattern: UF_TRACKED) } @_alwaysEmitIntoClient -internal var _UF_DATAVAULT: CInterop.FileFlags { UInt32(bitPattern: UF_DATAVAULT) } +internal var _UF_DATAVAULT: UInt32 { UInt32(bitPattern: UF_DATAVAULT) } @_alwaysEmitIntoClient -internal var _SF_RESTRICTED: CInterop.FileFlags { UInt32(bitPattern: SF_RESTRICTED) } +internal var _SF_RESTRICTED: UInt32 { UInt32(bitPattern: SF_RESTRICTED) } @_alwaysEmitIntoClient -internal var _SF_FIRMLINK: CInterop.FileFlags { UInt32(bitPattern: SF_FIRMLINK) } +internal var _SF_FIRMLINK: UInt32 { UInt32(bitPattern: SF_FIRMLINK) } @_alwaysEmitIntoClient -internal var _SF_DATALESS: CInterop.FileFlags { UInt32(bitPattern: SF_DATALESS) } +internal var _SF_DATALESS: UInt32 { UInt32(bitPattern: SF_DATALESS) } #endif // MARK: Flags Available on FreeBSD Only #if os(FreeBSD) @_alwaysEmitIntoClient -internal var _UF_NOUNLINK: CInterop.FileFlags { UInt32(bitPattern: UF_NOUNLINK) } +internal var _UF_NOUNLINK: UInt32 { UInt32(bitPattern: UF_NOUNLINK) } @_alwaysEmitIntoClient -internal var _UF_OFFLINE: CInterop.FileFlags { UInt32(bitPattern: UF_OFFLINE) } +internal var _UF_OFFLINE: UInt32 { UInt32(bitPattern: UF_OFFLINE) } @_alwaysEmitIntoClient -internal var _UF_READONLY: CInterop.FileFlags { UInt32(bitPattern: UF_READONLY) } +internal var _UF_READONLY: UInt32 { UInt32(bitPattern: UF_READONLY) } @_alwaysEmitIntoClient -internal var _UF_REPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_REPARSE) } +internal var _UF_REPARSE: UInt32 { UInt32(bitPattern: UF_REPARSE) } @_alwaysEmitIntoClient -internal var _UF_SPARSE: CInterop.FileFlags { UInt32(bitPattern: UF_SPARSE) } +internal var _UF_SPARSE: UInt32 { UInt32(bitPattern: UF_SPARSE) } @_alwaysEmitIntoClient -internal var _UF_SYSTEM: CInterop.FileFlags { UInt32(bitPattern: UF_SYSTEM) } +internal var _UF_SYSTEM: UInt32 { UInt32(bitPattern: UF_SYSTEM) } @_alwaysEmitIntoClient -internal var _SF_SNAPSHOT: CInterop.FileFlags { UInt32(bitPattern: SF_SNAPSHOT) } +internal var _SF_SNAPSHOT: UInt32 { UInt32(bitPattern: SF_SNAPSHOT) } #endif #endif // !os(Windows) diff --git a/Tests/SystemTests/FileModeTests.swift b/Tests/SystemTests/FileModeTests.swift index ca010335..46c3a5d6 100644 --- a/Tests/SystemTests/FileModeTests.swift +++ b/Tests/SystemTests/FileModeTests.swift @@ -36,6 +36,7 @@ import Android @Suite("FileMode") private struct FileModeTests { + @available(System 99, *) @Test func basics() async throws { var mode = FileMode(rawValue: S_IFREG | 0o644) // Regular file, rw-r--r-- #expect(mode.type == .regular) @@ -66,6 +67,7 @@ private struct FileModeTests { #expect(mode.type == mode2.type) } + @available(System 99, *) @Test func invalidInput() async throws { // No permissions, all other bits set var invalidMode = FileMode(rawValue: ~0o7777) diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index 4fefe7cf..299dd8ee 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -39,6 +39,7 @@ import Android @Suite("Stat") private struct StatTests { + @available(System 99, *) @Test func basics() async throws { try withTemporaryFilePath(basename: "Stat_basics") { tempDir in let dirStatFromFilePath = try tempDir.stat() @@ -80,8 +81,8 @@ private struct StatTests { } } - @Test - func followSymlinkInits() async throws { + @available(System 99, *) + @Test func followSymlinkInits() async throws { try withTemporaryFilePath(basename: "Stat_followSymlinkInits") { tempDir in let targetFilePath = tempDir.appending("target.txt") let symlinkPath = tempDir.appending("symlink") @@ -189,6 +190,7 @@ private struct StatTests { } } + @available(System 99, *) @Test func permissions() async throws { try withTemporaryFilePath(basename: "Stat_permissions") { tempDir in let testFile = tempDir.appending("test.txt") @@ -218,8 +220,8 @@ private struct StatTests { } } - @Test - func times() async throws { + @available(System 99, *) + @Test func times() async throws { var start = timespec() try #require(clock_gettime(CLOCK_REALTIME, &start) == 0, "\(Errno.current)") start.tv_sec -= 1 // A little wiggle room @@ -347,6 +349,7 @@ private struct StatTests { } #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + @available(System 99, *) @Test func flags() async throws { try withTemporaryFilePath(basename: "Stat_flags") { tempDir in let filePath = tempDir.appending("test.txt") From c2e385e849608d12afdbeeba2d113a74b72eb6bd Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Wed, 29 Oct 2025 12:01:13 -0600 Subject: [PATCH 167/172] Remove commented-out code pt. 2 --- Sources/System/Internals/Constants.swift | 6 ------ Tests/SystemTests/StatTests.swift | 7 ------- 2 files changed, 13 deletions(-) diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 8740d81e..3e71ec90 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -663,12 +663,6 @@ internal var _AT_SYMLINK_NOFOLLOW_ANY: CInt { AT_SYMLINK_NOFOLLOW_ANY } internal var _AT_RESOLVE_BENEATH: CInt { AT_RESOLVE_BENEATH } #endif -// TODO: Re-enable when _GNU_SOURCE can be defined. -//#if os(FreeBSD) || os(Linux) || os(Android) -//@_alwaysEmitIntoClient -//internal var _AT_EMPTY_PATH: CInt { AT_EMPTY_PATH } -//#endif - // MARK: - File Mode / File Type @_alwaysEmitIntoClient diff --git a/Tests/SystemTests/StatTests.swift b/Tests/SystemTests/StatTests.swift index 299dd8ee..524226d2 100644 --- a/Tests/SystemTests/StatTests.swift +++ b/Tests/SystemTests/StatTests.swift @@ -400,13 +400,6 @@ private struct StatTests { } -// TODO: Re-enable for testing when _GNU_SOURCE can be defined. -//#if !SYSTEM_PACKAGE_DARWIN && !os(WASI) -//private extension FileDescriptor.OpenOptions { -// static var path: Self { Self(rawValue: O_PATH) } -//} -//#endif - // Comparison operators for timespec until UTCClock.Instant properties are available private func >= (lhs: timespec, rhs: timespec) -> Bool { (lhs.tv_sec, lhs.tv_nsec) >= (rhs.tv_sec, rhs.tv_nsec) From 87f8e49ba79c68598b36cbe5f04f762bbcd32f74 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Thu, 30 Oct 2025 14:11:45 -0600 Subject: [PATCH 168/172] Assign rawValue instead of self --- Sources/System/FileSystem/Stat.swift | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index 98b7dc19..c335cf80 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -102,7 +102,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try path.withPlatformString { + self.rawValue = try path.withPlatformString { Self._stat( $0, followTargetSymlink: followTargetSymlink, @@ -125,7 +125,7 @@ public struct Stat: RawRepresentable, Sendable { followTargetSymlink: Bool = true, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._stat( + self.rawValue = try Self._stat( path, followTargetSymlink: followTargetSymlink, retryOnInterrupt: retryOnInterrupt @@ -137,7 +137,7 @@ public struct Stat: RawRepresentable, Sendable { _ ptr: UnsafePointer, followTargetSymlink: Bool, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { if followTargetSymlink { @@ -145,7 +145,7 @@ public struct Stat: RawRepresentable, Sendable { } else { system_lstat(ptr, &result) } - }.map { Stat(rawValue: result) } + }.map { result } } /// Creates a `Stat` struct from a `FileDescriptor`. @@ -156,7 +156,7 @@ public struct Stat: RawRepresentable, Sendable { _ fd: FileDescriptor, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._fstat( + self.rawValue = try Self._fstat( fd, retryOnInterrupt: retryOnInterrupt ).get() @@ -166,11 +166,11 @@ public struct Stat: RawRepresentable, Sendable { internal static func _fstat( _ fd: FileDescriptor, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstat(fd.rawValue, &result) - }.map { Stat(rawValue: result) } + }.map { result } } /// Creates a `Stat` struct from a `FilePath` and `Flags`. @@ -184,7 +184,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try path.withPlatformString { + self.rawValue = try path.withPlatformString { Self._fstatat( $0, relativeTo: _AT_FDCWD, @@ -208,7 +208,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try path.withPlatformString { + self.rawValue = try path.withPlatformString { Self._fstatat( $0, relativeTo: fd.rawValue, @@ -229,7 +229,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._fstatat( + self.rawValue = try Self._fstatat( path, relativeTo: _AT_FDCWD, flags: flags, @@ -251,7 +251,7 @@ public struct Stat: RawRepresentable, Sendable { flags: Stat.Flags, retryOnInterrupt: Bool = true ) throws(Errno) { - self = try Self._fstatat( + self.rawValue = try Self._fstatat( path, relativeTo: fd.rawValue, flags: flags, @@ -265,11 +265,11 @@ public struct Stat: RawRepresentable, Sendable { relativeTo fd: FileDescriptor.RawValue, flags: Stat.Flags, retryOnInterrupt: Bool - ) -> Result { + ) -> Result { var result = CInterop.Stat() return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_fstatat(fd, path, &result, flags.rawValue) - }.map { Stat(rawValue: result) } + }.map { result } } From 55350cd06dd5cc9e67ffa2373128d1dcf6151dd2 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Sun, 2 Nov 2025 22:31:37 -0700 Subject: [PATCH 169/172] Add availability to new CInterop typealiases --- Sources/System/FileSystem/FileFlags.swift | 4 ---- Sources/System/Internals/CInterop.swift | 8 +++++++- Sources/System/Internals/Exports.swift | 4 ++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/System/FileSystem/FileFlags.swift b/Sources/System/FileSystem/FileFlags.swift index 8fb54938..5a905ed4 100644 --- a/Sources/System/FileSystem/FileFlags.swift +++ b/Sources/System/FileSystem/FileFlags.swift @@ -39,10 +39,6 @@ // |------------------|---------------|---------------|---------------| #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) -@available(System 99, *) -extension CInterop { - public typealias FileFlags = UInt32 -} /// File-specific flags found in the `st_flags` property of a `stat` struct /// or used as input to `chflags()`. diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 7a35b09c..46406631 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -78,12 +78,18 @@ public enum CInterop { /// on API. public typealias PlatformUnicodeEncoding = UTF8 #endif +} - #if !os(Windows) +#if !os(Windows) +@available(System 99, *) +extension CInterop { public typealias Stat = stat public typealias DeviceID = dev_t public typealias Inode = ino_t public typealias UserID = uid_t public typealias GroupID = gid_t + #if SYSTEM_PACKAGE_DARWIN || os(FreeBSD) || os(OpenBSD) + public typealias FileFlags = UInt32 #endif } +#endif diff --git a/Sources/System/Internals/Exports.swift b/Sources/System/Internals/Exports.swift index 58d0db80..15ee45c3 100644 --- a/Sources/System/Internals/Exports.swift +++ b/Sources/System/Internals/Exports.swift @@ -91,15 +91,19 @@ internal func system_strlen(_ s: UnsafeMutablePointer) -> Int { } #if !os(Windows) +@available(System 99, *) internal func system_stat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { stat(p, &s) } +@available(System 99, *) internal func system_lstat(_ p: UnsafePointer, _ s: inout CInterop.Stat) -> Int32 { lstat(p, &s) } +@available(System 99, *) internal func system_fstat(_ fd: CInt, _ s: inout CInterop.Stat) -> Int32 { fstat(fd, &s) } +@available(System 99, *) internal func system_fstatat(_ fd: CInt, _ p: UnsafePointer, _ s: inout CInterop.Stat, _ flags: CInt) -> Int32 { fstatat(fd, p, &s, flags) } From e133abb3e0dab21627b98bbc8512abc86d53efb0 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 3 Nov 2025 18:05:50 -0700 Subject: [PATCH 170/172] Fix Stat build on Android --- Sources/System/FileSystem/Stat.swift | 16 ++++++++-------- Sources/System/Internals/CInterop.swift | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/System/FileSystem/Stat.swift b/Sources/System/FileSystem/Stat.swift index c335cf80..e30e2b16 100644 --- a/Sources/System/FileSystem/Stat.swift +++ b/Sources/System/FileSystem/Stat.swift @@ -280,8 +280,8 @@ public struct Stat: RawRepresentable, Sendable { /// The corresponding C property is `st_dev`. @_alwaysEmitIntoClient public var deviceID: DeviceID { - get { DeviceID(rawValue: rawValue.st_dev) } - set { rawValue.st_dev = newValue.rawValue } + get { DeviceID(rawValue: numericCast(rawValue.st_dev)) } + set { rawValue.st_dev = numericCast(newValue.rawValue) } } /// Inode number @@ -289,8 +289,8 @@ public struct Stat: RawRepresentable, Sendable { /// The corresponding C property is `st_ino`. @_alwaysEmitIntoClient public var inode: Inode { - get { Inode(rawValue: rawValue.st_ino) } - set { rawValue.st_ino = newValue.rawValue } + get { Inode(rawValue: numericCast(rawValue.st_ino)) } + set { rawValue.st_ino = numericCast(newValue.rawValue) } } /// File mode @@ -298,8 +298,8 @@ public struct Stat: RawRepresentable, Sendable { /// The corresponding C property is `st_mode`. @_alwaysEmitIntoClient public var mode: FileMode { - get { FileMode(rawValue: rawValue.st_mode) } - set { rawValue.st_mode = newValue.rawValue } + get { FileMode(rawValue: numericCast(rawValue.st_mode)) } + set { rawValue.st_mode = numericCast(newValue.rawValue) } } /// File type for the given mode @@ -366,8 +366,8 @@ public struct Stat: RawRepresentable, Sendable { /// The corresponding C property is `st_rdev`. @_alwaysEmitIntoClient public var specialDeviceID: DeviceID { - get { DeviceID(rawValue: rawValue.st_rdev) } - set { rawValue.st_rdev = newValue.rawValue } + get { DeviceID(rawValue: numericCast(rawValue.st_rdev)) } + set { rawValue.st_rdev = numericCast(newValue.rawValue) } } /// Total size, in bytes diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 46406631..7f85b9e7 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -21,7 +21,7 @@ import Musl #elseif canImport(WASILibc) import WASILibc #elseif canImport(Bionic) -@_implementationOnly import CSystem +import CSystem import Bionic #else #error("Unsupported Platform") From b252c123a7c07422d9b5b509b6722ed8daf94bf8 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 3 Nov 2025 18:11:02 -0700 Subject: [PATCH 171/172] Enable Android SDK build --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 49f6dc9b..5d5a3d2c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -45,6 +45,7 @@ jobs: {"os_version": "focal", "swift_version": "6.2"}, {"os_version": "focal", "swift_version": "nightly-main"}, ] + enable_android_sdk_build: true build-abi-stable: name: Build ABI Stable From f9a2b9443b27437a5ba9869f861ce5494990e46f Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Mon, 3 Nov 2025 18:29:15 -0700 Subject: [PATCH 172/172] Exclude focal for Android build --- .github/workflows/pull_request.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5d5a3d2c..1b78b37d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -46,6 +46,10 @@ jobs: {"os_version": "focal", "swift_version": "nightly-main"}, ] enable_android_sdk_build: true + android_exclude_swift_versions: | + [ + {"os_version": "focal", "swift_version": "nightly-main"}, + ] build-abi-stable: name: Build ABI Stable