diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 626f178..bb3d211 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,6 @@ jobs: # Disable strict concurrency checking as it intersects badly with # warnings-as-errors on 5.10 and later as SwiftPMs generated test manifest # has a non-sendable global property. - linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" # TODO: Enable warnings-as-errors on 6.0. linux_6_0_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_6_1_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1eb9cf9..6469f09 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,7 +18,6 @@ jobs: # Disable strict concurrency checking as it intersects badly with # warnings-as-errors on 5.10 and later as SwiftPMs generated test manifest # has a non-sendable global property. - linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" # TODO: Enable warnings-as-errors on 6.0. linux_6_0_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" linux_6_1_arguments_override: "-Xswiftc -strict-concurrency=complete --explicit-target-dependency-import-check error" diff --git a/Package.swift b/Package.swift index e89aba6..eb6c9b3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project diff --git a/Sources/OpenAPIURLSession/BufferedStream/BufferedStream.swift b/Sources/OpenAPIURLSession/BufferedStream/BufferedStream.swift index 60b5dca..6b30d21 100644 --- a/Sources/OpenAPIURLSession/BufferedStream/BufferedStream.swift +++ b/Sources/OpenAPIURLSession/BufferedStream/BufferedStream.swift @@ -130,7 +130,7 @@ import DequeModule /// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline -internal struct BufferedStream { +internal struct BufferedStream { @usableFromInline final class _Backing: Sendable { @usableFromInline diff --git a/Sources/OpenAPIURLSession/BufferedStream/Lock.swift b/Sources/OpenAPIURLSession/BufferedStream/Lock.swift index b114d46..589a84c 100644 --- a/Sources/OpenAPIURLSession/BufferedStream/Lock.swift +++ b/Sources/OpenAPIURLSession/BufferedStream/Lock.swift @@ -28,12 +28,22 @@ #if canImport(Darwin) import Darwin -#elseif canImport(Android) -import Android -#elseif canImport(Glibc) -import Glibc #elseif os(Windows) +import ucrt import WinSDK +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(Bionic) +@preconcurrency import Bionic +#elseif canImport(WASILibc) +@preconcurrency import WASILibc +#if canImport(wasi_pthread) +import wasi_pthread +#endif +#else +#error("The concurrency Lock module was unable to identify your C library.") #endif #if os(Windows) @@ -44,60 +54,74 @@ typealias LockPrimitive = SRWLOCK typealias LockPrimitive = pthread_mutex_t #endif + +/// A utility function that runs the body code only in debug builds, without +/// emitting compiler warnings. +/// +/// This is currently the only way to do this in Swift: see +/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion. +@inlinable +internal func debugOnly(_ body: () -> Void) { + assert({ body(); return true }()) +} + @usableFromInline -enum LockOperations {} +enum LockOperations: Sendable {} extension LockOperations { - @inlinable - static func create(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - InitializeSRWLock(mutex) - #else - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - - let err = pthread_mutex_init(mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func destroy(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - // SRWLOCK does not need to be freed - #else - let err = pthread_mutex_destroy(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func lock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - AcquireSRWLockExclusive(mutex) - #else - let err = pthread_mutex_lock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func unlock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - ReleaseSRWLockExclusive(mutex) - #else - let err = pthread_mutex_unlock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } + @inlinable + static func create(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + InitializeSRWLock(mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + debugOnly { + pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) + } + + let err = pthread_mutex_init(mutex, &attr) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func destroy(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + // SRWLOCK does not need to be free'd + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_destroy(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func lock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + AcquireSRWLockExclusive(mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_lock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func unlock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + ReleaseSRWLockExclusive(mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_unlock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } } // Tail allocate both the mutex and a generic value using ManagedBuffer. @@ -131,66 +155,71 @@ extension LockOperations { @usableFromInline final class LockStorage: ManagedBuffer { - @inlinable - static func create(value: Value) -> Self { - let buffer = Self.create(minimumCapacity: 1) { _ in - return value + @inlinable + static func create(value: Value) -> Self { + let buffer = Self.create(minimumCapacity: 1) { _ in + value + } + // Intentionally using a force cast here to avoid a miss compiliation in 5.10. + // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer + // can eliminate the upcast/downcast pair + let storage = buffer as! Self + + storage.withUnsafeMutablePointers { _, lockPtr in + LockOperations.create(lockPtr) + } + + return storage } - // Avoid 'unsafeDowncast' as there is a miscompilation on 5.10. - let storage = buffer as! Self - storage.withUnsafeMutablePointers { _, lockPtr in - LockOperations.create(lockPtr) + @inlinable + func lock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.lock(lockPtr) + } } - return storage - } - - @inlinable - func lock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.lock(lockPtr) + @inlinable + func unlock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.unlock(lockPtr) + } } - } - @inlinable - func unlock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.unlock(lockPtr) + @inlinable + deinit { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.destroy(lockPtr) + } } - } - @usableFromInline - deinit { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.destroy(lockPtr) - } - } - - @inlinable - func withLockPrimitive( - _ body: (UnsafeMutablePointer) throws -> T - ) rethrows -> T { - try self.withUnsafeMutablePointerToElements { lockPtr in - return try body(lockPtr) + @inlinable + func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointerToElements { lockPtr in + try body(lockPtr) + } } - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointers { valuePtr, lockPtr in - LockOperations.lock(lockPtr) - defer { LockOperations.unlock(lockPtr) } - return try mutate(&valuePtr.pointee) + + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointers { valuePtr, lockPtr in + LockOperations.lock(lockPtr) + defer { LockOperations.unlock(lockPtr) } + return try mutate(&valuePtr.pointee) + } } - } } -extension LockStorage: @unchecked Sendable {} +// This compiler guard is here becaue `ManagedBuffer` is already declaring +// Sendable unavailability after 6.1, which `LockStorage` inherits. +#if compiler(<6.2) +@available(*, unavailable) +extension LockStorage: Sendable {} +#endif /// A threading lock based on `libpthread` instead of `libdispatch`. /// -/// - note: ``Lock`` has reference semantics. +/// - Note: ``Lock`` has reference semantics. /// /// This object provides a lock on top of a single `pthread_mutex_t`. This kind /// of lock is safe to use with `libpthread`-based threading models, such as the @@ -198,83 +227,140 @@ extension LockStorage: @unchecked Sendable {} /// `SRWLOCK` type. @usableFromInline struct Lock { - @usableFromInline - internal let _storage: LockStorage - - /// Create a new lock. - @usableFromInline - init() { - self._storage = .create(value: ()) - } - - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - @inlinable - func lock() { - self._storage.lock() - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - @inlinable - func unlock() { - self._storage.unlock() - } - - @inlinable - internal func withLockPrimitive( - _ body: (UnsafeMutablePointer) throws -> T - ) rethrows -> T { - return try self._storage.withLockPrimitive(body) - } + @usableFromInline + internal let _storage: LockStorage + + /// Create a new lock. + @inlinable + init() { + self._storage = .create(value: ()) + } + + /// Acquire the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `unlock`, to simplify lock handling. + @inlinable + func lock() { + self._storage.lock() + } + + /// Release the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `lock`, to simplify lock handling. + @inlinable + func unlock() { + self._storage.unlock() + } + + @inlinable + internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { + try self._storage.withLockPrimitive(body) + } } extension Lock { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + @inlinable + func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } + + @inlinable + func withLockVoid(_ body: () throws -> Void) rethrows { + try self.withLock(body) } - return try body() - } } -extension Lock: Sendable {} +extension Lock: @unchecked Sendable {} extension UnsafeMutablePointer { - @inlinable - func assertValidAlignment() { - assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) - } + @inlinable + func assertValidAlignment() { + assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) + } } +/// Provides locked access to `Value`. +/// +/// - Note: ``LockedValueBox`` has reference semantics and holds the `Value` +/// alongside a lock behind a reference. +/// +/// This is no different than creating a ``Lock`` and protecting all +/// accesses to a value using the lock. But it's easy to forget to actually +/// acquire/release the lock in the correct place. ``LockedValueBox`` makes +/// that much easier. @usableFromInline struct LockedValueBox { - @usableFromInline - let storage: LockStorage - - @usableFromInline - init(_ value: Value) { - self.storage = .create(value: value) - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - return try self.storage.withLockedValue(mutate) - } + + @usableFromInline + internal let _storage: LockStorage + + /// Initialize the `Value`. + @inlinable + init(_ value: Value) { + self._storage = .create(value: value) + } + + /// Access the `Value`, allowing mutation of it. + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + try self._storage.withLockedValue(mutate) + } + + /// Provides an unsafe view over the lock and its value. + /// + /// This can be beneficial when you require fine grained control over the lock in some + /// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by + /// switching to ``NIOLock``. + var unsafe: Unsafe { + Unsafe(_storage: self._storage) + } + + /// Provides an unsafe view over the lock and its value. + struct Unsafe { + @usableFromInline + let _storage: LockStorage + + /// Manually acquire the lock. + @inlinable + func lock() { + self._storage.lock() + } + + /// Manually release the lock. + @inlinable + func unlock() { + self._storage.unlock() + } + + /// Mutate the value, assuming the lock has been acquired manually. + /// + /// - Parameter mutate: A closure with scoped access to the value. + /// - Returns: The result of the `mutate` closure. + @inlinable + func withValueAssumingLockIsAcquired( + _ mutate: (_ value: inout Value) throws -> Result + ) rethrows -> Result { + try self._storage.withUnsafeMutablePointerToHeader { value in + try mutate(&value.pointee) + } + } + } } -extension LockedValueBox: Sendable where Value: Sendable {} +extension LockedValueBox: @unchecked Sendable where Value: Sendable {} + +extension LockedValueBox.Unsafe: @unchecked Sendable where Value: Sendable {} diff --git a/Sources/OpenAPIURLSession/URLSessionTransport.swift b/Sources/OpenAPIURLSession/URLSessionTransport.swift index 5b8b269..65dd92e 100644 --- a/Sources/OpenAPIURLSession/URLSessionTransport.swift +++ b/Sources/OpenAPIURLSession/URLSessionTransport.swift @@ -291,12 +291,12 @@ extension URLSessionTransportError: CustomStringConvertible { } } -private let _debugLoggingEnabled = LockStorage.create(value: false) +private let _debugLoggingEnabled = LockedValueBox(false) var debugLoggingEnabled: Bool { get { _debugLoggingEnabled.withLockedValue { $0 } } set { _debugLoggingEnabled.withLockedValue { $0 = newValue } } } -private let _standardErrorLock = LockStorage.create(value: FileHandle.standardError) +private let _standardErrorLock = LockedValueBox(FileHandle.standardError) func debug(_ message: @autoclosure () -> String, function: String = #function, file: String = #file, line: UInt = #line) { assert( diff --git a/Tests/OpenAPIURLSessionTests/NIOAsyncHTTP1TestServer.swift b/Tests/OpenAPIURLSessionTests/NIOAsyncHTTP1TestServer.swift index aad5f3d..772a6b6 100644 --- a/Tests/OpenAPIURLSessionTests/NIOAsyncHTTP1TestServer.swift +++ b/Tests/OpenAPIURLSessionTests/NIOAsyncHTTP1TestServer.swift @@ -19,8 +19,8 @@ import NIOHTTP1 final class AsyncTestHTTP1Server { - typealias ConnectionHandler = @Sendable (NIOAsyncChannel) - async throws -> Void + typealias ConnectionHandler = + @Sendable (NIOAsyncChannel) async throws -> Void /// Use `start(host:port:connectionHandler:)` instead. private init() {}