diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3deea2c..d012eb6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,7 +71,7 @@ jobs: linux_swift_6_1: runs-on: ubuntu-latest - container: swift:6.1 + container: swift:6.1.2 steps: - name: Checkout uses: actions/checkout@v4 @@ -123,7 +123,7 @@ jobs: linux_swift_6_1_musl: runs-on: ubuntu-latest - container: swift:6.1 + container: swift:6.1.2 steps: - name: Checkout uses: actions/checkout@v4 @@ -146,7 +146,7 @@ jobs: - name: Install Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.1.0" + swift-version: "6.1.2" - name: Version run: swift --version - name: Build diff --git a/Sources/AllocatedLock.swift b/Sources/AllocatedLock.swift index 2f09569..124d848 100644 --- a/Sources/AllocatedLock.swift +++ b/Sources/AllocatedLock.swift @@ -30,6 +30,7 @@ // // Backports the Swift interface around OSAllocatedUnfairLock available in recent Darwin platforms +@available(*, deprecated, message: "Unused by Mutex and will be removed in future versions.") public struct AllocatedLock: @unchecked Sendable { @usableFromInline diff --git a/Sources/Mutex.swift b/Sources/Mutex.swift index 8bdaaa2..bfa8372 100644 --- a/Sources/Mutex.swift +++ b/Sources/Mutex.swift @@ -29,82 +29,181 @@ // SOFTWARE. // +#if compiler(>=6) + +#if !canImport(WinSDK) + // Backports the Swift 6 type Mutex to all Darwin platforms -// @available(macOS, deprecated: 15.0, message: "use Mutex from Synchronization module included with Swift 6") -// @available(iOS, deprecated: 18.0, message: "use Mutex from Synchronization module included with Swift 6") -// @available(tvOS, deprecated: 18.0, message: "use Mutex from Synchronization module included with Swift 6") -// @available(watchOS, deprecated: 11.0, message: "use Mutex from Synchronization module included with Swift 6") -// @available(visionOS, deprecated: 2.0, message: "use Mutex from Synchronization module included with Swift 6") -public struct Mutex: Sendable { - let lock: AllocatedLock // Compatible with OSAllocatedUnfairLock iOS 16+ +@available(macOS, introduced: 13.0, deprecated: 15.0, message: "use Mutex from Synchronization module") +@available(iOS, introduced: 16.0, deprecated: 18.0, message: "use Mutex from Synchronization module") +@available(tvOS, introduced: 18.0, deprecated: 15.0, message: "use Mutex from Synchronization module") +@available(watchOS, introduced: 11.0, deprecated: 15.0, message: "use Mutex from Synchronization module") +@available(visionOS, introduced: 2.0, deprecated: 15.0, message: "use Mutex from Synchronization module") +public struct Mutex: @unchecked Sendable, ~Copyable { + let storage: Storage + + public init(_ initialValue: consuming sending Value) { + self.storage = Storage(initialValue) + } + + public borrowing func withLock( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result { + storage.lock() + defer { storage.unlock() } + return try body(&storage.value) + } + + public borrowing func withLockIfAvailable( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result? { + guard storage.tryLock() else { return nil } + defer { storage.unlock() } + return try body(&storage.value) + } } -#if compiler(>=6) -public extension Mutex { - init(_ initialValue: consuming sending Value) { - self.lock = AllocatedLock(uncheckedState: initialValue) +#else + +// Windows doesn't support ~Copyable yet + +public struct Mutex: @unchecked Sendable { + let storage: Storage + + public init(_ initialValue: consuming sending Value) { + self.storage = Storage(initialValue) } - borrowing func withLock( + public borrowing func withLock( _ body: (inout sending Value) throws(E) -> sending Result ) throws(E) -> sending Result { - do { - return try lock.withLockUnchecked { value in - nonisolated(unsafe) var copy = value - defer { value = copy } - return try Transferring(body(©)) - }.value - } catch let error as E { - throw error - } catch { - preconditionFailure("cannot occur") - } - } - - borrowing func withLockIfAvailable( + storage.lock() + defer { storage.unlock() } + return try body(&storage.value) + } + + public borrowing func withLockIfAvailable( _ body: (inout sending Value) throws(E) -> sending Result - ) throws(E) -> sending Result? where E: Error { - do { - return try lock.withLockIfAvailableUnchecked { value in - nonisolated(unsafe) var copy = value - defer { value = copy } - return try Transferring(body(©)) - }?.value - } catch let error as E { - throw error - } catch { - preconditionFailure("cannot occur") - } + ) throws(E) -> sending Result? { + guard storage.tryLock() else { return nil } + defer { storage.unlock() } + return try body(&storage.value) } } -private struct Transferring { - nonisolated(unsafe) var value: T - init(_ value: T) { - self.value = value +#endif + +#if canImport(Darwin) + +import struct os.os_unfair_lock_t +import struct os.os_unfair_lock +import func os.os_unfair_lock_lock +import func os.os_unfair_lock_unlock +import func os.os_unfair_lock_trylock + +final class Storage { + private let _lock: os_unfair_lock_t + var value: Value + + init(_ initialValue: consuming Value) { + self._lock = .allocate(capacity: 1) + self._lock.initialize(to: os_unfair_lock()) + self.value = initialValue + } + + func lock() { + os_unfair_lock_lock(_lock) + } + + func unlock() { + os_unfair_lock_unlock(_lock) + } + + func tryLock() -> Bool { + os_unfair_lock_trylock(_lock) + } + + deinit { + self._lock.deinitialize(count: 1) + self._lock.deallocate() } } + +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + +#if canImport(Musl) +import Musl +#elseif canImport(Bionic) +import Android #else -public extension Mutex { +import Glibc +#endif + +final class Storage { + private let _lock: UnsafeMutablePointer + + var value: Value + init(_ initialValue: consuming Value) { - self.lock = AllocatedLock(uncheckedState: initialValue) + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + self._lock = .allocate(capacity: 1) + let err = pthread_mutex_init(self._lock, &attr) + precondition(err == 0, "pthread_mutex_init error: \(err)") + self.value = initialValue + } + + func lock() { + let err = pthread_mutex_lock(_lock) + precondition(err == 0, "pthread_mutex_lock error: \(err)") } - borrowing func withLock( - _ body: (inout Value) throws -> Result - ) rethrows -> Result { - try lock.withLockUnchecked { - return try body(&$0) - } + func unlock() { + let err = pthread_mutex_unlock(_lock) + precondition(err == 0, "pthread_mutex_unlock error: \(err)") } - borrowing func withLockIfAvailable( - _ body: (inout Value) throws -> Result - ) rethrows -> Result? { - try lock.withLockIfAvailableUnchecked { - return try body(&$0) - } + func tryLock() -> Bool { + pthread_mutex_trylock(_lock) == 0 + } + + deinit { + let err = pthread_mutex_destroy(self._lock) + precondition(err == 0, "pthread_mutex_destroy error: \(err)") + self._lock.deallocate() } } + +#elseif canImport(WinSDK) + +import ucrt +import WinSDK + +final class Storage { + private let _lock: UnsafeMutablePointer + + var value: Value + + init(_ initialValue: Value) { + self._lock = .allocate(capacity: 1) + InitializeSRWLock(self._lock) + self.value = initialValue + } + + func lock() { + AcquireSRWLockExclusive(_lock) + } + + func unlock() { + ReleaseSRWLockExclusive(_lock) + } + + func tryLock() -> Bool { + TryAcquireSRWLockExclusive(_lock) != 0 + } +} + +#endif + #endif diff --git a/Sources/MutexSwift5.swift b/Sources/MutexSwift5.swift new file mode 100644 index 0000000..65632c5 --- /dev/null +++ b/Sources/MutexSwift5.swift @@ -0,0 +1,169 @@ +// +// MutexSwift5.swift +// swift-mutex +// +// Created by Simon Whitty on 03/06/2025. +// Copyright 2025 Simon Whitty +// +// Distributed under the permissive MIT license +// Get the latest version from here: +// +// https://github.com/swhitty/swift-mutex +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#if compiler(<6.0) + +// Backports the Swift 6 type Mutex to Swift 5 + +public struct Mutex: @unchecked Sendable { + let storage: Storage + + public init(_ initialValue: Value) { + self.storage = Storage(initialValue) + } + + public borrowing func withLock( + _ body: (inout Value) throws -> Result + ) rethrows -> Result { + storage.lock() + defer { storage.unlock() } + return try body(&storage.value) + } + + public borrowing func withLockIfAvailable( + _ body: (inout Value) throws -> Result + ) rethrows -> Result? { + guard storage.tryLock() else { return nil } + defer { storage.unlock() } + return try body(&storage.value) + } +} + +#if canImport(Darwin) + +import struct os.os_unfair_lock_t +import struct os.os_unfair_lock +import func os.os_unfair_lock_lock +import func os.os_unfair_lock_unlock +import func os.os_unfair_lock_trylock + +final class Storage { + private let _lock: os_unfair_lock_t + var value: Value + + init(_ initialValue: consuming Value) { + self._lock = .allocate(capacity: 1) + self._lock.initialize(to: os_unfair_lock()) + self.value = initialValue + } + + func lock() { + os_unfair_lock_lock(_lock) + } + + func unlock() { + os_unfair_lock_unlock(_lock) + } + + func tryLock() -> Bool { + os_unfair_lock_trylock(_lock) + } + + deinit { + self._lock.deinitialize(count: 1) + self._lock.deallocate() + } +} +#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) + +#if canImport(Musl) +import Musl +#elseif canImport(Bionic) +import Android +#else +import Glibc +#endif + +final class Storage { + private let _lock: UnsafeMutablePointer + var value: Value + + init(_ initialValue: consuming Value) { + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + self._lock = .allocate(capacity: 1) + let err = pthread_mutex_init(self._lock, &attr) + precondition(err == 0, "pthread_mutex_init error: \(err)") + self.value = initialValue + } + + func lock() { + let err = pthread_mutex_lock(_lock) + precondition(err == 0, "pthread_mutex_lock error: \(err)") + } + + func unlock() { + let err = pthread_mutex_unlock(_lock) + precondition(err == 0, "pthread_mutex_unlock error: \(err)") + } + + func tryLock() -> Bool { + pthread_mutex_trylock(_lock) == 0 + } + + deinit { + let err = pthread_mutex_destroy(self._lock) + precondition(err == 0, "pthread_mutex_destroy error: \(err)") + self._lock.deallocate() + } +} +#elseif canImport(WinSDK) + +import ucrt +import WinSDK + +final class Storage { + private let _lock: UnsafeMutablePointer + + var value: Value + + init(_ initialValue: Value) { + self._lock = .allocate(capacity: 1) + InitializeSRWLock(self._lock) + self.value = initialValue + } + + func lock() { + AcquireSRWLockExclusive(_lock) + } + + func unlock() { + ReleaseSRWLockExclusive(_lock) + } + + func tryLock() -> Bool { + TryAcquireSRWLockExclusive(_lock) != 0 + } +} + +#endif + +#endif diff --git a/Tests/MutexTests.swift b/Tests/MutexTests.swift index 68cde13..4b2f4fb 100644 --- a/Tests/MutexTests.swift +++ b/Tests/MutexTests.swift @@ -55,11 +55,11 @@ struct MutexTests { @Test func lockIfAvailable_ReturnsValue() { let mutex = Mutex("fish") - mutex.lock.unsafeLock() + mutex.unsafeLock() #expect( mutex.withLockIfAvailable { _ in "chips" } == nil ) - mutex.lock.unsafeUnlock() + mutex.unsafeUnlock() #expect( mutex.withLockIfAvailable { _ in "chips" } == "chips" ) @@ -73,4 +73,9 @@ struct MutexTests { } } } + +extension Mutex { + func unsafeLock() { storage.lock() } + func unsafeUnlock() { storage.unlock() } +} #endif diff --git a/Tests/MutexXCTests.swift b/Tests/MutexXCTests.swift index 534422b..5f31d33 100644 --- a/Tests/MutexXCTests.swift +++ b/Tests/MutexXCTests.swift @@ -52,11 +52,11 @@ final class MutexTests: XCTestCase { func testLockIfAvailable_ReturnsValue() { let mutex = Mutex("fish") - mutex.lock.unsafeLock() + mutex.unsafeLock() XCTAssertNil( mutex.withLockIfAvailable { _ in "chips" } ) - mutex.lock.unsafeUnlock() + mutex.unsafeUnlock() XCTAssertEqual( mutex.withLockIfAvailable { _ in "chips" }, "chips" @@ -70,4 +70,9 @@ final class MutexTests: XCTestCase { } } } + +extension Mutex { + func unsafeLock() { storage.lock() } + func unsafeUnlock() { storage.unlock() } +} #endif