Skip to content

Commit 3a428d8

Browse files
committed
Initial Commit
1 parent 912a347 commit 3a428d8

File tree

5 files changed

+300
-0
lines changed

5 files changed

+300
-0
lines changed

Package.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version:5.7
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "AllocatedLock",
7+
platforms: [
8+
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)
9+
],
10+
products: [
11+
.library(
12+
name: "AllocatedLock",
13+
targets: ["AllocatedLock"]
14+
)
15+
],
16+
targets: [
17+
.target(
18+
name: "AllocatedLock",
19+
path: "Sources"
20+
),
21+
.testTarget(
22+
name: "AllocatedLockTests",
23+
dependencies: ["AllocatedLock"],
24+
path: "Tests"
25+
)
26+
]
27+
)

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[![Build](https://github.com/swhitty/FlyingFox/actions/workflows/build.yml/badge.svg)](https://github.com/swhitty/AllocatedLock/actions/workflows/build.yml)
2+
[![Codecov](https://codecov.io/gh/swhitty/FlyingFox/graphs/badge.svg)](https://codecov.io/gh/swhitty/AllocatedLock)
3+
[![Platforms](https://img.shields.io/badge/platforms-iOS%20|%20Mac%20|%20tvOS%20|%20Linux%20)](https://github.com/swhitty/AllocatedLock/blob/main/Package.swift)
4+
[![Swift 5.8](https://img.shields.io/badge/swift-5.5%20–%205.8-red.svg?style=flat)](https://developer.apple.com/swift)
5+
[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://opensource.org/licenses/MIT)
6+
[![Twitter](https://img.shields.io/badge/twitter-@simonwhitty-blue.svg)](http://twitter.com/simonwhitty)
7+
8+
# Introduction
9+
10+
**AllocatedLock** is a lightweight cross platform lock with an API compatible with [`OSAllocatedUnfairLock`](https://developer.apple.com/documentation/os/osallocatedunfairlock) available in macOS Ventura, iOS 16 and later. The lock wraps [`os_unfair_lock_t`](https://developer.apple.com/documentation/os/os_unfair_lock_t) on Darwin platforms and [`pthread_mutex_t`](https://man.freebsd.org/cgi/man.cgi?pthread_mutex_lock(3)) on Linux.
11+
12+
# Installation
13+
14+
AllocatedLock can be installed by using Swift Package Manager.
15+
16+
**Note:** AllocatedLock requires Swift 5.5 on Xcode 13.2+. It runs on iOS 13+, tvOS 13+, macOS 10.15+ and Linux.
17+
18+
To install using Swift Package Manager, add this to the `dependencies:` section in your Package.swift file:
19+
20+
```swift
21+
.package(url: "https://github.com/swhitty/AllocatedLock.git", .upToNextMajor(from: "0.0.1"))
22+
```
23+
24+
# Usage
25+
26+
Usage is similar with [`OSAllocatedUnfairLock`](https://developer.apple.com/documentation/os/osallocatedunfairlock).
27+
28+
The recommended usage to create a lock that protects some state:
29+
```swift
30+
let state = AllocatedLock<Int>(initialState: 0)
31+
```
32+
33+
Use `.withLock` to acquire the lock to read the state:
34+
```swift
35+
let val = state.withLock { $0 }
36+
```
37+
38+
Or mutate the state
39+
```swift
40+
let val = state.withLock { $0 += 1 }
41+
```
42+
43+
# Credits
44+
45+
AllocatedLock is primarily the work of [Simon Whitty](https://github.com/swhitty).
46+
47+
([Full list of contributors](https://github.com/swhitty/AllocatedLock/graphs/contributors))

Sources/AllocatedLock.swift

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//
2+
// AllocatedLock.swift
3+
// AllocatedLock
4+
//
5+
// Created by Simon Whitty on 10/04/2023.
6+
// Copyright 2023 Simon Whitty
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/AllocatedLock
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
// Backports the Swift interface around os_unfair_lock_t available in recent Darwin platforms
33+
//
34+
@available(iOS, deprecated: 16.0, message: "use OSAllocatedUnfairLock directly")
35+
@available(tvOS, deprecated: 16.0, message: "use OSAllocatedUnfairLock directly")
36+
@available(watchOS, deprecated: 9, message: "use OSAllocatedUnfairLock directly")
37+
@available(macOS, deprecated: 13.0, message: "use OSAllocatedUnfairLock directly")
38+
public struct AllocatedLock<State> {
39+
40+
private let storage: Storage
41+
42+
public init(initialState: State) {
43+
self.storage = Storage(initialState: initialState)
44+
}
45+
46+
public func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R: Sendable {
47+
storage.lock()
48+
defer { storage.unlock() }
49+
return try body(&storage.state)
50+
}
51+
}
52+
53+
public extension AllocatedLock where State == Void {
54+
55+
init() {
56+
self.storage = Storage(initialState: ())
57+
}
58+
59+
func lock() {
60+
storage.lock()
61+
}
62+
63+
func unlock() {
64+
storage.unlock()
65+
}
66+
67+
func withLock<R>(_ body: @Sendable () throws -> R) rethrows -> R where R: Sendable {
68+
storage.lock()
69+
defer { storage.unlock() }
70+
return try body()
71+
}
72+
}
73+
74+
#if canImport(Darwin)
75+
@_implementationOnly import os
76+
77+
private extension AllocatedLock {
78+
final class Storage {
79+
private let _lock: os_unfair_lock_t
80+
var state: State
81+
82+
init(initialState: State) {
83+
self._lock = .allocate(capacity: 1)
84+
self._lock.initialize(to: os_unfair_lock())
85+
self.state = initialState
86+
}
87+
88+
func lock() {
89+
os_unfair_lock_lock(_lock)
90+
}
91+
92+
func unlock() {
93+
os_unfair_lock_unlock(_lock)
94+
}
95+
96+
deinit {
97+
self._lock.deinitialize(count: 1)
98+
self._lock.deallocate()
99+
}
100+
}
101+
}
102+
103+
#elseif canImport(Glibc)
104+
@_implementationOnly import Glibc
105+
106+
private extension AllocatedLock {
107+
final class Storage {
108+
private let _lock: UnsafeMutablePointer<pthread_mutex_t>
109+
110+
var state: State
111+
112+
init(initialState: State) {
113+
var attr = pthread_mutexattr_t()
114+
pthread_mutexattr_init(&attr)
115+
self._lock = .allocate(capacity: 1)
116+
let err = pthread_mutex_init(self._lock, &attr)
117+
precondition(err == 0, "pthread_mutex_init error: \(err)")
118+
self.state = initialState
119+
}
120+
121+
func lock() {
122+
let err = pthread_mutex_lock(_lock)
123+
precondition(err == 0, "pthread_mutex_lock error: \(err)")
124+
}
125+
126+
func unlock() {
127+
let err = pthread_mutex_unlock(_lock)
128+
precondition(err == 0, "pthread_mutex_unlock error: \(err)")
129+
}
130+
131+
deinit {
132+
let err = pthread_mutex_destroy(self._lock)
133+
precondition(err == 0, "pthread_mutex_destroy error: \(err)")
134+
self._lock.deallocate()
135+
}
136+
}
137+
}
138+
#endif

Tests/AllocatedLockTests.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//
2+
// AllocatedLockTests.swift
3+
// AllocatedLock
4+
//
5+
// Created by Simon Whitty on 10/04/2023.
6+
// Copyright 2023 Simon Whitty
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/AllocatedLock
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
import AllocatedLock
33+
import XCTest
34+
35+
final class AllocatedLockTests: XCTestCase {
36+
37+
func testLockState_IsProtected() async {
38+
let state = AllocatedLock<Int>(initialState: 0)
39+
40+
let total = await withTaskGroup(of: Void.self) { group in
41+
for i in 1...10000 {
42+
group.addTask {
43+
state.withLock { $0 += i }
44+
}
45+
}
46+
await group.waitForAll()
47+
return state.withLock { $0 }
48+
}
49+
50+
XCTAssertEqual(total, 50005000)
51+
}
52+
53+
@MainActor
54+
func testLock_Blocks() async {
55+
let lock = AllocatedLock()
56+
lock.lock()
57+
58+
Task {
59+
try? await Task.sleep(nanoseconds: 1_000_000_000)
60+
lock.unlock()
61+
}
62+
63+
let results = await withTaskGroup(of: Bool.self) { group in
64+
group.addTask {
65+
try? await Task.sleep(nanoseconds: 5_000_000)
66+
return true
67+
}
68+
group.addTask {
69+
lock.lock()
70+
lock.unlock()
71+
return false
72+
}
73+
let first = await group.next()!
74+
let second = await group.next()!
75+
return [first, second]
76+
}
77+
XCTAssertEqual(results, [true, false])
78+
}
79+
}

docker-run-tests.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu
4+
5+
docker run -it \
6+
--rm \
7+
--mount src="$(pwd)",target=/allocatedLock,type=bind \
8+
swift \
9+
/usr/bin/swift test --package-path /allocatedLock

0 commit comments

Comments
 (0)