Skip to content

Commit ed89127

Browse files
committed
Mutex should be ~Copyable
1 parent c7c4773 commit ed89127

File tree

5 files changed

+256
-61
lines changed

5 files changed

+256
-61
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171

7272
linux_swift_6_1:
7373
runs-on: ubuntu-latest
74-
container: swift:6.1
74+
container: swift:6.1.2
7575
steps:
7676
- name: Checkout
7777
uses: actions/checkout@v4
@@ -123,7 +123,7 @@ jobs:
123123

124124
linux_swift_6_1_musl:
125125
runs-on: ubuntu-latest
126-
container: swift:6.1
126+
container: swift:6.1.2
127127
steps:
128128
- name: Checkout
129129
uses: actions/checkout@v4
@@ -146,7 +146,7 @@ jobs:
146146
- name: Install Swift
147147
uses: SwiftyLab/setup-swift@latest
148148
with:
149-
swift-version: "6.1.0"
149+
swift-version: "6.1.2"
150150
- name: Version
151151
run: swift --version
152152
- name: Build

Sources/AllocatedLock.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
//
3131

3232
// Backports the Swift interface around OSAllocatedUnfairLock available in recent Darwin platforms
33+
@available(*, deprecated, message: "Unused by Mutex and will be removed in future versions.")
3334
public struct AllocatedLock<State>: @unchecked Sendable {
3435

3536
@usableFromInline

Sources/Mutex.swift

Lines changed: 243 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -29,82 +29,271 @@
2929
// SOFTWARE.
3030
//
3131

32+
#if compiler(>=6) && !canImport(WinSDK)
33+
3234
// Backports the Swift 6 type Mutex<Value> to all Darwin platforms
3335

34-
// @available(macOS, deprecated: 15.0, message: "use Mutex from Synchronization module included with Swift 6")
35-
// @available(iOS, deprecated: 18.0, message: "use Mutex from Synchronization module included with Swift 6")
36-
// @available(tvOS, deprecated: 18.0, message: "use Mutex from Synchronization module included with Swift 6")
37-
// @available(watchOS, deprecated: 11.0, message: "use Mutex from Synchronization module included with Swift 6")
38-
// @available(visionOS, deprecated: 2.0, message: "use Mutex from Synchronization module included with Swift 6")
39-
public struct Mutex<Value>: Sendable {
40-
let lock: AllocatedLock<Value> // Compatible with OSAllocatedUnfairLock iOS 16+
41-
}
36+
@available(macOS, introduced: 13.0, deprecated: 15.0, message: "use Mutex from Synchronization module")
37+
@available(iOS, introduced: 16.0, deprecated: 18.0, message: "use Mutex from Synchronization module")
38+
@available(tvOS, introduced: 18.0, deprecated: 15.0, message: "use Mutex from Synchronization module")
39+
@available(watchOS, introduced: 11.0, deprecated: 15.0, message: "use Mutex from Synchronization module")
40+
@available(visionOS, introduced: 2.0, deprecated: 15.0, message: "use Mutex from Synchronization module")
41+
public struct Mutex<Value: ~Copyable>: @unchecked Sendable, ~Copyable {
42+
let storage: Storage<Value>
4243

43-
#if compiler(>=6)
44-
public extension Mutex {
45-
init(_ initialValue: consuming sending Value) {
46-
self.lock = AllocatedLock(uncheckedState: initialValue)
44+
public init(_ initialValue: consuming sending Value) {
45+
self.storage = Storage(initialValue)
4746
}
4847

49-
borrowing func withLock<Result, E: Error>(
48+
public borrowing func withLock<Result, E: Error>(
5049
_ body: (inout sending Value) throws(E) -> sending Result
5150
) throws(E) -> sending Result {
52-
do {
53-
return try lock.withLockUnchecked { value in
54-
nonisolated(unsafe) var copy = value
55-
defer { value = copy }
56-
return try Transferring(body(&copy))
57-
}.value
58-
} catch let error as E {
59-
throw error
60-
} catch {
61-
preconditionFailure("cannot occur")
62-
}
63-
}
64-
65-
borrowing func withLockIfAvailable<Result, E>(
51+
storage.lock()
52+
defer { storage.unlock() }
53+
return try body(&storage.value)
54+
}
55+
56+
public borrowing func withLockIfAvailable<Result, E: Error>(
6657
_ body: (inout sending Value) throws(E) -> sending Result
67-
) throws(E) -> sending Result? where E: Error {
68-
do {
69-
return try lock.withLockIfAvailableUnchecked { value in
70-
nonisolated(unsafe) var copy = value
71-
defer { value = copy }
72-
return try Transferring(body(&copy))
73-
}?.value
74-
} catch let error as E {
75-
throw error
76-
} catch {
77-
preconditionFailure("cannot occur")
78-
}
58+
) throws(E) -> sending Result? {
59+
guard storage.tryLock() else { return nil }
60+
defer { storage.unlock() }
61+
return try body(&storage.value)
7962
}
8063
}
81-
private struct Transferring<T> {
82-
nonisolated(unsafe) var value: T
8364

84-
init(_ value: T) {
85-
self.value = value
65+
#elseif compiler(>=6) && canImport(WinSDK)
66+
67+
// Windows doesn't support ~Copyable yet
68+
69+
public struct Mutex<Value>: @unchecked Sendable {
70+
let storage: Storage<Value>
71+
72+
public init(_ initialValue: consuming sending Value) {
73+
self.storage = Storage(initialValue)
74+
}
75+
76+
public borrowing func withLock<Result, E: Error>(
77+
_ body: (inout sending Value) throws(E) -> sending Result
78+
) throws(E) -> sending Result {
79+
storage.lock()
80+
defer { storage.unlock() }
81+
return try body(&storage.value)
82+
}
83+
84+
public borrowing func withLockIfAvailable<Result, E: Error>(
85+
_ body: (inout sending Value) throws(E) -> sending Result
86+
) throws(E) -> sending Result? {
87+
guard storage.tryLock() else { return nil }
88+
defer { storage.unlock() }
89+
return try body(&storage.value)
8690
}
8791
}
92+
8893
#else
89-
public extension Mutex {
90-
init(_ initialValue: consuming Value) {
91-
self.lock = AllocatedLock(uncheckedState: initialValue)
94+
95+
// Use Swift 5 compatible version
96+
97+
public struct Mutex<Value>: @unchecked Sendable {
98+
let storage: Storage<Value>
99+
100+
public init(_ initialValue: Value) {
101+
self.storage = Storage(initialValue)
92102
}
93103

94-
borrowing func withLock<Result>(
104+
public borrowing func withLock<Result>(
95105
_ body: (inout Value) throws -> Result
96106
) rethrows -> Result {
97-
try lock.withLockUnchecked {
98-
return try body(&$0)
99-
}
107+
storage.lock()
108+
defer { storage.unlock() }
109+
return try body(&storage.value)
100110
}
101111

102-
borrowing func withLockIfAvailable<Result>(
112+
public borrowing func withLockIfAvailable<Result>(
103113
_ body: (inout Value) throws -> Result
104114
) rethrows -> Result? {
105-
try lock.withLockIfAvailableUnchecked {
106-
return try body(&$0)
107-
}
115+
guard storage.tryLock() else { return nil }
116+
defer { storage.unlock() }
117+
return try body(&storage.value)
118+
}
119+
}
120+
121+
#endif
122+
123+
#if canImport(Darwin)
124+
125+
import struct os.os_unfair_lock_t
126+
import struct os.os_unfair_lock
127+
import func os.os_unfair_lock_lock
128+
import func os.os_unfair_lock_unlock
129+
import func os.os_unfair_lock_trylock
130+
131+
#if compiler(>=6.0)
132+
final class Storage<Value: ~Copyable> {
133+
private let _lock: os_unfair_lock_t
134+
var value: Value
135+
136+
init(_ initialValue: consuming Value) {
137+
self._lock = .allocate(capacity: 1)
138+
self._lock.initialize(to: os_unfair_lock())
139+
self.value = initialValue
140+
}
141+
142+
func lock() {
143+
os_unfair_lock_lock(_lock)
144+
}
145+
146+
func unlock() {
147+
os_unfair_lock_unlock(_lock)
148+
}
149+
150+
func tryLock() -> Bool {
151+
os_unfair_lock_trylock(_lock)
152+
}
153+
154+
deinit {
155+
self._lock.deinitialize(count: 1)
156+
self._lock.deallocate()
157+
}
158+
}
159+
#else
160+
final class Storage<Value> {
161+
private let _lock: os_unfair_lock_t
162+
var value: Value
163+
164+
init(_ initialValue: consuming Value) {
165+
self._lock = .allocate(capacity: 1)
166+
self._lock.initialize(to: os_unfair_lock())
167+
self.value = initialValue
168+
}
169+
170+
func lock() {
171+
os_unfair_lock_lock(_lock)
172+
}
173+
174+
func unlock() {
175+
os_unfair_lock_unlock(_lock)
176+
}
177+
178+
func tryLock() -> Bool {
179+
os_unfair_lock_trylock(_lock)
180+
}
181+
182+
deinit {
183+
self._lock.deinitialize(count: 1)
184+
self._lock.deallocate()
185+
}
186+
}
187+
#endif
188+
189+
#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic)
190+
191+
#if canImport(Musl)
192+
import Musl
193+
#elseif canImport(Bionic)
194+
import Android
195+
#else
196+
import Glibc
197+
#endif
198+
199+
#if compiler(>=6.0)
200+
final class Storage<Value: ~Copyable> {
201+
private let _lock: UnsafeMutablePointer<pthread_mutex_t>
202+
203+
var value: Value
204+
205+
init(_ initialValue: consuming Value) {
206+
var attr = pthread_mutexattr_t()
207+
pthread_mutexattr_init(&attr)
208+
self._lock = .allocate(capacity: 1)
209+
let err = pthread_mutex_init(self._lock, &attr)
210+
precondition(err == 0, "pthread_mutex_init error: \(err)")
211+
self.value = initialValue
212+
}
213+
214+
func lock() {
215+
let err = pthread_mutex_lock(_lock)
216+
precondition(err == 0, "pthread_mutex_lock error: \(err)")
217+
}
218+
219+
func unlock() {
220+
let err = pthread_mutex_unlock(_lock)
221+
precondition(err == 0, "pthread_mutex_unlock error: \(err)")
222+
}
223+
224+
func tryLock() -> Bool {
225+
pthread_mutex_trylock(_lock) == 0
226+
}
227+
228+
deinit {
229+
let err = pthread_mutex_destroy(self._lock)
230+
precondition(err == 0, "pthread_mutex_destroy error: \(err)")
231+
self._lock.deallocate()
232+
}
233+
}
234+
#else
235+
final class Storage<Value> {
236+
private let _lock: UnsafeMutablePointer<pthread_mutex_t>
237+
var value: Value
238+
239+
init(_ initialValue: consuming Value) {
240+
var attr = pthread_mutexattr_t()
241+
pthread_mutexattr_init(&attr)
242+
self._lock = .allocate(capacity: 1)
243+
let err = pthread_mutex_init(self._lock, &attr)
244+
precondition(err == 0, "pthread_mutex_init error: \(err)")
245+
self.value = initialValue
246+
}
247+
248+
func lock() {
249+
let err = pthread_mutex_lock(_lock)
250+
precondition(err == 0, "pthread_mutex_lock error: \(err)")
251+
}
252+
253+
func unlock() {
254+
let err = pthread_mutex_unlock(_lock)
255+
precondition(err == 0, "pthread_mutex_unlock error: \(err)")
256+
}
257+
258+
func tryLock() -> Bool {
259+
pthread_mutex_trylock(_lock) == 0
260+
}
261+
262+
deinit {
263+
let err = pthread_mutex_destroy(self._lock)
264+
precondition(err == 0, "pthread_mutex_destroy error: \(err)")
265+
self._lock.deallocate()
266+
}
267+
}
268+
#endif
269+
270+
#elseif canImport(WinSDK)
271+
272+
import ucrt
273+
import WinSDK
274+
275+
final class Storage<Value> {
276+
private let _lock: UnsafeMutablePointer<SRWLOCK>
277+
278+
var value: Value
279+
280+
init(_ initialValue: Value) {
281+
self._lock = .allocate(capacity: 1)
282+
InitializeSRWLock(self._lock)
283+
self.value = initialValue
284+
}
285+
286+
func lock() {
287+
AcquireSRWLockExclusive(_lock)
288+
}
289+
290+
func unlock() {
291+
ReleaseSRWLockExclusive(_lock)
292+
}
293+
294+
func tryLock() -> Bool {
295+
TryAcquireSRWLockExclusive(_lock) != 0
108296
}
109297
}
298+
110299
#endif

Tests/MutexTests.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ struct MutexTests {
5555
@Test
5656
func lockIfAvailable_ReturnsValue() {
5757
let mutex = Mutex("fish")
58-
mutex.lock.unsafeLock()
58+
mutex.unsafeLock()
5959
#expect(
6060
mutex.withLockIfAvailable { _ in "chips" } == nil
6161
)
62-
mutex.lock.unsafeUnlock()
62+
mutex.unsafeUnlock()
6363
#expect(
6464
mutex.withLockIfAvailable { _ in "chips" } == "chips"
6565
)
@@ -74,3 +74,8 @@ struct MutexTests {
7474
}
7575
}
7676
#endif
77+
78+
extension Mutex {
79+
func unsafeLock() { storage.lock() }
80+
func unsafeUnlock() { storage.unlock() }
81+
}

Tests/MutexXCTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ final class MutexTests: XCTestCase {
5252

5353
func testLockIfAvailable_ReturnsValue() {
5454
let mutex = Mutex("fish")
55-
mutex.lock.unsafeLock()
55+
mutex.unsafeLock()
5656
XCTAssertNil(
5757
mutex.withLockIfAvailable { _ in "chips" }
5858
)
59-
mutex.lock.unsafeUnlock()
59+
mutex.unsafeUnlock()
6060
XCTAssertEqual(
6161
mutex.withLockIfAvailable { _ in "chips" },
6262
"chips"

0 commit comments

Comments
 (0)