Skip to content

Commit 5ba8280

Browse files
committed
refactor: Update ifdefs and @_spi guards to allow development against traditional iOS targets, but to elide exact Dispatch api replacements for any current or future target that does not include Dispatch.
1 parent 2fb14f7 commit 5ba8280

12 files changed

+450
-348
lines changed

Package.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ let package = Package(
77
products: [
88
.library(
99
name: "DispatchAsync",
10-
targets: ["DispatchAsync"])
10+
targets: ["DispatchAsync"]
11+
),
1112
],
1213
targets: [
1314
.target(
14-
name: "DispatchAsync"),
15+
name: "DispatchAsync",
16+
),
1517
.testTarget(
1618
name: "DispatchAsyncTests",
17-
dependencies: ["DispatchAsync"]
19+
dependencies: [
20+
"DispatchAsync"
21+
],
1822
),
1923
]
2024
)

Sources/DispatchAsync/AsyncSemaphore.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414

1515
/// Provides a semaphore implantation in `async` context, with a safe wait method. Provides easy safe replacement
1616
/// for DispatchSemaphore usage.
17-
@available(macOS 10.15, *)
18-
actor AsyncSemaphore {
17+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
18+
public actor AsyncSemaphore {
1919
private var value: Int
2020
private var waiters: [CheckedContinuation<Void, Never>] = []
2121

22-
init(value: Int = 1) {
22+
public init(value: Int = 1) {
2323
self.value = value
2424
}
2525

26-
func wait() async {
26+
public func wait() async {
2727
value -= 1
2828

2929
if value >= 0 { return }
@@ -32,7 +32,7 @@ actor AsyncSemaphore {
3232
}
3333
}
3434

35-
func signal() {
35+
public func signal() {
3636
self.value += 1
3737

3838
guard !waiters.isEmpty else { return }
@@ -41,15 +41,15 @@ actor AsyncSemaphore {
4141
}
4242
}
4343

44-
@available(macOS 10.15, *)
44+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
4545
extension AsyncSemaphore {
46-
func withLock<T: Sendable>(_ closure: () async throws -> T) async rethrows -> T {
46+
public func withLock<T: Sendable>(_ closure: () async throws -> T) async rethrows -> T {
4747
await wait()
4848
defer { signal() }
4949
return try await closure()
5050
}
5151

52-
func withLockVoid(_ closure: () async throws -> Void) async rethrows {
52+
public func withLockVoid(_ closure: () async throws -> Void) async rethrows {
5353
await wait()
5454
defer { signal() }
5555
try await closure()

Sources/DispatchAsync/PackageConstants.swift renamed to Sources/DispatchAsync/DispatchAsync.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
package let kNanosecondsPerSecond: UInt64 = 1_000_000_000
16-
package let kNanosecondsPerMillisecond: UInt64 = 1_000_000
17-
package let kNanoSecondsPerMicrosecond: UInt64 = 1_000
15+
/// Top level namespace for functionality provided in DispatchAsync.
16+
///
17+
/// Used to avoid namespacing conflicts with `Dispatch` and `Foundation`
18+
///
19+
/// Platforms other than WASI shouldn't consume this library for now
20+
/// except for testing and development purposes.
21+
///
22+
/// TODO: SM: Add github permalink to this, after it is merged.
23+
#if !os(WASI)
24+
@_spi(DispatchAsync)
25+
#endif
26+
public enum DispatchAsync {}

Sources/DispatchAsync/DispatchGroup.swift

Lines changed: 106 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -12,118 +12,138 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
// MARK: - Public Interface for Non-Async Usage -
16-
17-
/// `DispatchGroup` is a drop-in replacement for the `DispatchGroup` implemented
18-
/// in Grand Central Dispatch. However, this class uses Swift Concurrency, instead of low-level threading API's.
19-
///
20-
/// The primary goal of this implementation is to enable WASM support for Dispatch.
21-
///
22-
/// Refer to documentation for the original [DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup)
23-
/// for more details,
24-
@available(macOS 10.15, *)
25-
public class DispatchGroup: @unchecked Sendable {
26-
private let group = _AsyncGroup()
27-
private let queue = FIFOQueue()
28-
29-
public func enter() {
30-
queue.enqueue { [weak self] in
31-
guard let self else { return }
32-
await group.enter()
15+
// NOTE: The following typealias mirrors Dispatch API's, but only for
16+
// specific compilation conditions where Dispatch is not available.
17+
// It is designed to safely elide away if and when Dispatch is introduced
18+
// in the required Dispatch support becomes available.
19+
#if os(WASI) && !canImport(Dispatch)
20+
/// Drop-in replacement for ``Dispatch.DispatchGroup``, implemented using pure swift.
21+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
22+
public typealias DispatchGroup = DispatchAsync.DispatchGroup
23+
#endif
24+
25+
extension DispatchAsync {
26+
// MARK: - Public Interface for Non-Async Usage -
27+
28+
/// Drop-in replacement for ``Dispatch.DispatchGroup``, implemented using pure swift.
29+
///
30+
/// The primary goal of this implementation is to enable WASM support for Dispatch.
31+
///
32+
/// Refer to documentation for the original [DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup)
33+
/// for more details,
34+
#if !os(WASI)
35+
@_spi(DispatchAsync)
36+
#endif
37+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
38+
public class DispatchGroup: @unchecked Sendable {
39+
private let group = _AsyncGroup()
40+
private let queue = DispatchAsync.FIFOQueue()
41+
42+
public func enter() {
43+
queue.enqueue { [weak self] in
44+
guard let self else { return }
45+
await group.enter()
46+
}
3347
}
34-
}
3548

36-
public func leave() {
37-
queue.enqueue { [weak self] in
38-
guard let self else { return }
39-
await group.leave()
49+
public func leave() {
50+
queue.enqueue { [weak self] in
51+
guard let self else { return }
52+
await group.leave()
53+
}
4054
}
41-
}
4255

43-
public func notify(queue notificationQueue: DispatchQueue, execute work: @escaping @Sendable @convention(block) () -> Void) {
44-
queue.enqueue { [weak self] in
45-
guard let self else { return }
46-
await group.notify {
47-
await withCheckedContinuation { continuation in
48-
notificationQueue.async {
49-
work()
50-
continuation.resume()
56+
public func notify(
57+
queue notificationQueue: DispatchAsync.DispatchQueue,
58+
execute work: @escaping @Sendable @convention(block) () -> Void
59+
) {
60+
queue.enqueue { [weak self] in
61+
guard let self else { return }
62+
await group.notify {
63+
await withCheckedContinuation { continuation in
64+
notificationQueue.async {
65+
work()
66+
continuation.resume()
67+
}
5168
}
5269
}
5370
}
5471
}
55-
}
5672

57-
func wait() async {
58-
await withCheckedContinuation { continuation in
59-
queue.enqueue { [weak self] in
60-
guard let self else { return }
61-
// NOTE: We use a task for the wait, because
62-
// otherwise the queue won't execute any more
63-
// tasks until the wait finishes, which is not the
64-
// behavior we want here. We want to enqueue the wait
65-
// in FIFO call order, but then we want to allow the wait
66-
// to be non-blocking for the queue until the last leave
67-
// is called on the group.
68-
Task {
69-
await group.wait()
70-
continuation.resume()
73+
func wait() async {
74+
await withCheckedContinuation { continuation in
75+
queue.enqueue { [weak self] in
76+
guard let self else { return }
77+
// NOTE: We use a task for the wait, because
78+
// otherwise the queue won't execute any more
79+
// tasks until the wait finishes, which is not the
80+
// behavior we want here. We want to enqueue the wait
81+
// in FIFO call order, but then we want to allow the wait
82+
// to be non-blocking for the queue until the last leave
83+
// is called on the group.
84+
Task {
85+
await group.wait()
86+
continuation.resume()
87+
}
7188
}
7289
}
7390
}
91+
92+
public init() {}
7493
}
7594

76-
public init() {}
77-
}
95+
// MARK: - Private Interface for Async Usage -
7896

79-
// MARK: - Private Interface for Async Usage -
97+
#if !os(WASI)
98+
@_spi(DispatchAsync)
99+
#endif
100+
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
101+
fileprivate actor _AsyncGroup {
102+
private var taskCount = 0
103+
private var notifyHandlers: [@Sendable () async -> Void] = []
80104

81-
@available(macOS 10.15, *)
82-
fileprivate actor _AsyncGroup {
83-
private var taskCount = 0
84-
private var notifyHandlers: [@Sendable () async -> Void] = []
105+
func enter() {
106+
taskCount += 1
107+
}
85108

86-
func enter() {
87-
taskCount += 1
88-
}
109+
func leave() {
110+
defer {
111+
checkCompletion()
112+
}
113+
guard taskCount > 0 else {
114+
assertionFailure("leave() called more times than enter()")
115+
return
116+
}
117+
taskCount -= 1
118+
}
89119

90-
func leave() {
91-
defer {
120+
func notify(handler: @escaping @Sendable () async -> Void) {
121+
notifyHandlers.append(handler)
92122
checkCompletion()
93123
}
94-
guard taskCount > 0 else {
95-
assertionFailure("leave() called more times than enter()")
96-
return
97-
}
98-
taskCount -= 1
99-
}
100124

101-
func notify(handler: @escaping @Sendable () async -> Void) {
102-
notifyHandlers.append(handler)
103-
checkCompletion()
104-
}
105-
106-
func wait() async {
107-
if taskCount <= 0 {
108-
return
109-
}
125+
func wait() async {
126+
if taskCount <= 0 {
127+
return
128+
}
110129

111-
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
112-
notify {
113-
continuation.resume()
130+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
131+
notify {
132+
continuation.resume()
133+
}
134+
checkCompletion()
114135
}
115-
checkCompletion()
116136
}
117-
}
118137

119-
private func checkCompletion() {
120-
if taskCount <= 0, !notifyHandlers.isEmpty {
121-
let handlers = notifyHandlers
122-
notifyHandlers.removeAll()
138+
private func checkCompletion() {
139+
if taskCount <= 0, !notifyHandlers.isEmpty {
140+
let handlers = notifyHandlers
141+
notifyHandlers.removeAll()
123142

124-
for handler in handlers {
125-
Task {
126-
await handler()
143+
for handler in handlers {
144+
Task {
145+
await handler()
146+
}
127147
}
128148
}
129149
}

0 commit comments

Comments
 (0)