Skip to content

Commit 7a12547

Browse files
committed
Compatibility with OSAllocatedUnfairLock
1 parent 1b6f355 commit 7a12547

File tree

5 files changed

+217
-26
lines changed

5 files changed

+217
-26
lines changed

.github/workflows/build.yml

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ on:
66
workflow_dispatch:
77

88
jobs:
9-
xcode_15_2:
9+
xcode_16:
1010
runs-on: macos-14
11-
env:
12-
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
1311
steps:
1412
- name: Checkout
1513
uses: actions/checkout@v4
14+
- name: 🔍 Xcode Select
15+
run: |
16+
XCODE_PATH=`mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode' && kMDItemVersion = '16.*'" -onlyin /Applications | head -1`
17+
echo "DEVELOPER_DIR=$XCODE_PATH/Contents/Developer" >> $GITHUB_ENV
1618
- name: Version
1719
run: swift --version
1820
- name: Build
@@ -27,10 +29,10 @@ jobs:
2729
token: ${{ secrets.CODECOV_TOKEN }}
2830
files: ./coverage_report.lcov
2931

30-
xcode_14_3_1:
31-
runs-on: macos-13
32+
xcode_15_4:
33+
runs-on: macos-14
3234
env:
33-
DEVELOPER_DIR: /Applications/Xcode_14.3.1.app/Contents/Developer
35+
DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
3436
steps:
3537
- name: Checkout
3638
uses: actions/checkout@v4
@@ -41,9 +43,10 @@ jobs:
4143
- name: Test
4244
run: swift test
4345

44-
linux_swift_5_10:
45-
runs-on: ubuntu-latest
46-
container: swift:5.8
46+
xcode_15_2:
47+
runs-on: macos-14
48+
env:
49+
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
4750
steps:
4851
- name: Checkout
4952
uses: actions/checkout@v4
@@ -52,11 +55,11 @@ jobs:
5255
- name: Build
5356
run: swift build --build-tests
5457
- name: Test
55-
run: swift test --skip-build
58+
run: swift test
5659

57-
linux_swift_5_9:
60+
linux_swift_6_0:
5861
runs-on: ubuntu-latest
59-
container: swift:5.8
62+
container: swiftlang/swift:nightly-6.0-jammy
6063
steps:
6164
- name: Checkout
6265
uses: actions/checkout@v4
@@ -67,7 +70,7 @@ jobs:
6770
- name: Test
6871
run: swift test --skip-build
6972

70-
linux_swift_5_8:
73+
linux_swift_5_10:
7174
runs-on: ubuntu-latest
7275
container: swift:5.8
7376
steps:
@@ -80,9 +83,9 @@ jobs:
8083
- name: Test
8184
run: swift test --skip-build
8285

83-
linux_swift_5_7:
86+
linux_swift_5_9:
8487
runs-on: ubuntu-latest
85-
container: swift:5.7
88+
container: swift:5.8
8689
steps:
8790
- name: Checkout
8891
uses: actions/checkout@v4

Package.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.7
1+
// swift-tools-version:6.0
22

33
import PackageDescription
44

