Skip to content

Commit f78b061

Browse files
committed
full compatibility with OSAllocatedUnfairLock
1 parent 1b6f355 commit f78b061

File tree

5 files changed

+216
-27
lines changed

5 files changed

+216
-27
lines changed

.github/workflows/build.yml

Lines changed: 19 additions & 16 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,9 +70,9 @@ 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
72-
container: swift:5.8
75+
container: swift:5.10
7376
steps:
7477
- name: Checkout
7578
uses: actions/checkout@v4
@@ -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.9
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: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@
2929
// SOFTWARE.
3030
//
3131

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

3635
@usableFromInline
3736
let storage: Storage
3837

39-
public init(initialState: State) {
38+
public init(uncheckedState initialState: State) {
39+
self.storage = Storage(initialState: initialState)
40+
}
41+
42+
public init(initialState: State) where State: Sendable {
4043
self.storage = Storage(initialState: initialState)
4144
}
4245

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

5176
public extension AllocatedLock where State == Void {
@@ -59,6 +84,11 @@ public extension AllocatedLock where State == Void {
5984
storage.lock()
6085
}
6186

87+
@inlinable @available(*, noasync)
88+
func lockIfAvailable() -> Bool {
89+
storage.tryLock()
90+
}
91+
6292
@inlinable @available(*, noasync)
6393
func unlock() {
6494
storage.unlock()
@@ -70,6 +100,27 @@ public extension AllocatedLock where State == Void {
70100
defer { storage.unlock() }
71101
return try body()
72102
}
103+
104+
@inlinable
105+
func withLockIfAvailable<R>(_ body: @Sendable () throws -> R) rethrows -> R? where R: Sendable {
106+
guard storage.tryLock() else { return nil }
107+
defer { storage.unlock() }
108+
return try body()
109+
}
110+
111+
@inlinable
112+
func withLockIfAvailableUnchecked<R>(_ body: () throws -> R) rethrows -> R? {
113+
guard storage.tryLock() else { return nil }
114+
defer { storage.unlock() }
115+
return try body()
116+
}
117+
118+
@inlinable
119+
func withLockUnchecked<R>(_ body: () throws -> R) rethrows -> R {
120+
storage.lock()
121+
defer { storage.unlock() }
122+
return try body()
123+
}
73124
}
74125

75126
#if canImport(Darwin)
@@ -78,6 +129,7 @@ import struct os.os_unfair_lock_t
78129
import struct os.os_unfair_lock
79130
import func os.os_unfair_lock_lock
80131
import func os.os_unfair_lock_unlock
132+
import func os.os_unfair_lock_trylock
81133

82134
extension AllocatedLock {
83135
@usableFromInline
@@ -103,6 +155,11 @@ extension AllocatedLock {
103155
os_unfair_lock_unlock(_lock)
104156
}
105157

158+
@usableFromInline
159+
func tryLock() -> Bool {
160+
os_unfair_lock_trylock(_lock)
161+
}
162+
106163
deinit {
107164
self._lock.deinitialize(count: 1)
108165
self._lock.deallocate()
@@ -143,6 +200,11 @@ extension AllocatedLock {
143200
precondition(err == 0, "pthread_mutex_unlock error: \(err)")
144201
}
145202

203+
@usableFromInline
204+
func tryLock() -> Bool {
205+
pthread_mutex_trylock(&_lock) == 0
206+
}
207+
146208
deinit {
147209
let err = pthread_mutex_destroy(self._lock)
148210
precondition(err == 0, "pthread_mutex_destroy error: \(err)")
@@ -179,6 +241,17 @@ extension AllocatedLock {
179241
func unlock() {
180242
ReleaseSRWLockExclusive(_lock)
181243
}
244+
245+
@usableFromInline
246+
func tryLock() -> Bool {
247+
TryAcquireSRWLockExclusive(_lock)
248+
}
249+
250+
@usableFromInline
251+
func tryLock() -> Bool {
252+
os_unfair_lock_trylock(_lock)
253+
}
254+
182255
}
183256
}
184257

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)