@@ -16,12 +16,23 @@ let package = Package(
1616
targets: [
1717
.target(
1818
name: "AllocatedLock",
19-
path: "Sources"
19+
path: "Sources",
20+
swiftSettings: .upcomingFeatures
2021
),
2122
.testTarget(
2223
name: "AllocatedLockTests",
2324
dependencies: ["AllocatedLock"],
24-
path: "Tests"
25+
path: "Tests",
26+
swiftSettings: .upcomingFeatures
2527
)
2628
]
2729
)
30+
31+
extension Array where Element == SwiftSetting {
32+
33+
static var upcomingFeatures: [SwiftSetting] {
34+
[
35+
.swiftLanguageMode(.v6)
36+
]
37+
}
38+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.8
1+
// swift-tools-version:5.9
22

33
import PackageDescription
44

Sources/AllocatedLock.swift

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@
2929
// SOFTWARE.
3030
//
3131

32-
// Backports the Swift interface around os_unfair_lock_t available in recent Darwin platforms
33-
//
32+
import os
33+
34+
// Backports the Swift interface around OSAllocatedUnfairLock available in recent Darwin platforms
3435
public struct AllocatedLock<State>: @unchecked Sendable {
3536

3637
@usableFromInline
3738
let storage: Storage
3839

39-
public init(initialState: State) {
40+
public init(uncheckedState initialState: State) {
41+
self.storage = Storage(initialState: initialState)
42+
}
43+
44+
public init(initialState: State) where State: Sendable {
4045
self.storage = Storage(initialState: initialState)
4146
}
4247

@@ -46,6 +51,28 @@ public struct AllocatedLock<State>: @unchecked Sendable {
4651
defer { storage.unlock() }
4752
return try body(&storage.state)
4853
}
54+
55+
@inlinable
56+
public func withLockIfAvailable<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R? where R: Sendable {
57+
guard storage.tryLock() else { return nil }
58+
defer { storage.unlock() }
59+
return try body(&storage.state)
60+
}
61+
62+
@inlinable
63+
public func withLockIfAvailableUnchecked<R>(_ body: (inout State) throws -> R) rethrows -> R? {
64+
guard storage.tryLock() else { return nil }
65+
defer { storage.unlock() }
66+
return try body(&storage.state)
67+
}
68+
69+
@inlinable
70+
public func withLockUnchecked<R>(_ body: (inout State) throws -> R) rethrows -> R {
71+
storage.lock()
72+
defer { storage.unlock() }
73+
return try body(&storage.state)
74+
}
75+
4976
}
5077

5178
public extension AllocatedLock where State == Void {
@@ -59,6 +86,11 @@ public extension AllocatedLock where State == Void {
5986
storage.lock()
6087
}
6188

89+
@inlinable @available(*, noasync)
90+
func lockIfAvailable() -> Bool {
91+
storage.tryLock()
92+
}
93+
6294
@inlinable @available(*, noasync)
6395
func unlock() {
6496
storage.unlock()
@@ -70,6 +102,27 @@ public extension AllocatedLock where State == Void {
70102
defer { storage.unlock() }
71103
return try body()
72104
}
105+
106+
@inlinable
107+
func withLockIfAvailable<R>(_ body: @Sendable () throws -> R) rethrows -> R? where R: Sendable {
108+
guard storage.tryLock() else { return nil }
109+
defer { storage.unlock() }
110+
return try body()
111+
}
112+
113+
@inlinable
114+
func withLockIfAvailableUnchecked<R>(_ body: () throws -> R) rethrows -> R? {
115+
guard storage.tryLock() else { return nil }
116+
defer { storage.unlock() }
117+
return try body()
118+
}
119+
120+
@inlinable
121+
func withLockUnchecked<R>(_ body: () throws -> R) rethrows -> R {
122+
storage.lock()
123+
defer { storage.unlock() }
124+
return try body()
125+
}
73126
}
74127

75128
#if canImport(Darwin)
@@ -78,6 +131,7 @@ import struct os.os_unfair_lock_t
78131
import struct os.os_unfair_lock
79132
import func os.os_unfair_lock_lock
80133
import func os.os_unfair_lock_unlock
134+
import func os.os_unfair_lock_trylock
81135

82136
extension AllocatedLock {
83137
@usableFromInline
@@ -103,6 +157,11 @@ extension AllocatedLock {
103157
os_unfair_lock_unlock(_lock)
104158
}
105159

160+
@usableFromInline
161+
func tryLock() -> Bool {
162+
os_unfair_lock_trylock(_lock)
163+
}
164+
106165
deinit {
107166
self._lock.deinitialize(count: 1)
108167
self._lock.deallocate()
@@ -143,6 +202,11 @@ extension AllocatedLock {
143202
precondition(err == 0, "pthread_mutex_unlock error: \(err)")
144203
}
145204

205+
@usableFromInline
206+
func tryLock() -> Bool {
207+
pthread_mutex_trylock(&_lock) == 0
208+
}
209+
146210
deinit {
147211
let err = pthread_mutex_destroy(self._lock)
148212
precondition(err == 0, "pthread_mutex_destroy error: \(err)")
@@ -179,6 +243,17 @@ extension AllocatedLock {
179243
func unlock() {
180244
ReleaseSRWLockExclusive(_lock)
181245
}
246+
247+
@usableFromInline
248+
func tryLock() -> Bool {
249+
TryAcquireSRWLockExclusive(_lock)
250+
}
251+
252+
@usableFromInline
253+
func tryLock() -> Bool {
254+
os_unfair_lock_trylock(_lock)
255+
}
256+
182257
}
183258
}
184259

Tests/AllocatedLockTests.swift

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
// SOFTWARE.
3030
//
3131

32-
import AllocatedLock
32+
@testable import AllocatedLock
3333
import XCTest
3434

3535
final class AllocatedLockTests: XCTestCase {
@@ -84,10 +84,112 @@ final class AllocatedLockTests: XCTestCase {
8484
}
8585
XCTAssertEqual(results, [true, false])
8686
}
87+
88+
func testTryLock() {
89+
let lock = AllocatedLock()
90+
let value = lock.withLock { true }
91+
XCTAssertTrue(value)
92+
}
93+
94+
func testIfAvailable() {
95+
let lock = AllocatedLock(uncheckedState: 5)
96+
XCTAssertEqual(
97+
lock.withLock { _ in "fish" },
98+
"fish"
99+
)
100+
101+
lock.unsafeLock()
102+
XCTAssertEqual(
103+
lock.withLockIfAvailable { _ in "fish" },
104+
String?.none
105+
)
106+
107+
lock.unsafeUnlock()
108+
XCTAssertEqual(
109+
lock.withLockIfAvailable { _ in "fish" },
110+
"fish"
111+
)
112+
}
113+
114+
func testIfAvailableUnchecked() {
115+
let lock = AllocatedLock(uncheckedState: NonSendable("fish"))
116+
XCTAssertEqual(
117+
lock.withLockUnchecked { $0 }.name,
118+
"fish"
119+
)
120+
121+
lock.unsafeLock()
122+
XCTAssertNil(
123+
lock.withLockIfAvailableUnchecked { $0 }?.name
124+
)
125+
126+
lock.unsafeUnlock()
127+
XCTAssertEqual(
128+
lock.withLockIfAvailableUnchecked { $0 }?.name,
129+
"fish"
130+
)
131+
}
132+
133+
func testVoidIfAvailable() {
134+
let lock = AllocatedLock()
135+
XCTAssertEqual(
136+
lock.withLock { "fish" },
137+
"fish"
138+
)
139+
140+
lock.unsafeLock()
141+
XCTAssertEqual(
142+
lock.withLockIfAvailable { "fish" },
143+
String?.none
144+
)
145+
146+
lock.unsafeUnlock()
147+
XCTAssertEqual(
148+
lock.withLockIfAvailable { "fish" },
149+
"fish"
150+
)
151+
}
152+
153+
func testVoidIfAvailableUnchecked() {
154+
let lock = AllocatedLock()
155+
XCTAssertEqual(
156+
lock.withLockUnchecked { NonSendable("fish") }.name,
157+
"fish"
158+
)
159+
160+
lock.lock()
161+
XCTAssertNil(
162+
lock.withLockIfAvailableUnchecked { NonSendable("fish") }
163+
)
164+
165+
lock.unlock()
166+
XCTAssertEqual(
167+
lock.withLockIfAvailableUnchecked { NonSendable("chips") }?.name,
168+
"chips"
169+
)
170+
}
171+
172+
func testVoidLock() {
173+
let lock = AllocatedLock()
174+
lock.lock()
175+
XCTAssertFalse(lock.lockIfAvailable())
176+
lock.unlock()
177+
XCTAssertTrue(lock.lockIfAvailable())
178+
lock.unlock()
179+
}
180+
}
181+
182+
public final class NonSendable {
183+
184+
let name: String
185+
186+
init(_ name: String) {
187+
self.name = name
188+
}
87189
}
88190

89191
// sidestep warning: unavailable from asynchronous contexts
90-
extension AllocatedLock where State == Void {
91-
func unsafeLock() { lock() }
92-
func unsafeUnlock() { unlock() }
192+
extension AllocatedLock {
193+
func unsafeLock() { storage.lock() }
194+
func unsafeUnlock() { storage.unlock() }
93195
}

0 commit comments

Comments
 (0)