From 1a8946bb68398f09d8670b0d961c13e2f5936f2f Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 25 Jun 2025 15:27:37 -0600 Subject: [PATCH 01/53] chore: refine gitignore --- .gitignore | 68 ++++++------------------------------------------------ 1 file changed, 7 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 52fe2f7..0023a53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,62 +1,8 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings +.DS_Store +/.build +/Packages xcuserdata/ - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc From 940349b50795f51e6e92f0defa8536727e07ab99 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 25 Jun 2025 15:27:58 -0600 Subject: [PATCH 02/53] chore: Set up initial swift package manifest file. --- Package.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Package.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..3817f11 --- /dev/null +++ b/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 6.1 + +import PackageDescription + +let package = Package( + name: "dispatch-async", + products: [ + .library( + name: "DispatchAsync", + targets: ["DispatchAsync"]), + ], + targets: [ + .target( + name: "DispatchAsync"), + .testTarget( + name: "DispatchAsyncTests", + dependencies: ["DispatchAsync"] + ), + ] +) From d90e8b840590fcf7aa6af5a79fdccd43b44f72d5 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:36:44 -0600 Subject: [PATCH 03/53] feat: Implement DispatchQueue using Swift Concurrency. --- Sources/DispatchAsync/DispatchQueue.swift | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 Sources/DispatchAsync/DispatchQueue.swift diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift new file mode 100644 index 0000000..abec425 --- /dev/null +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// `DispatchQueue` is a drop-in replacement for the `DispatchQueue` implemented +/// in Grand Central Dispatch. However, this class uses Swift Concurrency, instead of low-level threading API's. +/// +/// The primary goal of this implementation is to enable WASM support for Dispatch. +/// +/// Refer to documentation for the original [DispatchQueue](https://developer.apple.com/documentation/dispatch/dispatchqueue) +/// for more details, +@available(macOS 10.15, *) +public class DispatchQueue: @unchecked Sendable { + public static let main = DispatchQueue(isMain: true) + + private static let _global = DispatchQueue() + public static func global() -> DispatchQueue { + Self._global + } + + public enum Attributes { + case concurrent + } + + private let targetQueue: DispatchQueue? + + /// Indicates whether calling context is running from the main DispatchQueue instance, or some other DispatchQueue instance. + @TaskLocal public static var isMain = false + + /// This is set during the initialization of the DispatchQueue, and controls whether `async` calls run on MainActor or not + private let isMain: Bool + private let label: String? + private let attributes: DispatchQueue.Attributes? + + public convenience init( + label: String? = nil, + attributes: DispatchQueue.Attributes? = nil, + target: DispatchQueue? = nil + ) { + self.init(isMain: false, label: label, attributes: attributes, target: target) + } + + private init( + isMain: Bool, + label: String? = nil, + attributes: DispatchQueue.Attributes? = nil, + target: DispatchQueue? = nil + ) { + self.isMain = isMain + self.label = label + self.attributes = attributes + self.targetQueue = target + } + + public func async( + execute work: @escaping @Sendable @convention(block) () -> Void + ) { + if let targetQueue, targetQueue !== self { + // Recursively call this function on the target queue + // until we reach a nil queue, or this queue. + targetQueue.async(execute: work) + } else { + if isMain { + Task { @MainActor [work] in + DispatchQueue.$isMain.withValue(true) { + work() + } + } + } else { + Task { + work() + } + } + } + } +} From 2ecbb15c76249244abde9d6bbc486b4ea2177062 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:36:56 -0600 Subject: [PATCH 04/53] feat: Implement DispatchGroup using Swift Concurrency. --- Sources/DispatchAsync/DispatchGroup.swift | 129 ++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 Sources/DispatchAsync/DispatchGroup.swift diff --git a/Sources/DispatchAsync/DispatchGroup.swift b/Sources/DispatchAsync/DispatchGroup.swift new file mode 100644 index 0000000..fd039f1 --- /dev/null +++ b/Sources/DispatchAsync/DispatchGroup.swift @@ -0,0 +1,129 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// MARK: - Public Interface for Non-Async Usage - + +/// `DispatchGroup` is a drop-in replacement for the `DispatchGroup` implemented +/// in Grand Central Dispatch. However, this class uses Swift Concurrency, instead of low-level threading API's. +/// +/// The primary goal of this implementation is to enable WASM support for Dispatch. +/// +/// Refer to documentation for the original [DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup) +/// for more details, +@available(macOS 10.15, *) +public class DispatchGroup: @unchecked Sendable { + /// Used to ensure FIFO access to the enter and leave calls + @globalActor + private actor DispatchGroupEntryActor: GlobalActor { + static let shared = DispatchGroupEntryActor() + } + + private let group = AsyncGroup() + + public func enter() { + Task { @DispatchGroupEntryActor [] in + // ^--- Ensures serial FIFO entrance/exit into the group + await group.enter() + } + } + + public func leave() { + Task { @DispatchGroupEntryActor [] in + // ^--- Ensures serial FIFO entrance/exit into the group + await group.leave() + } + } + + public func notify(queue: DispatchQueue, execute work: @escaping @Sendable @convention(block) () -> Void) { + Task { @DispatchGroupEntryActor [] in + // ^--- Ensures serial FIFO entrance/exit into the group + await group.notify { + await withCheckedContinuation { continuation in + queue.async { + work() + continuation.resume() + } + } + } + } + } + + func wait() async { + await group.wait() + } + + public init() {} +} + +// MARK: - Private Interface for Async Usage - + +@available(macOS 10.15, *) +fileprivate actor AsyncGroup { + private var taskCount = 0 + private var continuation: CheckedContinuation? + private var isWaiting = false + private var notifyHandlers: [@Sendable () async -> Void] = [] + + func enter() { + taskCount += 1 + } + + func leave() { + defer { + checkCompletion() + } + guard taskCount > 0 else { + assertionFailure("leave() called more times than enter()") + return + } + taskCount -= 1 + } + + func notify(handler: @escaping @Sendable () async -> Void) { + notifyHandlers.append(handler) + checkCompletion() + } + + func wait() async { + if taskCount <= 0 { + return + } + + isWaiting = true + + await withCheckedContinuation { (continuation: CheckedContinuation) in + self.continuation = continuation + checkCompletion() + } + } + + private func checkCompletion() { + if taskCount <= 0 { + if isWaiting { + continuation?.resume() + continuation = nil + isWaiting = false + } + + if !notifyHandlers.isEmpty { + let handlers = notifyHandlers + notifyHandlers.removeAll() + + for handler in handlers { + Task { + await handler() + } + } + } + } + } +} From 5108dec3122d57ffe7a379f3edacada71006a756 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:37:09 -0600 Subject: [PATCH 05/53] feat: Implement DispatchSemaphore using Swift Concurrency. --- Sources/DispatchAsync/AsyncSemaphore.swift | 54 ++++++++++++++ Sources/DispatchAsync/DispatchSemaphore.swift | 72 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 Sources/DispatchAsync/AsyncSemaphore.swift create mode 100644 Sources/DispatchAsync/DispatchSemaphore.swift diff --git a/Sources/DispatchAsync/AsyncSemaphore.swift b/Sources/DispatchAsync/AsyncSemaphore.swift new file mode 100644 index 0000000..cb5769c --- /dev/null +++ b/Sources/DispatchAsync/AsyncSemaphore.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Provides a semaphore implantation in `async` context, with a safe wait method. Provides easy safer replacement +/// for DispatchSemaphore usage. +@available(macOS 10.15, *) +actor AsyncSemaphore { + private var value: Int + private var waiters: Array> = [] + + init(value: Int = 1) { + self.value = value + } + + func wait() async { + value -= 1 + if value >= 0 { return } + await withCheckedContinuation { + waiters.append($0) + } + } + + func signal() { + self.value += 1 + + guard !waiters.isEmpty else { return } + let first = waiters.removeFirst() + first.resume() + } +} + +@available(macOS 10.15, *) +extension AsyncSemaphore { + func withLock(_ closure: () async throws -> T) async rethrows -> T { + await wait() + defer { signal() } + return try await closure() + } + + func withLockVoid(_ closure: () async throws -> Void) async rethrows { + await wait() + defer { signal() } + try await closure() + } +} diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift new file mode 100644 index 0000000..c9538bf --- /dev/null +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// This implementation assumes the single-threaded +// environment that swift wasm executables typically run in. +// +// It is not appropriate for true multi-threaded environments. +// +// For safety, this class is only defined for WASI platforms. +// +// +#if os(WASI) + +/// DispatchSemaphore is not safe to use for most wasm executables. +/// +/// Most wasm executables are single-threaded. Calling DispatchSemaphore.wait +/// when it's value is 0 or lower would be likely cause a frozen main thread, +/// because that would block the calling thread. And there is usually +/// only one thread in the wasm world (right now). +/// +/// For now, we guard against that case with both compile-time deprecation +/// pointing to the much safer ``AsyncSemaphore``, and also at run-time with +/// assertions. +/// +/// ``AsyncSemaphore`` provides full functionality, but only exposes +/// Swift Concurrency api's with a safe async wait function. +@available( + *, + deprecated, + renamed: "AsyncSemaphore", + message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." +) +@available(macOS 10.15, *) +public class DispatchSemaphore: @unchecked Sendable { + public var value: Int + + public init(value: Int) { + self.value = value + } + + @discardableResult + public func signal() -> Int { + MainActor.assertIsolated() + value += 1 + return value + } + + public func wait() { + // NOTE: wasm is currently mostly single threaded. + // And we don't have a Thread.sleep API yet. + // So + MainActor.assertIsolated() + assert(value > 0, "DispatchSemaphore is currently only designed for single-threaded use.") + value -= 1 + } +} + +#else + +@available(macOS 10.15, *) +typealias DispatchSemaphore = AsyncSemaphore + +#endif // #if os(WASI) From d98950ccc43401272f857dec0582cf8f2bbac516 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:38:32 -0600 Subject: [PATCH 06/53] feat: Implement DispatchTime using Swift Concurrency. --- Sources/DispatchAsync/DispatchTime.swift | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 Sources/DispatchAsync/DispatchTime.swift diff --git a/Sources/DispatchAsync/DispatchTime.swift b/Sources/DispatchAsync/DispatchTime.swift new file mode 100644 index 0000000..969570f --- /dev/null +++ b/Sources/DispatchAsync/DispatchTime.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(macOS 13, *) +public typealias DispatchTime = ContinuousClock.Instant + +/// The very first time someone tries to reference a `uptimeNanoseconds` or a similar +/// function that references a beginning point, this variable will be initialized as a beginning +/// reference point. This guarantees that all calls to `uptimeNanoseconds` or similar +/// will be 0 or greater. +/// +/// By design, it is not possible to related `ContinuousClock.Instant` to +/// `ProcessInfo.processInfo.systemUptime`, and even if one devised such +/// a mechanism, it would open the door for fingerprinting. It's best to let the concept +/// of uptime be relative to previous uptime calls. +@available(macOS 13, *) +private let uptimeBeginning: DispatchTime = DispatchTime.now() + +@available(macOS 13, *) +extension DispatchTime { + public static func now() -> DispatchTime { + now + } + + public var uptimeNanoseconds: UInt64 { + let beginning = uptimeBeginning + let rightNow = DispatchTime.now() + let uptimeDuration: Int64 = beginning.duration(to: rightNow).nanosecondsClamped + guard uptimeDuration >= 0 else { + assertionFailure("It shouldn't be possible to get a negative duration since uptimeBeginning.") + return 0 + } + return UInt64(uptimeDuration) + } +} + +// NOTE: The following was copied from swift-nio/Source/NIOCore/TimeAmount+Duration on June 27, 2025 +// It was copied rather than brought via dependencies to avoid introducing +// a dependency on swift-nio for such a small piece of code. +// +// This library will need to have no depedendencies to be able to be integrated into GCD. +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +extension Swift.Duration { + /// The duration represented as nanoseconds, clamped to maximum expressible value. + fileprivate var nanosecondsClamped: Int64 { + let components = self.components + + let secondsComponentNanos = components.seconds.multipliedReportingOverflow(by: 1_000_000_000) + let attosCompononentNanos = components.attoseconds / 1_000_000_000 + let combinedNanos = secondsComponentNanos.partialValue.addingReportingOverflow(attosCompononentNanos) + + guard + !secondsComponentNanos.overflow, + !combinedNanos.overflow + else { + return .max + } + + return combinedNanos.partialValue + } +} From 64036b926dd23a96ec8af3e9df9ef09497d753b5 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:38:44 -0600 Subject: [PATCH 07/53] feat: Implement DispatchTimeInterval using Swift Concurrency. --- .../DispatchAsync/DispatchTimeInterval.swift | 67 +++++++++++++++++++ Sources/DispatchAsync/PackageConstants.swift | 15 +++++ 2 files changed, 82 insertions(+) create mode 100644 Sources/DispatchAsync/DispatchTimeInterval.swift create mode 100644 Sources/DispatchAsync/PackageConstants.swift diff --git a/Sources/DispatchAsync/DispatchTimeInterval.swift b/Sources/DispatchAsync/DispatchTimeInterval.swift new file mode 100644 index 0000000..ed7cecd --- /dev/null +++ b/Sources/DispatchAsync/DispatchTimeInterval.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// NOTE: This is an excerpt from libDispatch, see +/// https://github.com/swiftlang/swift-corelibs-libdispatch/blob/main/src/swift/Time.swift#L168 +/// +/// Represents a time interval that can be used as an offset from a `DispatchTime` +/// or `DispatchWallTime`. +/// +/// For example: +/// let inOneSecond = DispatchTime.now() + DispatchTimeInterval.seconds(1) +/// +/// If the requested time interval is larger then the internal representation +/// permits, the result of adding it to a `DispatchTime` or `DispatchWallTime` +/// is `DispatchTime.distantFuture` and `DispatchWallTime.distantFuture` +/// respectively. Such time intervals compare as equal: +/// +/// let t1 = DispatchTimeInterval.seconds(Int.max) +/// let t2 = DispatchTimeInterval.milliseconds(Int.max) +/// let result = t1 == t2 // true +public enum DispatchTimeInterval: Equatable, Sendable { + case seconds(Int) + case milliseconds(Int) + case microseconds(Int) + case nanoseconds(Int) + case never + + internal var rawValue: Int64 { + switch self { + case .seconds(let s): return clampedInt64Product(Int64(s), Int64(NSEC_PER_SEC)) + case .milliseconds(let ms): return clampedInt64Product(Int64(ms), Int64(NSEC_PER_MSEC)) + case .microseconds(let us): return clampedInt64Product(Int64(us), Int64(NSEC_PER_USEC)) + case .nanoseconds(let ns): return Int64(ns) + case .never: return Int64.max + } + } + + public static func ==(lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool { + switch (lhs, rhs) { + case (.never, .never): return true + case (.never, _): return false + case (_, .never): return false + default: return lhs.rawValue == rhs.rawValue + } + } + + // Returns m1 * m2, clamped to the range [Int64.min, Int64.max]. + // Because of the way this function is used, we can always assume + // that m2 > 0. + private func clampedInt64Product(_ m1: Int64, _ m2: Int64) -> Int64 { + assert(m2 > 0, "multiplier must be positive") + let (result, overflow) = m1.multipliedReportingOverflow(by: m2) + if overflow { + return m1 > 0 ? Int64.max : Int64.min + } + return result + } +} diff --git a/Sources/DispatchAsync/PackageConstants.swift b/Sources/DispatchAsync/PackageConstants.swift new file mode 100644 index 0000000..492f23a --- /dev/null +++ b/Sources/DispatchAsync/PackageConstants.swift @@ -0,0 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +package let NSEC_PER_SEC: UInt64 = 1_000_000_000 +package let NSEC_PER_MSEC: UInt64 = 1_000_000 +package let NSEC_PER_USEC: UInt64 = 1_000 From 40b798d0549be897977b2da9b565a982c5777aaa Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:40:02 -0600 Subject: [PATCH 08/53] chore: Add some basic testing. --- .../DispatchGroupTests.swift | 121 ++++++++++++++++++ .../DispatchQueueTests.swift | 46 +++++++ .../DispatchSemaphoreTests.swift | 53 ++++++++ .../DispatchTimeTests.swift | 21 +++ 4 files changed, 241 insertions(+) create mode 100644 Tests/DispatchAsyncTests/DispatchGroupTests.swift create mode 100644 Tests/DispatchAsyncTests/DispatchQueueTests.swift create mode 100644 Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift create mode 100644 Tests/DispatchAsyncTests/DispatchTimeTests.swift diff --git a/Tests/DispatchAsyncTests/DispatchGroupTests.swift b/Tests/DispatchAsyncTests/DispatchGroupTests.swift new file mode 100644 index 0000000..29f00c2 --- /dev/null +++ b/Tests/DispatchAsyncTests/DispatchGroupTests.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +@testable import DispatchAsync + +@Test +func dispatchGroupOrderCleanliness() async throws { + // Repeating this 100 times to help rule out + // edge cases that only show up some of the time + for index in 0 ..< 100 { + Task { + actor Result { + private(set) var value = "" + + func append(value: String) { + self.value.append(value) + } + } + + let result = Result() + + let group = DispatchGroup() + await result.append(value: "|πŸ”΅\(index)") + + group.enter() + Task { + await result.append(value: "🟣/") + group.leave() + } + + group.enter() + Task { + await result.append(value: "🟣^") + group.leave() + } + + group.enter() + Task { + await result.append(value: "🟣\\") + group.leave() + } + + await withCheckedContinuation { continuation in + group.notify(queue: .main) { + Task { + await result.append(value: "🟒\(index)=") + continuation.resume() + } + } + } + + let finalValue = await result.value + + /// NOTE: If you need to visually debug issues, you can uncomment + /// the following to watch a visual representation of the group ordering. + /// + /// In general, you'll see something like the following printed over and over + /// to the console: + /// + /// ``` + /// |πŸ”΅42🟣/🟣^🟣\🟒42= + /// ``` + /// + /// What you should observe: + /// + /// - The index number be the same at the beginning and end of each line, and it + /// should always increment by one. + /// - The πŸ”΅ should always be first, and the 🟒 should always be last for each line. + /// - There should always be 3 🟣's in between the πŸ”΅ and 🟒. + /// - The ordering of the 🟣 can be random, and that is fine. + /// + /// For example, for of the following are valid outputs: + /// + /// ``` + /// // GOOD + /// |πŸ”΅42🟣/🟣^🟣\🟒42= + /// ``` + /// + /// ``` + /// // GOOD + /// |πŸ”΅42🟣/🟣\🟣^🟒42= + /// ``` + /// + /// But the following would not be valid: + /// + /// ``` + /// // BAD! + /// |πŸ”΅43🟣/🟣^🟣\🟒43= + /// |πŸ”΅42🟣/🟣^🟣\🟒42= + /// |πŸ”΅44🟣/🟣^🟣\🟒44= + /// ``` + /// + /// ``` + /// // BAD! + /// |πŸ”΅42🟣/🟣^🟒42🟣\= + /// ``` + /// + + // Uncomment to use troubleshooting method above: + // print(finalValue) + + #expect(finalValue.prefix(1) == "|") + #expect(finalValue.count { $0 == "🟣"} == 3) + #expect(finalValue.count { $0 == "🟒"} == 1) + #expect(finalValue.lastIndex(of: "🟣")! < finalValue.firstIndex(of: "🟒")!) + #expect(finalValue.suffix(1) == "=") + } + } +} + + diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift new file mode 100644 index 0000000..d73b934 --- /dev/null +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +@testable import DispatchAsync + +#if !os(WASI) +import class Foundation.Thread +#endif + +@Test +func testBasicDispatchQueueMain() async throws { + let asyncValue = await withCheckedContinuation { continuation in + DispatchQueue.main.async { + // Main queue should be on main thread. + #if !os(WASI) + #expect(Thread.isMainThread) + #endif + continuation.resume(returning: true) + } + } + #expect(asyncValue == true) +} + +@Test +func testBasicDispatchQueueGlobal() async throws { + let asyncValue = await withCheckedContinuation { continuation in + DispatchQueue.global().async { + // Global queue should NOT be on main thread. + #if !os(WASI) + #expect(!Thread.isMainThread) + #endif + continuation.resume(returning: true) + } + } + #expect(asyncValue == true) +} diff --git a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift new file mode 100644 index 0000000..1cff42e --- /dev/null +++ b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +@testable import DispatchAsync + +nonisolated(unsafe) private var sharedPoolCompletionCount = 0 + +@Test func basicDispatchSemaphoreTest() async throws { + let totalConcurrentPools = 10 + + let semaphore = DispatchSemaphore(value: 1) + + await withTaskGroup(of: Void.self) { group in + for _ in 0 ..< totalConcurrentPools { + group.addTask { + // Wait for any other pools currently holding the semaphore + await semaphore.wait() + + // Only one task should mutate counter at a time + // + // If there are issues with the semaphore, then + // we would expect to grab incorrect values here occasionally, + // which would result in an incorrect final completion count. + // + let existingPoolCompletionCount = sharedPoolCompletionCount + + // Add artificial delay to amplify race conditions + // Pools started shortly after this "semaphore-locked" + // pool starts will run before this line, unless + // this pool contains a valid lock. + try? await Task.sleep(nanoseconds: 100) + + sharedPoolCompletionCount = existingPoolCompletionCount + 1 + + // When we exit this flow, release our hold on the semaphore + await semaphore.signal() + } + } + } + + // After all tasks are done, counter should be 10 + #expect(sharedPoolCompletionCount == totalConcurrentPools) +} diff --git a/Tests/DispatchAsyncTests/DispatchTimeTests.swift b/Tests/DispatchAsyncTests/DispatchTimeTests.swift new file mode 100644 index 0000000..3d7b8fb --- /dev/null +++ b/Tests/DispatchAsyncTests/DispatchTimeTests.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +@testable import DispatchAsync + +@Test +func testDispatchTimeContinousClockBasics() async throws { + let a = DispatchTime.now().uptimeNanoseconds + let b = DispatchTime.now().uptimeNanoseconds + #expect(a <= b) +} From da07ce22043bb1435b03437a0856b212d6981091 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 13:40:14 -0600 Subject: [PATCH 09/53] chore: Update Readme. --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4d71d3..09b803b 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# dispatch-async \ No newline at end of file +# dispatch-async + +## ⚠️ WARNING - This is an πŸ§ͺexperimentalπŸ§ͺ repository and should not be adopted at large. + +DispatchAsync is a temporary experimental repository aimed at implementing missing Dispatch support in the SwiftWasm toolchain. +Currently, [SwiftWasm doesn't include Dispatch](https://book.swiftwasm.org/getting-started/porting.html#swift-foundation-and-dispatch). +But, SwiftWasm does support Swift Concurrency. DispatchAsync implements a number of common Dispatch API's using Swift Concurrency +under the hood. + +Dispatch Async does not provide blocking API's such as `DispatchQueue.sync`, primarily due to the intentional lack of blocking +API's in Swift Concurrency. + +# Toolchain Adoption Plans + +DispatchAsync is not meant for consumption abroad directly as a new Swift Module. Rather, the intention is to provide eventual integration +as a drop-in replacement for Dispatch when compiling to Wasm. + +There are a few paths to adoption into the Swift toolchain + +- DispatchAsync can be emplaced inside the [libDispatch repository](https://github.com/swiftlang/swift-corelibs-libdispatch), and compiled +into the toolchain only for wasm targets. +- DispatchAsync can be consumed in place of libDispatch when building the Swift toolchain. + +Ideally, with either approach, this repository would transfer ownership to the swiftlang organization. + +# DispatchSemaphore Limitations + +The current implementation of `DispatchSemaphore` has some limitations. Blocking threads goes against the design goals of Swift Concurrency. +The `wait` function on `DispatchSemaphore` goes against this goal. Furthermore, most wasm targets run on a single thread from the web +browser, so any time the `wait` function ends up blocking the calling thread, it would almost certainly hang a single-threaded wasm +executable. + +To navigate these issues, there are some limitations: + +- For wasm compilation targets, `DispatchSemaphore` assumes single-threaded execution, and lacks various safeguards that would otherwise +be needed for multi-threaded execution. This makes the implementation much easier. +- For wasm targets, calls to `signal` and `wait` must be balanced. An assertion triggers if `wait` is called more times than `signal`. +- DispatchSemaphore is deprecated for wasm targets, and AsyncSemaphore is encouraged as the replacement. +- For non-wasm targets, DispatchSemaphore is simply a typealias for `AsyncSemaphore`, and provides only a non-blocking async `wait` +function. This reduces potential issues that can arise from wait being a thread-blocking function. From aed4dd6cca11e30bde88ac00755a2be037c4ef56 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 14:27:06 -0600 Subject: [PATCH 10/53] ci: Add pull request CI workflows. --- .github/workflows/pull_request.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/pull_request.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..d28e7b0 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,22 @@ +name: Pull request + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "Swift.org" + + tests: + name: tests + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + + wasm-sdk: + name: WebAssembly SDK + # TODO: Switch to this line after https://github.com/apple/swift-nio/pull/3159/ is merged + # uses: apple/swift-nio/.github/workflows/wasm_sdk.yml@main + uses: kateinoigakukun/swift-nio/.github/workflows/wasm_sdk.yml@katei/add-wasm-ci From f8d723e1ef22766ddfc7dbf6e5d74e913e81549f Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 15:01:54 -0600 Subject: [PATCH 11/53] chore: Silence some lint that is intentionally written this way to match constansts in libDispatch. --- Sources/DispatchAsync/PackageConstants.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/DispatchAsync/PackageConstants.swift b/Sources/DispatchAsync/PackageConstants.swift index 492f23a..e29daf8 100644 --- a/Sources/DispatchAsync/PackageConstants.swift +++ b/Sources/DispatchAsync/PackageConstants.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +// swiftlint:disable identifier_name package let NSEC_PER_SEC: UInt64 = 1_000_000_000 package let NSEC_PER_MSEC: UInt64 = 1_000_000 package let NSEC_PER_USEC: UInt64 = 1_000 +// swiftlint:enable identifier_name From dae1d1d7f96ae7be77e0345c527fcaa0f0d629fb Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 15:35:34 -0600 Subject: [PATCH 12/53] ci: Disable api breakage check for now. --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d28e7b0..8696efb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,6 +10,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift.org" + api_breakage_check_enabled: false tests: name: tests From e2e3b8300b09e413b6a4e02346c2838721462d2d Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 15:35:56 -0600 Subject: [PATCH 13/53] ci: Don't test swift versions before 6.1 --- .github/workflows/pull_request.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8696efb..2ae2f1a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,6 +15,10 @@ jobs: tests: name: tests uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main + with: + macos_xcode_versions: "[\"16.3\"]" + linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"6.0\"}]" + windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"6.0\"}]" wasm-sdk: name: WebAssembly SDK From b5abf14c46efee77650071dc75d78ed16d0b5519 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 15:45:05 -0600 Subject: [PATCH 14/53] chore: Update file headers --- Sources/DispatchAsync/AsyncSemaphore.swift | 10 ++++++---- Sources/DispatchAsync/DispatchGroup.swift | 10 ++++++---- Sources/DispatchAsync/DispatchQueue.swift | 10 ++++++---- Sources/DispatchAsync/DispatchSemaphore.swift | 10 ++++++---- Sources/DispatchAsync/DispatchTime.swift | 10 ++++++---- Sources/DispatchAsync/DispatchTimeInterval.swift | 10 ++++++---- Sources/DispatchAsync/PackageConstants.swift | 10 ++++++---- Tests/DispatchAsyncTests/DispatchGroupTests.swift | 10 ++++++---- Tests/DispatchAsyncTests/DispatchQueueTests.swift | 10 ++++++---- Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift | 10 ++++++---- Tests/DispatchAsyncTests/DispatchTimeTests.swift | 10 ++++++---- 11 files changed, 66 insertions(+), 44 deletions(-) diff --git a/Sources/DispatchAsync/AsyncSemaphore.swift b/Sources/DispatchAsync/AsyncSemaphore.swift index cb5769c..2e2bee5 100644 --- a/Sources/DispatchAsync/AsyncSemaphore.swift +++ b/Sources/DispatchAsync/AsyncSemaphore.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Sources/DispatchAsync/DispatchGroup.swift b/Sources/DispatchAsync/DispatchGroup.swift index fd039f1..2251558 100644 --- a/Sources/DispatchAsync/DispatchGroup.swift +++ b/Sources/DispatchAsync/DispatchGroup.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift index abec425..fc9a6f6 100644 --- a/Sources/DispatchAsync/DispatchQueue.swift +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index c9538bf..863eee7 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Sources/DispatchAsync/DispatchTime.swift b/Sources/DispatchAsync/DispatchTime.swift index 969570f..c9c1935 100644 --- a/Sources/DispatchAsync/DispatchTime.swift +++ b/Sources/DispatchAsync/DispatchTime.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Sources/DispatchAsync/DispatchTimeInterval.swift b/Sources/DispatchAsync/DispatchTimeInterval.swift index ed7cecd..5a2ed41 100644 --- a/Sources/DispatchAsync/DispatchTimeInterval.swift +++ b/Sources/DispatchAsync/DispatchTimeInterval.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Sources/DispatchAsync/PackageConstants.swift b/Sources/DispatchAsync/PackageConstants.swift index e29daf8..f7de7ff 100644 --- a/Sources/DispatchAsync/PackageConstants.swift +++ b/Sources/DispatchAsync/PackageConstants.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Tests/DispatchAsyncTests/DispatchGroupTests.swift b/Tests/DispatchAsyncTests/DispatchGroupTests.swift index 29f00c2..e743655 100644 --- a/Tests/DispatchAsyncTests/DispatchGroupTests.swift +++ b/Tests/DispatchAsyncTests/DispatchGroupTests.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index d73b934..657cbd0 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift index 1cff42e..73bf00e 100644 --- a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift +++ b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// diff --git a/Tests/DispatchAsyncTests/DispatchTimeTests.swift b/Tests/DispatchAsyncTests/DispatchTimeTests.swift index 3d7b8fb..42f526d 100644 --- a/Tests/DispatchAsyncTests/DispatchTimeTests.swift +++ b/Tests/DispatchAsyncTests/DispatchTimeTests.swift @@ -2,11 +2,13 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 // -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// From ec5c7da6a4239264cf4bfa530fc8f5eae9fed2ef Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 15:52:16 -0600 Subject: [PATCH 15/53] chore: Changing wording in readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09b803b..898693e 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Ideally, with either approach, this repository would transfer ownership to the s The current implementation of `DispatchSemaphore` has some limitations. Blocking threads goes against the design goals of Swift Concurrency. The `wait` function on `DispatchSemaphore` goes against this goal. Furthermore, most wasm targets run on a single thread from the web -browser, so any time the `wait` function ends up blocking the calling thread, it would almost certainly hang a single-threaded wasm +browser, so any time the `wait` function ends up blocking the calling thread, it would almost certainly freeze the single-threaded wasm executable. To navigate these issues, there are some limitations: From c87843c39642053a1353268bdffb197b679686f2 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:00:44 -0600 Subject: [PATCH 16/53] chore: Ignore missing license header in Package.swift file. --- .licenseignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .licenseignore diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 0000000..c73cf0c --- /dev/null +++ b/.licenseignore @@ -0,0 +1 @@ +Package.swift \ No newline at end of file From 1a3e651c8ad99f003acbb279d47f6ec90e07ed00 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:03:38 -0600 Subject: [PATCH 17/53] chore: Clean up lint a different way. --- Sources/DispatchAsync/DispatchTimeInterval.swift | 6 +++--- Sources/DispatchAsync/PackageConstants.swift | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Sources/DispatchAsync/DispatchTimeInterval.swift b/Sources/DispatchAsync/DispatchTimeInterval.swift index 5a2ed41..aa0f38f 100644 --- a/Sources/DispatchAsync/DispatchTimeInterval.swift +++ b/Sources/DispatchAsync/DispatchTimeInterval.swift @@ -38,9 +38,9 @@ public enum DispatchTimeInterval: Equatable, Sendable { internal var rawValue: Int64 { switch self { - case .seconds(let s): return clampedInt64Product(Int64(s), Int64(NSEC_PER_SEC)) - case .milliseconds(let ms): return clampedInt64Product(Int64(ms), Int64(NSEC_PER_MSEC)) - case .microseconds(let us): return clampedInt64Product(Int64(us), Int64(NSEC_PER_USEC)) + case .seconds(let s): return clampedInt64Product(Int64(s), Int64(kNanosecondsPerSecond)) + case .milliseconds(let ms): return clampedInt64Product(Int64(ms), Int64(kNanosecondsPerMillisecond)) + case .microseconds(let us): return clampedInt64Product(Int64(us), Int64(kNanoSecondsPerMicrosecond)) case .nanoseconds(let ns): return Int64(ns) case .never: return Int64.max } diff --git a/Sources/DispatchAsync/PackageConstants.swift b/Sources/DispatchAsync/PackageConstants.swift index f7de7ff..c781ab3 100644 --- a/Sources/DispatchAsync/PackageConstants.swift +++ b/Sources/DispatchAsync/PackageConstants.swift @@ -12,8 +12,6 @@ // //===----------------------------------------------------------------------===// -// swiftlint:disable identifier_name -package let NSEC_PER_SEC: UInt64 = 1_000_000_000 -package let NSEC_PER_MSEC: UInt64 = 1_000_000 -package let NSEC_PER_USEC: UInt64 = 1_000 -// swiftlint:enable identifier_name +package let kNanosecondsPerSecond: UInt64 = 1_000_000_000 +package let kNanosecondsPerMillisecond: UInt64 = 1_000_000 +package let kNanoSecondsPerMicrosecond: UInt64 = 1_000 From 91599e94291ad5ece12a3c1a1581804d02b3abdb Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:22:10 -0600 Subject: [PATCH 18/53] ci: update test targets --- .github/workflows/pull_request.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2ae2f1a..e21c126 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -16,9 +16,9 @@ jobs: name: tests uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - macos_xcode_versions: "[\"16.3\"]" - linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"6.0\"}]" - windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"6.0\"}]" + enable_macos_checks: false + linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10.1\"}, {\"swift_version\": \"6.0\"}]" + enable_windows_checks: false wasm-sdk: name: WebAssembly SDK From f78c0d542cdda4aa18c127a038d944113ced66d3 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:22:21 -0600 Subject: [PATCH 19/53] ci: Add wasm sdk installation script. --- scripts/install_wasm_sdk.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 scripts/install_wasm_sdk.sh diff --git a/scripts/install_wasm_sdk.sh b/scripts/install_wasm_sdk.sh new file mode 100755 index 0000000..0cd3c18 --- /dev/null +++ b/scripts/install_wasm_sdk.sh @@ -0,0 +1,23 @@ +#!/bin/bash +##===----------------------------------------------------------------------===// +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift.org project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of Swift.org project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===// + +# TODO: Remove this file once there is a valid reference available in github from +# this PR: https://github.com/apple/swift-nio/pull/3159 + +set -euo pipefail + +version="$(swiftc --version | head -n1)" +tag="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$version" '.[$v] | .[-1]')" +curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$tag.json" | jq -r '.["swift-sdks"]["wasm32-unknown-wasi"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x From e3af2d212577115a41c7ef59f5b3cef2be2983b6 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:23:41 -0600 Subject: [PATCH 20/53] chore: Fix license header format in bash script. --- scripts/install_wasm_sdk.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install_wasm_sdk.sh b/scripts/install_wasm_sdk.sh index 0cd3c18..b795fc3 100755 --- a/scripts/install_wasm_sdk.sh +++ b/scripts/install_wasm_sdk.sh @@ -1,5 +1,5 @@ #!/bin/bash -##===----------------------------------------------------------------------===// +##===----------------------------------------------------------------------===## ## ## This source file is part of the Swift.org open source project ## @@ -11,7 +11,7 @@ ## ## SPDX-License-Identifier: Apache-2.0 ## -##===----------------------------------------------------------------------===// +##===----------------------------------------------------------------------===## # TODO: Remove this file once there is a valid reference available in github from # this PR: https://github.com/apple/swift-nio/pull/3159 From 1e8b32234cbe7f6ebe58075abdf45a5b69b7b28d Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:35:37 -0600 Subject: [PATCH 21/53] chore: Update swift-format rules. --- .swift-format | 75 +++++++++++++++++++ Package.swift | 2 +- Sources/DispatchAsync/AsyncSemaphore.swift | 2 +- Sources/DispatchAsync/DispatchSemaphore.swift | 6 +- .../DispatchAsync/DispatchTimeInterval.swift | 2 +- Sources/DispatchAsync/PackageConstants.swift | 2 +- .../DispatchGroupTests.swift | 7 +- .../DispatchQueueTests.swift | 1 + .../DispatchSemaphoreTests.swift | 1 + .../DispatchTimeTests.swift | 1 + 10 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 .swift-format diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..e6d438f --- /dev/null +++ b/.swift-format @@ -0,0 +1,75 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "indentation" : { + "spaces" : 4 + }, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineBreakBetweenDeclarationAttributes" : false, + "lineLength" : 140, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "reflowMultilineStringLiterals" : "never", + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "AvoidRetroactiveConformances" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyLinesOpeningClosingBraces" : false, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : true, + "spacesBeforeEndOfLineComments" : 1, + "tabWidth" : 8, + "version" : 1 +} diff --git a/Package.swift b/Package.swift index 3817f11..943c8e3 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( products: [ .library( name: "DispatchAsync", - targets: ["DispatchAsync"]), + targets: ["DispatchAsync"]) ], targets: [ .target( diff --git a/Sources/DispatchAsync/AsyncSemaphore.swift b/Sources/DispatchAsync/AsyncSemaphore.swift index 2e2bee5..cbf60b3 100644 --- a/Sources/DispatchAsync/AsyncSemaphore.swift +++ b/Sources/DispatchAsync/AsyncSemaphore.swift @@ -17,7 +17,7 @@ @available(macOS 10.15, *) actor AsyncSemaphore { private var value: Int - private var waiters: Array> = [] + private var waiters: [CheckedContinuation] = [] init(value: Int = 1) { self.value = value diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index 863eee7..e96d6db 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -37,9 +37,9 @@ /// Swift Concurrency api's with a safe async wait function. @available( *, - deprecated, - renamed: "AsyncSemaphore", - message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." + deprecated, + renamed: "AsyncSemaphore", + message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." ) @available(macOS 10.15, *) public class DispatchSemaphore: @unchecked Sendable { diff --git a/Sources/DispatchAsync/DispatchTimeInterval.swift b/Sources/DispatchAsync/DispatchTimeInterval.swift index aa0f38f..469d691 100644 --- a/Sources/DispatchAsync/DispatchTimeInterval.swift +++ b/Sources/DispatchAsync/DispatchTimeInterval.swift @@ -46,7 +46,7 @@ public enum DispatchTimeInterval: Equatable, Sendable { } } - public static func ==(lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool { + public static func == (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool { switch (lhs, rhs) { case (.never, .never): return true case (.never, _): return false diff --git a/Sources/DispatchAsync/PackageConstants.swift b/Sources/DispatchAsync/PackageConstants.swift index c781ab3..762abf1 100644 --- a/Sources/DispatchAsync/PackageConstants.swift +++ b/Sources/DispatchAsync/PackageConstants.swift @@ -12,6 +12,6 @@ // //===----------------------------------------------------------------------===// -package let kNanosecondsPerSecond: UInt64 = 1_000_000_000 +package let kNanosecondsPerSecond: UInt64 = 1_000_000_000 package let kNanosecondsPerMillisecond: UInt64 = 1_000_000 package let kNanoSecondsPerMicrosecond: UInt64 = 1_000 diff --git a/Tests/DispatchAsyncTests/DispatchGroupTests.swift b/Tests/DispatchAsyncTests/DispatchGroupTests.swift index e743655..c1e622b 100644 --- a/Tests/DispatchAsyncTests/DispatchGroupTests.swift +++ b/Tests/DispatchAsyncTests/DispatchGroupTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Testing + @testable import DispatchAsync @Test @@ -112,12 +113,10 @@ func dispatchGroupOrderCleanliness() async throws { // print(finalValue) #expect(finalValue.prefix(1) == "|") - #expect(finalValue.count { $0 == "🟣"} == 3) - #expect(finalValue.count { $0 == "🟒"} == 1) + #expect(finalValue.count { $0 == "🟣" } == 3) + #expect(finalValue.count { $0 == "🟒" } == 1) #expect(finalValue.lastIndex(of: "🟣")! < finalValue.firstIndex(of: "🟒")!) #expect(finalValue.suffix(1) == "=") } } } - - diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index 657cbd0..a36c3c3 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Testing + @testable import DispatchAsync #if !os(WASI) diff --git a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift index 73bf00e..76ed242 100644 --- a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift +++ b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Testing + @testable import DispatchAsync nonisolated(unsafe) private var sharedPoolCompletionCount = 0 diff --git a/Tests/DispatchAsyncTests/DispatchTimeTests.swift b/Tests/DispatchAsyncTests/DispatchTimeTests.swift index 42f526d..f0261cb 100644 --- a/Tests/DispatchAsyncTests/DispatchTimeTests.swift +++ b/Tests/DispatchAsyncTests/DispatchTimeTests.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Testing + @testable import DispatchAsync @Test From f64e771819d78dc89cc9ad829521023e7b734b5c Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 27 Jun 2025 16:38:52 -0600 Subject: [PATCH 22/53] chore: Add convenience scripts to run the same commands CI uses for swift-format and swift-lint. --- format.sh | 16 ++++++++++++++++ lint.sh | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100755 format.sh create mode 100755 lint.sh diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..9536d71 --- /dev/null +++ b/format.sh @@ -0,0 +1,16 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift.org project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of Swift.org project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..dbd4763 --- /dev/null +++ b/lint.sh @@ -0,0 +1,16 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift.org project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of Swift.org project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## + +git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel From f38c234d504ca2719851f04a66eb9e7fe6214e6b Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:12:48 -0600 Subject: [PATCH 23/53] chore: Fix license setup for soundness checks. --- LICENSE => LICENSE.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.txt (100%) diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt From 716c1849501ae1c0c0d43515015da856a76a3fad Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:18:04 -0600 Subject: [PATCH 24/53] chore: Removing lint rule definitions not recognized by github online swift-format version. --- .swift-format | 2 -- 1 file changed, 2 deletions(-) diff --git a/.swift-format b/.swift-format index e6d438f..a289d85 100644 --- a/.swift-format +++ b/.swift-format @@ -28,7 +28,6 @@ "AlwaysUseLiteralForEmptyCollectionInit" : false, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : true, - "AvoidRetroactiveConformances" : true, "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, "DontRepeatTypeInStaticProperties" : true, @@ -43,7 +42,6 @@ "NoAssignmentInExpressions" : true, "NoBlockComments" : true, "NoCasesWithOnlyFallthrough" : true, - "NoEmptyLinesOpeningClosingBraces" : false, "NoEmptyTrailingClosureParentheses" : true, "NoLabelsInCasePatterns" : true, "NoLeadingUnderscores" : false, From b698b1bf3a17206296552bc910bc20cc75510954 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:28:09 -0600 Subject: [PATCH 25/53] ci: Don't run tests on Swift 5.10 either. Not supported. --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e21c126..ea2a555 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,7 +17,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: enable_macos_checks: false - linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10.1\"}, {\"swift_version\": \"6.0\"}]" + linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"5.10.1\"}, {\"swift_version\": \"6.0\"}]" enable_windows_checks: false wasm-sdk: From fac6c98269f32dd92eed09d0b0add19bd90e240f Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:28:58 -0600 Subject: [PATCH 26/53] fix: Fix potential main thread issue in DispatchQueue that currently only seems to show up in linux. --- Sources/DispatchAsync/DispatchQueue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift index fc9a6f6..3610c07 100644 --- a/Sources/DispatchAsync/DispatchQueue.swift +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -72,7 +72,7 @@ public class DispatchQueue: @unchecked Sendable { } else { if isMain { Task { @MainActor [work] in - DispatchQueue.$isMain.withValue(true) { + DispatchQueue.$isMain.withValue(true) { @MainActor [work] in work() } } From 2c92ea921aa9c9c3a99934a457bbba7e500618c9 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:30:02 -0600 Subject: [PATCH 27/53] ci: Try a slightly different mechanism to get the swift version, to attempt to resolve issues showing up in CI for wasm builds. --- scripts/install_wasm_sdk.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_wasm_sdk.sh b/scripts/install_wasm_sdk.sh index b795fc3..b293e50 100755 --- a/scripts/install_wasm_sdk.sh +++ b/scripts/install_wasm_sdk.sh @@ -18,6 +18,6 @@ set -euo pipefail -version="$(swiftc --version | head -n1)" +version="$(swift --version | head -n1)" tag="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$version" '.[$v] | .[-1]')" curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$tag.json" | jq -r '.["swift-sdks"]["wasm32-unknown-wasi"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x From 67011b57ddd2d2d448fab18980fc72b6ef3a029a Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:55:44 -0600 Subject: [PATCH 28/53] chore: update scripting --- format.sh | 4 ++++ lint.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/format.sh b/format.sh index 9536d71..41690c4 100755 --- a/format.sh +++ b/format.sh @@ -13,4 +13,8 @@ ## ##===----------------------------------------------------------------------===## +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place diff --git a/lint.sh b/lint.sh index dbd4763..ee8e084 100755 --- a/lint.sh +++ b/lint.sh @@ -13,4 +13,8 @@ ## ##===----------------------------------------------------------------------===## +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel From 1f4c5c8c246ad915a3d192c98b3616d7aa3b46e6 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 10:56:07 -0600 Subject: [PATCH 29/53] chore: Fix lint that CI wants one way, and local install wants a different way. CI will have to win on this one for now. --- Sources/DispatchAsync/DispatchSemaphore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index e96d6db..b0a71e5 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -71,4 +71,4 @@ public class DispatchSemaphore: @unchecked Sendable { @available(macOS 10.15, *) typealias DispatchSemaphore = AsyncSemaphore -#endif // #if os(WASI) +#endif // #if os(WASI) From aa4565b984c89e50733ab8271f3ef43e3b9c18ed Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 11:04:52 -0600 Subject: [PATCH 30/53] ci: Use my own bash adapted from Yuta Saito's open MR to build wasm for now. --- .github/workflows/pull_request.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ea2a555..0d0ee66 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,6 +22,12 @@ jobs: wasm-sdk: name: WebAssembly SDK - # TODO: Switch to this line after https://github.com/apple/swift-nio/pull/3159/ is merged - # uses: apple/swift-nio/.github/workflows/wasm_sdk.yml@main - uses: kateinoigakukun/swift-nio/.github/workflows/wasm_sdk.yml@katei/add-wasm-ci + runs-on: ubuntu-latest + container: + image: "swift:6.0-noble" + steps: + - name: WasmBuild + # TODO: Update this to use swift-nio once https://github.com/apple/swift-nio/pull/3159/ is merged + run: | + apt-get update -y -q && apt-get install -y -q curl && $workspace/scripts/install_wasm_sdk.sh + swift build --swift-sdk wasm32-unknown-wasi \ No newline at end of file From cada3e836a79a0bd7ca56ea5bfe1189a72682491 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 11:32:16 -0600 Subject: [PATCH 31/53] ci: move scripts inline to yml configuration to work around issues with script pathing in CI. --- .github/workflows/pull_request.yml | 10 +++++++--- scripts/install_wasm_sdk.sh | 23 ----------------------- 2 files changed, 7 insertions(+), 26 deletions(-) delete mode 100755 scripts/install_wasm_sdk.sh diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0d0ee66..61f2b3a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,11 @@ jobs: image: "swift:6.0-noble" steps: - name: WasmBuild - # TODO: Update this to use swift-nio once https://github.com/apple/swift-nio/pull/3159/ is merged + # TODO: Update this to use swift-nio once https://github.com/apple/swift-nio/pull/3159/ is merged run: | - apt-get update -y -q && apt-get install -y -q curl && $workspace/scripts/install_wasm_sdk.sh - swift build --swift-sdk wasm32-unknown-wasi \ No newline at end of file + apt-get update -y -q + apt-get install -y -q curl + version="$(swift --version | head -n1)" + tag="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$version" '.[$v] | .[-1]')" + curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$tag.json" | jq -r '.["swift-sdks"]["wasm32-unknown-wasi"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x + swift build --swift-sdk wasm32-unknown-wasi diff --git a/scripts/install_wasm_sdk.sh b/scripts/install_wasm_sdk.sh deleted file mode 100755 index b293e50..0000000 --- a/scripts/install_wasm_sdk.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift.org open source project -## -## Copyright (c) 2025 Apple Inc. and the Swift.org project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of Swift.org project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -# TODO: Remove this file once there is a valid reference available in github from -# this PR: https://github.com/apple/swift-nio/pull/3159 - -set -euo pipefail - -version="$(swift --version | head -n1)" -tag="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$version" '.[$v] | .[-1]')" -curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$tag.json" | jq -r '.["swift-sdks"]["wasm32-unknown-wasi"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x From 69e8aee5bdf0cc58868333db68f643286f59c692 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 11:50:58 -0600 Subject: [PATCH 32/53] test: Update unit tests to adjust expectations for linux targets. Linux doesn't run main actor on the main thread, so the expectation was not correct. Adjusted expectations. --- Tests/DispatchAsyncTests/DispatchQueueTests.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index a36c3c3..800a1e0 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -24,9 +24,14 @@ import class Foundation.Thread func testBasicDispatchQueueMain() async throws { let asyncValue = await withCheckedContinuation { continuation in DispatchQueue.main.async { - // Main queue should be on main thread. - #if !os(WASI) - #expect(Thread.isMainThread) + // Main queue should be on main thread on apple platforms. + // On linux platforms, there is no guarantee that the main queue is on the main thread, + // only that it is on the main actor. + + #if os(LINUX) + #expect(DispatchQueue.isMain) + #elseif !os(WASI) + #expect(Thread.isMainThread) // NOTE: Thread API's aren't currently available on OS(WASI), as of June 2025 #endif continuation.resume(returning: true) } From e2ee2201d5dfe650298ab7a4acda314bbd06cccb Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 12:31:44 -0600 Subject: [PATCH 33/53] ci: Install jq for wasm builds. --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 61f2b3a..c0d5fb0 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -31,6 +31,7 @@ jobs: run: | apt-get update -y -q apt-get install -y -q curl + apt-get install -y -q jq version="$(swift --version | head -n1)" tag="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$version" '.[$v] | .[-1]')" curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$tag.json" | jq -r '.["swift-sdks"]["wasm32-unknown-wasi"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x From 9ad3a89774e19ea1516fa8370660414d4ffadae0 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 12:32:08 -0600 Subject: [PATCH 34/53] fix: Fix unit test expectations for linux. Take two. --- Tests/DispatchAsyncTests/DispatchQueueTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index 800a1e0..3f333b5 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -28,7 +28,7 @@ func testBasicDispatchQueueMain() async throws { // On linux platforms, there is no guarantee that the main queue is on the main thread, // only that it is on the main actor. - #if os(LINUX) + #if os(Linux) #expect(DispatchQueue.isMain) #elseif !os(WASI) #expect(Thread.isMainThread) // NOTE: Thread API's aren't currently available on OS(WASI), as of June 2025 From 4ac24943d1b05937e305c84b327b21f6636654d0 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 12:32:15 -0600 Subject: [PATCH 35/53] chore: lint --- Tests/DispatchAsyncTests/DispatchQueueTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index 3f333b5..d4e8c77 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -31,7 +31,8 @@ func testBasicDispatchQueueMain() async throws { #if os(Linux) #expect(DispatchQueue.isMain) #elseif !os(WASI) - #expect(Thread.isMainThread) // NOTE: Thread API's aren't currently available on OS(WASI), as of June 2025 + // NOTE: Thread API's aren't currently available on OS(WASI), as of June 2025 + #expect(Thread.isMainThread) #endif continuation.resume(returning: true) } From e271490601c69242cc2a2f54438dee3998d26507 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 12:36:20 -0600 Subject: [PATCH 36/53] ci: wasm build needs to clone the code before it can build. --- .github/workflows/pull_request.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c0d5fb0..c6be0f7 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,6 +26,10 @@ jobs: container: image: "swift:6.0-noble" steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Swift version + run: swift --version - name: WasmBuild # TODO: Update this to use swift-nio once https://github.com/apple/swift-nio/pull/3159/ is merged run: | From 0950a4f4d3c33ae4ea42199d3e683ffd02ebd653 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 12:41:03 -0600 Subject: [PATCH 37/53] ci: use Swift 6.1 for wasm build. --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c6be0f7..9f8b096 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,7 +24,7 @@ jobs: name: WebAssembly SDK runs-on: ubuntu-latest container: - image: "swift:6.0-noble" + image: "swift:6.1-noble" steps: - name: Checkout repository uses: actions/checkout@v4 From a29c5aa8461f45ed10aa668965643c07d7b00828 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 30 Jun 2025 12:44:35 -0600 Subject: [PATCH 38/53] ci: Specifical swift 6.1.0 for wasm builds, not swift 6.1.2. --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9f8b096..e41d131 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -24,7 +24,7 @@ jobs: name: WebAssembly SDK runs-on: ubuntu-latest container: - image: "swift:6.1-noble" + image: "swift:6.1.0-noble" steps: - name: Checkout repository uses: actions/checkout@v4 From 33536b0c37db90b96d5b9384ac0cfe5edebe3aa2 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Thu, 3 Jul 2025 13:15:14 -0600 Subject: [PATCH 39/53] chore: Fix incomplete comment. --- Sources/DispatchAsync/DispatchSemaphore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index b0a71e5..050aa55 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -59,7 +59,8 @@ public class DispatchSemaphore: @unchecked Sendable { public func wait() { // NOTE: wasm is currently mostly single threaded. // And we don't have a Thread.sleep API yet. - // So + // So assert that we're on the main actor here. Usage from other + // actors is not currently supported. MainActor.assertIsolated() assert(value > 0, "DispatchSemaphore is currently only designed for single-threaded use.") value -= 1 From 9df1a129c275f724e7d1ebbc5669b2fa67298fa6 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Tue, 8 Jul 2025 13:14:05 -0600 Subject: [PATCH 40/53] fix: Fix a variety of issues found in DispatchAsync while implementing new tests. --- Sources/DispatchAsync/AsyncSemaphore.swift | 3 +- Sources/DispatchAsync/DispatchGroup.swift | 72 +++++++++++----------- Sources/DispatchAsync/DispatchQueue.swift | 69 ++++++++++++++++++++- Sources/DispatchAsync/DispatchTime.swift | 45 ++++++++------ 4 files changed, 131 insertions(+), 58 deletions(-) diff --git a/Sources/DispatchAsync/AsyncSemaphore.swift b/Sources/DispatchAsync/AsyncSemaphore.swift index cbf60b3..58b6a45 100644 --- a/Sources/DispatchAsync/AsyncSemaphore.swift +++ b/Sources/DispatchAsync/AsyncSemaphore.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -/// Provides a semaphore implantation in `async` context, with a safe wait method. Provides easy safer replacement +/// Provides a semaphore implantation in `async` context, with a safe wait method. Provides easy safe replacement /// for DispatchSemaphore usage. @available(macOS 10.15, *) actor AsyncSemaphore { @@ -25,6 +25,7 @@ actor AsyncSemaphore { func wait() async { value -= 1 + if value >= 0 { return } await withCheckedContinuation { waiters.append($0) diff --git a/Sources/DispatchAsync/DispatchGroup.swift b/Sources/DispatchAsync/DispatchGroup.swift index 2251558..559b67f 100644 --- a/Sources/DispatchAsync/DispatchGroup.swift +++ b/Sources/DispatchAsync/DispatchGroup.swift @@ -23,34 +23,29 @@ /// for more details, @available(macOS 10.15, *) public class DispatchGroup: @unchecked Sendable { - /// Used to ensure FIFO access to the enter and leave calls - @globalActor - private actor DispatchGroupEntryActor: GlobalActor { - static let shared = DispatchGroupEntryActor() - } - - private let group = AsyncGroup() + private let group = _AsyncGroup() + private let queue = FIFOQueue() public func enter() { - Task { @DispatchGroupEntryActor [] in - // ^--- Ensures serial FIFO entrance/exit into the group + queue.enqueue { [weak self] in + guard let self else { return } await group.enter() } } public func leave() { - Task { @DispatchGroupEntryActor [] in - // ^--- Ensures serial FIFO entrance/exit into the group + queue.enqueue { [weak self] in + guard let self else { return } await group.leave() } } - public func notify(queue: DispatchQueue, execute work: @escaping @Sendable @convention(block) () -> Void) { - Task { @DispatchGroupEntryActor [] in - // ^--- Ensures serial FIFO entrance/exit into the group + public func notify(queue notificationQueue: DispatchQueue, execute work: @escaping @Sendable @convention(block) () -> Void) { + queue.enqueue { [weak self] in + guard let self else { return } await group.notify { await withCheckedContinuation { continuation in - queue.async { + notificationQueue.async { work() continuation.resume() } @@ -60,7 +55,22 @@ public class DispatchGroup: @unchecked Sendable { } func wait() async { - await group.wait() + await withCheckedContinuation { continuation in + queue.enqueue { [weak self] in + guard let self else { return } + // NOTE: We use a task for the wait, because + // otherwise the queue won't execute any more + // tasks until the wait finishes, which is not the + // behavior we want here. We want to enqueue the wait + // in FIFO call order, but then we want to allow the wait + // to be non-blocking for the queue until the last leave + // is called on the group. + Task { + await group.wait() + continuation.resume() + } + } + } } public init() {} @@ -69,10 +79,8 @@ public class DispatchGroup: @unchecked Sendable { // MARK: - Private Interface for Async Usage - @available(macOS 10.15, *) -fileprivate actor AsyncGroup { +fileprivate actor _AsyncGroup { private var taskCount = 0 - private var continuation: CheckedContinuation? - private var isWaiting = false private var notifyHandlers: [@Sendable () async -> Void] = [] func enter() { @@ -100,30 +108,22 @@ fileprivate actor AsyncGroup { return } - isWaiting = true - await withCheckedContinuation { (continuation: CheckedContinuation) in - self.continuation = continuation + notify { + continuation.resume() + } checkCompletion() } } private func checkCompletion() { - if taskCount <= 0 { - if isWaiting { - continuation?.resume() - continuation = nil - isWaiting = false - } + if taskCount <= 0, !notifyHandlers.isEmpty { + let handlers = notifyHandlers + notifyHandlers.removeAll() - if !notifyHandlers.isEmpty { - let handlers = notifyHandlers - notifyHandlers.removeAll() - - for handler in handlers { - Task { - await handler() - } + for handler in handlers { + Task { + await handler() } } } diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift index 3610c07..3ee9cfb 100644 --- a/Sources/DispatchAsync/DispatchQueue.swift +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -23,17 +23,26 @@ public class DispatchQueue: @unchecked Sendable { public static let main = DispatchQueue(isMain: true) - private static let _global = DispatchQueue() + private static let _global = DispatchQueue(attributes: .concurrent) public static func global() -> DispatchQueue { Self._global } public enum Attributes { case concurrent + + fileprivate var isConcurrent: Bool { + switch self { + case .concurrent: + return true + } + } } private let targetQueue: DispatchQueue? + private let serialQueue = FIFOQueue() + /// Indicates whether calling context is running from the main DispatchQueue instance, or some other DispatchQueue instance. @TaskLocal public static var isMain = false @@ -56,6 +65,10 @@ public class DispatchQueue: @unchecked Sendable { attributes: DispatchQueue.Attributes? = nil, target: DispatchQueue? = nil ) { + if isMain, attributes == .concurrent { + assertionFailure("Should never create a concurrent main queue. Main queue should always be serial.") + } + self.isMain = isMain self.label = label self.attributes = attributes @@ -77,10 +90,60 @@ public class DispatchQueue: @unchecked Sendable { } } } else { - Task { - work() + if attributes?.isConcurrent == true { + Task { // FIFO is not important for concurrent queues, using global task executor here + work() + } + } else { + // We don't need to use a task for enqueing work to a non-main serial queue + // because the enqueue process is very light-weight, and it is important to + // preserve FIFO entry into the queue as much as possible. + serialQueue.enqueue(work) } } } } } + +/// A tiny FIFO job runner that executes each submitted async closure +/// strictly in the order it was enqueued. +/// +/// This is NOT part of the original GCD API. So it is intentionally kept +/// internal for now. +@available(macOS 10.15, *) +actor FIFOQueue { + /// A single item in the stream, which is a block of work that can be completed. + typealias WorkItem = @Sendable () async -> Void + + /// The stream’s continuation; lives inside the actor so nobody + /// else can yield into it. + private let continuation: AsyncStream.Continuation + + /// Spin up the stream and the single draining task. + init(bufferingPolicy: AsyncStream.Continuation.BufferingPolicy = .unbounded) { + let stream: AsyncStream + (stream, self.continuation) = AsyncStream.makeStream(of: WorkItem.self, bufferingPolicy: bufferingPolicy) + + // Dedicated worker that processes work items one-by-one. + Task { + for await work in stream { + // Run each job in order, allowing suspension, and awaiting full + // completion, before running the next work item + await work() + } + } + } + + /// Enqueue a new unit of work. + @discardableResult + nonisolated + func enqueue(_ workItem: @escaping WorkItem) -> AsyncStream.Continuation.YieldResult { + // Never suspends, preserves order + continuation.yield(workItem) + } + + deinit { + // Clean shutdown on deinit + continuation.finish() + } +} diff --git a/Sources/DispatchAsync/DispatchTime.swift b/Sources/DispatchAsync/DispatchTime.swift index c9c1935..3aa3598 100644 --- a/Sources/DispatchAsync/DispatchTime.swift +++ b/Sources/DispatchAsync/DispatchTime.swift @@ -13,30 +13,39 @@ //===----------------------------------------------------------------------===// @available(macOS 13, *) -public typealias DispatchTime = ContinuousClock.Instant +public struct DispatchTime { + private let instant: ContinuousClock.Instant -/// The very first time someone tries to reference a `uptimeNanoseconds` or a similar -/// function that references a beginning point, this variable will be initialized as a beginning -/// reference point. This guarantees that all calls to `uptimeNanoseconds` or similar -/// will be 0 or greater. -/// -/// By design, it is not possible to related `ContinuousClock.Instant` to -/// `ProcessInfo.processInfo.systemUptime`, and even if one devised such -/// a mechanism, it would open the door for fingerprinting. It's best to let the concept -/// of uptime be relative to previous uptime calls. -@available(macOS 13, *) -private let uptimeBeginning: DispatchTime = DispatchTime.now() + /// The very first time someone intializes a DispatchTime instance, we + /// reference this static let, causing it to be initialized. + /// + /// This is the closest we can get to snapshotting the start time of the running + /// executable, without using OS-specific calls. We want + /// to avoid OS-specific calls to maximize portability. + /// + /// To keep this robust, we initialize `self.durationSinceBeginning` + /// to this value using a default value, which is guaranteed to run before any + /// initializers run. This guarantees that uptimeBeginning will be the very + /// first + @available(macOS 13, *) + private static let uptimeBeginning: ContinuousClock.Instant = ContinuousClock.Instant.now + + /// See documentation for ``uptimeBeginning``. We intentionally + /// use this to guarantee a capture of `now` to uptimeBeginnin BEFORE + /// any DispatchTime instances are initialized. + private let durationSinceUptime = uptimeBeginning.duration(to: ContinuousClock.Instant.now) + + public init() { + self.instant = ContinuousClock.Instant.now + } -@available(macOS 13, *) -extension DispatchTime { public static func now() -> DispatchTime { - now + DispatchTime() } public var uptimeNanoseconds: UInt64 { - let beginning = uptimeBeginning - let rightNow = DispatchTime.now() - let uptimeDuration: Int64 = beginning.duration(to: rightNow).nanosecondsClamped + let beginning = DispatchTime.uptimeBeginning + let uptimeDuration: Int64 = beginning.duration(to: self.instant).nanosecondsClamped guard uptimeDuration >= 0 else { assertionFailure("It shouldn't be possible to get a negative duration since uptimeBeginning.") return 0 From 2fb14f78d4572ba64f8a87d523451695903db622 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Tue, 8 Jul 2025 14:07:51 -0600 Subject: [PATCH 41/53] chore: Use swift version 6.0 as the minimum rather than 6.1. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 943c8e3..791209c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.1 +// swift-tools-version: 6.0 import PackageDescription From 5ba82802530049da9ccdd1a62d2810b36eebc8ea Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 14:16:34 -0600 Subject: [PATCH 42/53] 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. --- Package.swift | 10 +- Sources/DispatchAsync/AsyncSemaphore.swift | 16 +- ...ageConstants.swift => DispatchAsync.swift} | 15 +- Sources/DispatchAsync/DispatchGroup.swift | 192 ++++++++------- Sources/DispatchAsync/DispatchQueue.swift | 232 ++++++++++-------- Sources/DispatchAsync/DispatchSemaphore.swift | 115 ++++----- Sources/DispatchAsync/DispatchTime.swift | 82 ++++--- .../DispatchAsync/DispatchTimeInterval.swift | 113 +++++---- .../DispatchGroupTests.swift | 4 +- .../DispatchQueueTests.swift | 4 +- .../DispatchSemaphoreTests.swift | 9 +- .../DispatchTimeTests.swift | 6 +- 12 files changed, 450 insertions(+), 348 deletions(-) rename Sources/DispatchAsync/{PackageConstants.swift => DispatchAsync.swift} (54%) diff --git a/Package.swift b/Package.swift index 791209c..e75b7c0 100644 --- a/Package.swift +++ b/Package.swift @@ -7,14 +7,18 @@ let package = Package( products: [ .library( name: "DispatchAsync", - targets: ["DispatchAsync"]) + targets: ["DispatchAsync"] + ), ], targets: [ .target( - name: "DispatchAsync"), + name: "DispatchAsync", + ), .testTarget( name: "DispatchAsyncTests", - dependencies: ["DispatchAsync"] + dependencies: [ + "DispatchAsync" + ], ), ] ) diff --git a/Sources/DispatchAsync/AsyncSemaphore.swift b/Sources/DispatchAsync/AsyncSemaphore.swift index 58b6a45..b23f105 100644 --- a/Sources/DispatchAsync/AsyncSemaphore.swift +++ b/Sources/DispatchAsync/AsyncSemaphore.swift @@ -14,16 +14,16 @@ /// Provides a semaphore implantation in `async` context, with a safe wait method. Provides easy safe replacement /// for DispatchSemaphore usage. -@available(macOS 10.15, *) -actor AsyncSemaphore { +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public actor AsyncSemaphore { private var value: Int private var waiters: [CheckedContinuation] = [] - init(value: Int = 1) { + public init(value: Int = 1) { self.value = value } - func wait() async { + public func wait() async { value -= 1 if value >= 0 { return } @@ -32,7 +32,7 @@ actor AsyncSemaphore { } } - func signal() { + public func signal() { self.value += 1 guard !waiters.isEmpty else { return } @@ -41,15 +41,15 @@ actor AsyncSemaphore { } } -@available(macOS 10.15, *) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension AsyncSemaphore { - func withLock(_ closure: () async throws -> T) async rethrows -> T { + public func withLock(_ closure: () async throws -> T) async rethrows -> T { await wait() defer { signal() } return try await closure() } - func withLockVoid(_ closure: () async throws -> Void) async rethrows { + public func withLockVoid(_ closure: () async throws -> Void) async rethrows { await wait() defer { signal() } try await closure() diff --git a/Sources/DispatchAsync/PackageConstants.swift b/Sources/DispatchAsync/DispatchAsync.swift similarity index 54% rename from Sources/DispatchAsync/PackageConstants.swift rename to Sources/DispatchAsync/DispatchAsync.swift index 762abf1..8ef63d2 100644 --- a/Sources/DispatchAsync/PackageConstants.swift +++ b/Sources/DispatchAsync/DispatchAsync.swift @@ -12,6 +12,15 @@ // //===----------------------------------------------------------------------===// -package let kNanosecondsPerSecond: UInt64 = 1_000_000_000 -package let kNanosecondsPerMillisecond: UInt64 = 1_000_000 -package let kNanoSecondsPerMicrosecond: UInt64 = 1_000 +/// Top level namespace for functionality provided in DispatchAsync. +/// +/// Used to avoid namespacing conflicts with `Dispatch` and `Foundation` +/// +/// Platforms other than WASI shouldn't consume this library for now +/// except for testing and development purposes. +/// +/// TODO: SM: Add github permalink to this, after it is merged. +#if !os(WASI) +@_spi(DispatchAsync) +#endif +public enum DispatchAsync {} diff --git a/Sources/DispatchAsync/DispatchGroup.swift b/Sources/DispatchAsync/DispatchGroup.swift index 559b67f..87a0e00 100644 --- a/Sources/DispatchAsync/DispatchGroup.swift +++ b/Sources/DispatchAsync/DispatchGroup.swift @@ -12,118 +12,138 @@ // //===----------------------------------------------------------------------===// -// MARK: - Public Interface for Non-Async Usage - - -/// `DispatchGroup` is a drop-in replacement for the `DispatchGroup` implemented -/// in Grand Central Dispatch. However, this class uses Swift Concurrency, instead of low-level threading API's. -/// -/// The primary goal of this implementation is to enable WASM support for Dispatch. -/// -/// Refer to documentation for the original [DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup) -/// for more details, -@available(macOS 10.15, *) -public class DispatchGroup: @unchecked Sendable { - private let group = _AsyncGroup() - private let queue = FIFOQueue() - - public func enter() { - queue.enqueue { [weak self] in - guard let self else { return } - await group.enter() +// NOTE: The following typealias mirrors Dispatch API's, but only for +// specific compilation conditions where Dispatch is not available. +// It is designed to safely elide away if and when Dispatch is introduced +// in the required Dispatch support becomes available. +#if os(WASI) && !canImport(Dispatch) +/// Drop-in replacement for ``Dispatch.DispatchGroup``, implemented using pure swift. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public typealias DispatchGroup = DispatchAsync.DispatchGroup +#endif + +extension DispatchAsync { + // MARK: - Public Interface for Non-Async Usage - + + /// Drop-in replacement for ``Dispatch.DispatchGroup``, implemented using pure swift. + /// + /// The primary goal of this implementation is to enable WASM support for Dispatch. + /// + /// Refer to documentation for the original [DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup) + /// for more details, + #if !os(WASI) + @_spi(DispatchAsync) + #endif + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public class DispatchGroup: @unchecked Sendable { + private let group = _AsyncGroup() + private let queue = DispatchAsync.FIFOQueue() + + public func enter() { + queue.enqueue { [weak self] in + guard let self else { return } + await group.enter() + } } - } - public func leave() { - queue.enqueue { [weak self] in - guard let self else { return } - await group.leave() + public func leave() { + queue.enqueue { [weak self] in + guard let self else { return } + await group.leave() + } } - } - public func notify(queue notificationQueue: DispatchQueue, execute work: @escaping @Sendable @convention(block) () -> Void) { - queue.enqueue { [weak self] in - guard let self else { return } - await group.notify { - await withCheckedContinuation { continuation in - notificationQueue.async { - work() - continuation.resume() + public func notify( + queue notificationQueue: DispatchAsync.DispatchQueue, + execute work: @escaping @Sendable @convention(block) () -> Void + ) { + queue.enqueue { [weak self] in + guard let self else { return } + await group.notify { + await withCheckedContinuation { continuation in + notificationQueue.async { + work() + continuation.resume() + } } } } } - } - func wait() async { - await withCheckedContinuation { continuation in - queue.enqueue { [weak self] in - guard let self else { return } - // NOTE: We use a task for the wait, because - // otherwise the queue won't execute any more - // tasks until the wait finishes, which is not the - // behavior we want here. We want to enqueue the wait - // in FIFO call order, but then we want to allow the wait - // to be non-blocking for the queue until the last leave - // is called on the group. - Task { - await group.wait() - continuation.resume() + func wait() async { + await withCheckedContinuation { continuation in + queue.enqueue { [weak self] in + guard let self else { return } + // NOTE: We use a task for the wait, because + // otherwise the queue won't execute any more + // tasks until the wait finishes, which is not the + // behavior we want here. We want to enqueue the wait + // in FIFO call order, but then we want to allow the wait + // to be non-blocking for the queue until the last leave + // is called on the group. + Task { + await group.wait() + continuation.resume() + } } } } + + public init() {} } - public init() {} -} + // MARK: - Private Interface for Async Usage - -// MARK: - Private Interface for Async Usage - + #if !os(WASI) + @_spi(DispatchAsync) + #endif + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + fileprivate actor _AsyncGroup { + private var taskCount = 0 + private var notifyHandlers: [@Sendable () async -> Void] = [] -@available(macOS 10.15, *) -fileprivate actor _AsyncGroup { - private var taskCount = 0 - private var notifyHandlers: [@Sendable () async -> Void] = [] + func enter() { + taskCount += 1 + } - func enter() { - taskCount += 1 - } + func leave() { + defer { + checkCompletion() + } + guard taskCount > 0 else { + assertionFailure("leave() called more times than enter()") + return + } + taskCount -= 1 + } - func leave() { - defer { + func notify(handler: @escaping @Sendable () async -> Void) { + notifyHandlers.append(handler) checkCompletion() } - guard taskCount > 0 else { - assertionFailure("leave() called more times than enter()") - return - } - taskCount -= 1 - } - func notify(handler: @escaping @Sendable () async -> Void) { - notifyHandlers.append(handler) - checkCompletion() - } - - func wait() async { - if taskCount <= 0 { - return - } + func wait() async { + if taskCount <= 0 { + return + } - await withCheckedContinuation { (continuation: CheckedContinuation) in - notify { - continuation.resume() + await withCheckedContinuation { (continuation: CheckedContinuation) in + notify { + continuation.resume() + } + checkCompletion() } - checkCompletion() } - } - private func checkCompletion() { - if taskCount <= 0, !notifyHandlers.isEmpty { - let handlers = notifyHandlers - notifyHandlers.removeAll() + private func checkCompletion() { + if taskCount <= 0, !notifyHandlers.isEmpty { + let handlers = notifyHandlers + notifyHandlers.removeAll() - for handler in handlers { - Task { - await handler() + for handler in handlers { + Task { + await handler() + } } } } diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift index 3ee9cfb..31cb240 100644 --- a/Sources/DispatchAsync/DispatchQueue.swift +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -12,138 +12,152 @@ // //===----------------------------------------------------------------------===// -/// `DispatchQueue` is a drop-in replacement for the `DispatchQueue` implemented -/// in Grand Central Dispatch. However, this class uses Swift Concurrency, instead of low-level threading API's. -/// -/// The primary goal of this implementation is to enable WASM support for Dispatch. -/// -/// Refer to documentation for the original [DispatchQueue](https://developer.apple.com/documentation/dispatch/dispatchqueue) -/// for more details, -@available(macOS 10.15, *) -public class DispatchQueue: @unchecked Sendable { - public static let main = DispatchQueue(isMain: true) - - private static let _global = DispatchQueue(attributes: .concurrent) - public static func global() -> DispatchQueue { - Self._global - } +// NOTE: The following typealias mirrors Dispatch API's, but only for +// specific compilation conditions where Dispatch is not available. +// It is designed to safely elide away if and when Dispatch is introduced +// in the required Dispatch support becomes available. +#if os(WASI) && !canImport(Dispatch) +/// Drop-in replacement for ``Dispatch.DispatchQueue``, implemented using pure swift. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public typealias DispatchQueue = DispatchAsync.DispatchQueue +#endif + +extension DispatchAsync { + /// Drop-in replacement for ``Dispatch.DispatchQueue``, implemented using pure swift. + /// + /// The primary goal of this implementation is to enable WASM support for Dispatch. + /// + /// Refer to documentation for the original [DispatchQueue](https://developer.apple.com/documentation/dispatch/dispatchqueue) + /// for more details, + #if !os(WASI) + @_spi(DispatchAsync) + #endif + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public class DispatchQueue: @unchecked Sendable { + public static let main = DispatchQueue(isMain: true) + + private static let _global = DispatchQueue(attributes: .concurrent) + public static func global() -> DispatchQueue { + Self._global + } - public enum Attributes { - case concurrent + public enum Attributes { + case concurrent - fileprivate var isConcurrent: Bool { - switch self { - case .concurrent: - return true + fileprivate var isConcurrent: Bool { + switch self { + case .concurrent: + return true + } } } - } - - private let targetQueue: DispatchQueue? - private let serialQueue = FIFOQueue() + private let targetQueue: DispatchQueue? - /// Indicates whether calling context is running from the main DispatchQueue instance, or some other DispatchQueue instance. - @TaskLocal public static var isMain = false + private let serialQueue = FIFOQueue() - /// This is set during the initialization of the DispatchQueue, and controls whether `async` calls run on MainActor or not - private let isMain: Bool - private let label: String? - private let attributes: DispatchQueue.Attributes? + /// Indicates whether calling context is running from the main DispatchQueue instance, or some other DispatchQueue instance. + @TaskLocal public static var isMain = false - public convenience init( - label: String? = nil, - attributes: DispatchQueue.Attributes? = nil, - target: DispatchQueue? = nil - ) { - self.init(isMain: false, label: label, attributes: attributes, target: target) - } + /// This is set during the initialization of the DispatchQueue, and controls whether `async` calls run on MainActor or not + private let isMain: Bool + private let label: String? + private let attributes: DispatchQueue.Attributes? - private init( - isMain: Bool, - label: String? = nil, - attributes: DispatchQueue.Attributes? = nil, - target: DispatchQueue? = nil - ) { - if isMain, attributes == .concurrent { - assertionFailure("Should never create a concurrent main queue. Main queue should always be serial.") + public convenience init( + label: String? = nil, + attributes: DispatchQueue.Attributes? = nil, + target: DispatchQueue? = nil + ) { + self.init(isMain: false, label: label, attributes: attributes, target: target) } - self.isMain = isMain - self.label = label - self.attributes = attributes - self.targetQueue = target - } + private init( + isMain: Bool, + label: String? = nil, + attributes: DispatchQueue.Attributes? = nil, + target: DispatchQueue? = nil + ) { + if isMain, attributes == .concurrent { + assertionFailure("Should never create a concurrent main queue. Main queue should always be serial.") + } - public func async( - execute work: @escaping @Sendable @convention(block) () -> Void - ) { - if let targetQueue, targetQueue !== self { - // Recursively call this function on the target queue - // until we reach a nil queue, or this queue. - targetQueue.async(execute: work) - } else { - if isMain { - Task { @MainActor [work] in - DispatchQueue.$isMain.withValue(true) { @MainActor [work] in - work() - } - } + self.isMain = isMain + self.label = label + self.attributes = attributes + self.targetQueue = target + } + + public func async( + execute work: @escaping @Sendable @convention(block) () -> Void + ) { + if let targetQueue, targetQueue !== self { + // Recursively call this function on the target queue + // until we reach a nil queue, or this queue. + targetQueue.async(execute: work) } else { - if attributes?.isConcurrent == true { - Task { // FIFO is not important for concurrent queues, using global task executor here - work() + if isMain { + Task { @MainActor [work] in + DispatchQueue.$isMain.withValue(true) { @MainActor [work] in + work() + } } } else { - // We don't need to use a task for enqueing work to a non-main serial queue - // because the enqueue process is very light-weight, and it is important to - // preserve FIFO entry into the queue as much as possible. - serialQueue.enqueue(work) + if attributes?.isConcurrent == true { + Task { // FIFO is not important for concurrent queues, using global task executor here + work() + } + } else { + // We don't need to use a task for enqueing work to a non-main serial queue + // because the enqueue process is very light-weight, and it is important to + // preserve FIFO entry into the queue as much as possible. + serialQueue.enqueue(work) + } } } } } -} -/// A tiny FIFO job runner that executes each submitted async closure -/// strictly in the order it was enqueued. -/// -/// This is NOT part of the original GCD API. So it is intentionally kept -/// internal for now. -@available(macOS 10.15, *) -actor FIFOQueue { - /// A single item in the stream, which is a block of work that can be completed. - typealias WorkItem = @Sendable () async -> Void - - /// The stream’s continuation; lives inside the actor so nobody - /// else can yield into it. - private let continuation: AsyncStream.Continuation - - /// Spin up the stream and the single draining task. - init(bufferingPolicy: AsyncStream.Continuation.BufferingPolicy = .unbounded) { - let stream: AsyncStream - (stream, self.continuation) = AsyncStream.makeStream(of: WorkItem.self, bufferingPolicy: bufferingPolicy) - - // Dedicated worker that processes work items one-by-one. - Task { - for await work in stream { - // Run each job in order, allowing suspension, and awaiting full - // completion, before running the next work item - await work() + /// A tiny FIFO job runner that executes each submitted async closure + /// strictly in the order it was enqueued. + #if !os(WASI) + @_spi(DispatchAsync) + #endif + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public actor FIFOQueue { + /// A single item in the stream, which is a block of work that can be completed. + public typealias WorkItem = @Sendable () async -> Void + + /// The stream’s continuation; lives inside the actor so nobody + /// else can yield into it. + private let continuation: AsyncStream.Continuation + + /// Spin up the stream and the single draining task. + public init(bufferingPolicy: AsyncStream.Continuation.BufferingPolicy = .unbounded) { + let stream: AsyncStream + (stream, self.continuation) = AsyncStream.makeStream(of: WorkItem.self, bufferingPolicy: bufferingPolicy) + + // Dedicated worker that processes work items one-by-one. + Task { + for await work in stream { + // Run each job in order, allowing suspension, and awaiting full + // completion, before running the next work item + await work() + } } } - } - /// Enqueue a new unit of work. - @discardableResult - nonisolated - func enqueue(_ workItem: @escaping WorkItem) -> AsyncStream.Continuation.YieldResult { - // Never suspends, preserves order - continuation.yield(workItem) - } + /// Enqueue a new unit of work. + @discardableResult + nonisolated + func enqueue(_ workItem: @escaping WorkItem) -> AsyncStream.Continuation.YieldResult { + // Never suspends, preserves order + continuation.yield(workItem) + } - deinit { - // Clean shutdown on deinit - continuation.finish() + deinit { + // Clean shutdown on deinit + continuation.finish() + } } } diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index 050aa55..b3c5095 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -12,64 +12,69 @@ // //===----------------------------------------------------------------------===// -// This implementation assumes the single-threaded -// environment that swift wasm executables typically run in. -// -// It is not appropriate for true multi-threaded environments. -// -// For safety, this class is only defined for WASI platforms. -// -// -#if os(WASI) +// NOTE: The following typealias mirrors Dispatch API's, but only for +// specific compilation conditions where Dispatch is not available. +// It is designed to safely elide away if and when Dispatch is introduced +// in the required Dispatch support becomes available. +#if os(WASI) && !canImport(Dispatch) +/// Drop-in replacement for ``Dispatch.DispatchSemaphore``, implemented using pure swift. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public typealias DispatchSemaphore = DispatchAsync.DispatchSemaphore +#endif // os(WASI) && !canImport(Dispatch) -/// DispatchSemaphore is not safe to use for most wasm executables. -/// -/// Most wasm executables are single-threaded. Calling DispatchSemaphore.wait -/// when it's value is 0 or lower would be likely cause a frozen main thread, -/// because that would block the calling thread. And there is usually -/// only one thread in the wasm world (right now). -/// -/// For now, we guard against that case with both compile-time deprecation -/// pointing to the much safer ``AsyncSemaphore``, and also at run-time with -/// assertions. -/// -/// ``AsyncSemaphore`` provides full functionality, but only exposes -/// Swift Concurrency api's with a safe async wait function. -@available( - *, - deprecated, - renamed: "AsyncSemaphore", - message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." -) -@available(macOS 10.15, *) -public class DispatchSemaphore: @unchecked Sendable { - public var value: Int +extension DispatchAsync { + /// DispatchSemaphore is not safe to use for most wasm executables. + /// + /// This implementation assumes the single-threaded + /// environment that swift wasm executables typically run in. + /// + /// It is not appropriate for true multi-threaded environments. + /// + /// For safety, this class is only defined for WASI platforms. + /// + /// Most wasm executables are single-threaded. Calling DispatchSemaphore.wait + /// when it's value is 0 or lower would be likely cause a frozen main thread, + /// because that would block the calling thread. And there is usually + /// only one thread in the wasm world (right now). + /// + /// For now, we guard against that case with both compile-time deprecation + /// pointing to the much safer ``AsyncSemaphore``, and also at run-time with + /// assertions. + /// + /// ``AsyncSemaphore`` provides full functionality, but only exposes + /// Swift Concurrency api's with a safe async wait function. + #if !os(WASI) + @_spi(DispatchAsyncSingleThreadedSemaphore) + #endif + @available( + *, + deprecated, + renamed: "AsyncSemaphore", + message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." + ) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public class DispatchSemaphore: @unchecked Sendable { + public var value: Int - public init(value: Int) { - self.value = value - } + public init(value: Int) { + self.value = value + } - @discardableResult - public func signal() -> Int { - MainActor.assertIsolated() - value += 1 - return value - } + @discardableResult + public func signal() -> Int { + MainActor.assertIsolated() + value += 1 + return value + } - public func wait() { - // NOTE: wasm is currently mostly single threaded. - // And we don't have a Thread.sleep API yet. - // So assert that we're on the main actor here. Usage from other - // actors is not currently supported. - MainActor.assertIsolated() - assert(value > 0, "DispatchSemaphore is currently only designed for single-threaded use.") - value -= 1 + public func wait() { + // NOTE: wasm is currently mostly single threaded. + // And we don't have a Thread.sleep API yet. + // So assert that we're on the main actor here. Usage from other + // actors is not currently supported. + MainActor.assertIsolated() + assert(value > 0, "DispatchSemaphore is currently only designed for single-threaded use.") + value -= 1 + } } } - -#else - -@available(macOS 10.15, *) -typealias DispatchSemaphore = AsyncSemaphore - -#endif // #if os(WASI) diff --git a/Sources/DispatchAsync/DispatchTime.swift b/Sources/DispatchAsync/DispatchTime.swift index 3aa3598..906a178 100644 --- a/Sources/DispatchAsync/DispatchTime.swift +++ b/Sources/DispatchAsync/DispatchTime.swift @@ -12,45 +12,61 @@ // //===----------------------------------------------------------------------===// -@available(macOS 13, *) -public struct DispatchTime { - private let instant: ContinuousClock.Instant +// NOTE: The following typealias mirrors Dispatch API's, but only for +// specific compilation conditions where Dispatch is not available. +// It is designed to safely elide away if and when Dispatch is introduced +// in the required Dispatch support becomes available. +#if os(WASI) && !canImport(Dispatch) +/// Drop-in replacement for ``Dispatch.DispatchTime``, implemented using pure swift. +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +public typealias DispatchTime = DispatchAsync.DispatchTime +#endif - /// The very first time someone intializes a DispatchTime instance, we - /// reference this static let, causing it to be initialized. - /// - /// This is the closest we can get to snapshotting the start time of the running - /// executable, without using OS-specific calls. We want - /// to avoid OS-specific calls to maximize portability. - /// - /// To keep this robust, we initialize `self.durationSinceBeginning` - /// to this value using a default value, which is guaranteed to run before any - /// initializers run. This guarantees that uptimeBeginning will be the very - /// first - @available(macOS 13, *) - private static let uptimeBeginning: ContinuousClock.Instant = ContinuousClock.Instant.now +extension DispatchAsync { + /// Drop-in replacement for ``Dispatch.DispatchTime``, implemented using pure swift. + #if !os(WASI) + @_spi(DispatchAsync) + #endif + @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) + public struct DispatchTime { + private let instant: ContinuousClock.Instant - /// See documentation for ``uptimeBeginning``. We intentionally - /// use this to guarantee a capture of `now` to uptimeBeginnin BEFORE - /// any DispatchTime instances are initialized. - private let durationSinceUptime = uptimeBeginning.duration(to: ContinuousClock.Instant.now) + /// The very first time someone intializes a DispatchTime instance, we + /// reference this static let, causing it to be initialized. + /// + /// This is the closest we can get to snapshotting the start time of the running + /// executable, without using OS-specific calls. We want + /// to avoid OS-specific calls to maximize portability. + /// + /// To keep this robust, we initialize `self.durationSinceBeginning` + /// to this value using a default value, which is guaranteed to run before any + /// initializers run. This guarantees that uptimeBeginning will be the very + /// first + @available(macOS 13, *) + private static let uptimeBeginning: ContinuousClock.Instant = ContinuousClock.Instant.now - public init() { - self.instant = ContinuousClock.Instant.now - } + /// See documentation for ``uptimeBeginning``. We intentionally + /// use this to guarantee a capture of `now` to uptimeBeginnin BEFORE + /// any DispatchTime instances are initialized. + private let durationSinceUptime = uptimeBeginning.duration(to: ContinuousClock.Instant.now) - public static func now() -> DispatchTime { - DispatchTime() - } + public init() { + self.instant = ContinuousClock.Instant.now + } + + public static func now() -> Self { + DispatchTime() + } - public var uptimeNanoseconds: UInt64 { - let beginning = DispatchTime.uptimeBeginning - let uptimeDuration: Int64 = beginning.duration(to: self.instant).nanosecondsClamped - guard uptimeDuration >= 0 else { - assertionFailure("It shouldn't be possible to get a negative duration since uptimeBeginning.") - return 0 + public var uptimeNanoseconds: UInt64 { + let beginning = DispatchTime.uptimeBeginning + let uptimeDuration: Int64 = beginning.duration(to: self.instant).nanosecondsClamped + guard uptimeDuration >= 0 else { + assertionFailure("It shouldn't be possible to get a negative duration since uptimeBeginning.") + return 0 + } + return UInt64(uptimeDuration) } - return UInt64(uptimeDuration) } } diff --git a/Sources/DispatchAsync/DispatchTimeInterval.swift b/Sources/DispatchAsync/DispatchTimeInterval.swift index 469d691..fa82f4c 100644 --- a/Sources/DispatchAsync/DispatchTimeInterval.swift +++ b/Sources/DispatchAsync/DispatchTimeInterval.swift @@ -12,58 +12,77 @@ // //===----------------------------------------------------------------------===// -/// NOTE: This is an excerpt from libDispatch, see -/// https://github.com/swiftlang/swift-corelibs-libdispatch/blob/main/src/swift/Time.swift#L168 -/// -/// Represents a time interval that can be used as an offset from a `DispatchTime` -/// or `DispatchWallTime`. -/// -/// For example: -/// let inOneSecond = DispatchTime.now() + DispatchTimeInterval.seconds(1) -/// -/// If the requested time interval is larger then the internal representation -/// permits, the result of adding it to a `DispatchTime` or `DispatchWallTime` -/// is `DispatchTime.distantFuture` and `DispatchWallTime.distantFuture` -/// respectively. Such time intervals compare as equal: -/// -/// let t1 = DispatchTimeInterval.seconds(Int.max) -/// let t2 = DispatchTimeInterval.milliseconds(Int.max) -/// let result = t1 == t2 // true -public enum DispatchTimeInterval: Equatable, Sendable { - case seconds(Int) - case milliseconds(Int) - case microseconds(Int) - case nanoseconds(Int) - case never +// NOTE: The following typealias mirrors Dispatch API's, but only for +// specific compilation conditions where Dispatch is not available. +// It is designed to safely elide away if and when Dispatch is introduced +// in the required Dispatch support becomes available. +#if os(WASI) && !canImport(Dispatch) +/// Drop-in replacement for ``Dispatch.DispatchQueue``, implemented using pure swift. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public typealias DispatchTimeInterval = DispatchAsync.DispatchTimeInterval +#endif - internal var rawValue: Int64 { - switch self { - case .seconds(let s): return clampedInt64Product(Int64(s), Int64(kNanosecondsPerSecond)) - case .milliseconds(let ms): return clampedInt64Product(Int64(ms), Int64(kNanosecondsPerMillisecond)) - case .microseconds(let us): return clampedInt64Product(Int64(us), Int64(kNanoSecondsPerMicrosecond)) - case .nanoseconds(let ns): return Int64(ns) - case .never: return Int64.max +extension DispatchAsync { + private static let kNanosecondsPerSecond: UInt64 = 1_000_000_000 + private static let kNanosecondsPerMillisecond: UInt64 = 1_000_000 + private static let kNanoSecondsPerMicrosecond: UInt64 = 1_000 + + /// NOTE: This is an excerpt from libDispatch, see + /// https://github.com/swiftlang/swift-corelibs-libdispatch/blob/main/src/swift/Time.swift#L168 + /// + /// Represents a time interval that can be used as an offset from a `DispatchTime` + /// or `DispatchWallTime`. + /// + /// For example: + /// let inOneSecond = DispatchTime.now() + DispatchTimeInterval.seconds(1) + /// + /// If the requested time interval is larger then the internal representation + /// permits, the result of adding it to a `DispatchTime` or `DispatchWallTime` + /// is `DispatchTime.distantFuture` and `DispatchWallTime.distantFuture` + /// respectively. Such time intervals compare as equal: + /// + /// let t1 = DispatchTimeInterval.seconds(Int.max) + /// let t2 = DispatchTimeInterval.milliseconds(Int.max) + /// let result = t1 == t2 // true + #if !os(WASI) + @_spi(DispatchAsync) + #endif + public enum DispatchTimeInterval: Equatable, Sendable { + case seconds(Int) + case milliseconds(Int) + case microseconds(Int) + case nanoseconds(Int) + case never + + internal var rawValue: Int64 { + switch self { + case .seconds(let s): return clampedInt64Product(Int64(s), Int64(kNanosecondsPerSecond)) + case .milliseconds(let ms): return clampedInt64Product(Int64(ms), Int64(kNanosecondsPerMillisecond)) + case .microseconds(let us): return clampedInt64Product(Int64(us), Int64(kNanoSecondsPerMicrosecond)) + case .nanoseconds(let ns): return Int64(ns) + case .never: return Int64.max + } } - } - public static func == (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool { - switch (lhs, rhs) { - case (.never, .never): return true - case (.never, _): return false - case (_, .never): return false - default: return lhs.rawValue == rhs.rawValue + public static func == (lhs: DispatchTimeInterval, rhs: DispatchTimeInterval) -> Bool { + switch (lhs, rhs) { + case (.never, .never): return true + case (.never, _): return false + case (_, .never): return false + default: return lhs.rawValue == rhs.rawValue + } } - } - // Returns m1 * m2, clamped to the range [Int64.min, Int64.max]. - // Because of the way this function is used, we can always assume - // that m2 > 0. - private func clampedInt64Product(_ m1: Int64, _ m2: Int64) -> Int64 { - assert(m2 > 0, "multiplier must be positive") - let (result, overflow) = m1.multipliedReportingOverflow(by: m2) - if overflow { - return m1 > 0 ? Int64.max : Int64.min + // Returns m1 * m2, clamped to the range [Int64.min, Int64.max]. + // Because of the way this function is used, we can always assume + // that m2 > 0. + private func clampedInt64Product(_ m1: Int64, _ m2: Int64) -> Int64 { + assert(m2 > 0, "multiplier must be positive") + let (result, overflow) = m1.multipliedReportingOverflow(by: m2) + if overflow { + return m1 > 0 ? Int64.max : Int64.min + } + return result } - return result } } diff --git a/Tests/DispatchAsyncTests/DispatchGroupTests.swift b/Tests/DispatchAsyncTests/DispatchGroupTests.swift index c1e622b..14d6ee1 100644 --- a/Tests/DispatchAsyncTests/DispatchGroupTests.swift +++ b/Tests/DispatchAsyncTests/DispatchGroupTests.swift @@ -14,7 +14,9 @@ import Testing -@testable import DispatchAsync +@_spi(DispatchAsync) import DispatchAsync + +private typealias DispatchGroup = DispatchAsync.DispatchGroup @Test func dispatchGroupOrderCleanliness() async throws { diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index d4e8c77..f89dc2c 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -14,7 +14,9 @@ import Testing -@testable import DispatchAsync +@_spi(DispatchAsync) import DispatchAsync + +private typealias DispatchQueue = DispatchAsync.DispatchQueue #if !os(WASI) import class Foundation.Thread diff --git a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift index 76ed242..d8e22bb 100644 --- a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift +++ b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift @@ -12,13 +12,20 @@ // //===----------------------------------------------------------------------===// +// TODO: SM: Rename this file to AsyncSemaphoreTests (coming in next PR that adds tests) + import Testing @testable import DispatchAsync +// NOTE: AsyncSempahore is nearly API-compatible with DispatchSemaphore, +// This typealias helps demonstrate that fact. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +private typealias DispatchSemaphore = AsyncSemaphore + nonisolated(unsafe) private var sharedPoolCompletionCount = 0 -@Test func basicDispatchSemaphoreTest() async throws { +@Test func basicAsyncSemaphoreTest() async throws { let totalConcurrentPools = 10 let semaphore = DispatchSemaphore(value: 1) diff --git a/Tests/DispatchAsyncTests/DispatchTimeTests.swift b/Tests/DispatchAsyncTests/DispatchTimeTests.swift index f0261cb..951ef9d 100644 --- a/Tests/DispatchAsyncTests/DispatchTimeTests.swift +++ b/Tests/DispatchAsyncTests/DispatchTimeTests.swift @@ -14,8 +14,12 @@ import Testing -@testable import DispatchAsync +@_spi(DispatchAsync) import DispatchAsync +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +private typealias DispatchTime = DispatchAsync.DispatchTime + +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) @Test func testDispatchTimeContinousClockBasics() async throws { let a = DispatchTime.now().uptimeNanoseconds From 671380d5cbb174fe739cbddf7928203786a569ae Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 14:44:22 -0600 Subject: [PATCH 43/53] refactor: Change copyright owner to PassiveLogic for now. --- Sources/DispatchAsync/AsyncSemaphore.swift | 2 +- Sources/DispatchAsync/DispatchAsync.swift | 2 +- Sources/DispatchAsync/DispatchGroup.swift | 2 +- Sources/DispatchAsync/DispatchQueue.swift | 2 +- Sources/DispatchAsync/DispatchSemaphore.swift | 2 +- Sources/DispatchAsync/DispatchTime.swift | 2 +- Sources/DispatchAsync/DispatchTimeInterval.swift | 2 +- Tests/DispatchAsyncTests/DispatchGroupTests.swift | 2 +- Tests/DispatchAsyncTests/DispatchQueueTests.swift | 2 +- Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift | 2 +- Tests/DispatchAsyncTests/DispatchTimeTests.swift | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/DispatchAsync/AsyncSemaphore.swift b/Sources/DispatchAsync/AsyncSemaphore.swift index b23f105..349696f 100644 --- a/Sources/DispatchAsync/AsyncSemaphore.swift +++ b/Sources/DispatchAsync/AsyncSemaphore.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/DispatchAsync/DispatchAsync.swift b/Sources/DispatchAsync/DispatchAsync.swift index 8ef63d2..271b33c 100644 --- a/Sources/DispatchAsync/DispatchAsync.swift +++ b/Sources/DispatchAsync/DispatchAsync.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/DispatchAsync/DispatchGroup.swift b/Sources/DispatchAsync/DispatchGroup.swift index 87a0e00..c7cab54 100644 --- a/Sources/DispatchAsync/DispatchGroup.swift +++ b/Sources/DispatchAsync/DispatchGroup.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift index 31cb240..e5e4748 100644 --- a/Sources/DispatchAsync/DispatchQueue.swift +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index b3c5095..af8f409 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/DispatchAsync/DispatchTime.swift b/Sources/DispatchAsync/DispatchTime.swift index 906a178..5b05fef 100644 --- a/Sources/DispatchAsync/DispatchTime.swift +++ b/Sources/DispatchAsync/DispatchTime.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/DispatchAsync/DispatchTimeInterval.swift b/Sources/DispatchAsync/DispatchTimeInterval.swift index fa82f4c..30bd49a 100644 --- a/Sources/DispatchAsync/DispatchTimeInterval.swift +++ b/Sources/DispatchAsync/DispatchTimeInterval.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/DispatchAsyncTests/DispatchGroupTests.swift b/Tests/DispatchAsyncTests/DispatchGroupTests.swift index 14d6ee1..d687703 100644 --- a/Tests/DispatchAsyncTests/DispatchGroupTests.swift +++ b/Tests/DispatchAsyncTests/DispatchGroupTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index f89dc2c..b2c6f34 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift index d8e22bb..4c35698 100644 --- a/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift +++ b/Tests/DispatchAsyncTests/DispatchSemaphoreTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/DispatchAsyncTests/DispatchTimeTests.swift b/Tests/DispatchAsyncTests/DispatchTimeTests.swift index 951ef9d..a29caa2 100644 --- a/Tests/DispatchAsyncTests/DispatchTimeTests.swift +++ b/Tests/DispatchAsyncTests/DispatchTimeTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Copyright (c) 2025 PassiveLogic, Inc. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information From 375a973d96d577b8931cf74d599e1759c2c509b8 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 14:48:22 -0600 Subject: [PATCH 44/53] chore: Run swift-format --- Package.swift | 2 +- Sources/DispatchAsync/DispatchQueue.swift | 3 +-- Sources/DispatchAsync/DispatchSemaphore.swift | 7 ++++--- Tests/DispatchAsyncTests/DispatchGroupTests.swift | 3 +-- Tests/DispatchAsyncTests/DispatchQueueTests.swift | 3 +-- Tests/DispatchAsyncTests/DispatchTimeTests.swift | 3 +-- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Package.swift b/Package.swift index e75b7c0..f3352e6 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library( name: "DispatchAsync", targets: ["DispatchAsync"] - ), + ) ], targets: [ .target( diff --git a/Sources/DispatchAsync/DispatchQueue.swift b/Sources/DispatchAsync/DispatchQueue.swift index e5e4748..74c74ab 100644 --- a/Sources/DispatchAsync/DispatchQueue.swift +++ b/Sources/DispatchAsync/DispatchQueue.swift @@ -149,8 +149,7 @@ extension DispatchAsync { /// Enqueue a new unit of work. @discardableResult - nonisolated - func enqueue(_ workItem: @escaping WorkItem) -> AsyncStream.Continuation.YieldResult { + nonisolated func enqueue(_ workItem: @escaping WorkItem) -> AsyncStream.Continuation.YieldResult { // Never suspends, preserves order continuation.yield(workItem) } diff --git a/Sources/DispatchAsync/DispatchSemaphore.swift b/Sources/DispatchAsync/DispatchSemaphore.swift index af8f409..82eae92 100644 --- a/Sources/DispatchAsync/DispatchSemaphore.swift +++ b/Sources/DispatchAsync/DispatchSemaphore.swift @@ -48,9 +48,10 @@ extension DispatchAsync { #endif @available( *, - deprecated, - renamed: "AsyncSemaphore", - message: "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." + deprecated, + renamed: "AsyncSemaphore", + message: + "DispatchSemaphore.wait is dangerous because of it's thread-blocking nature. Use AsyncSemaphore and Swift Concurrency instead." ) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public class DispatchSemaphore: @unchecked Sendable { diff --git a/Tests/DispatchAsyncTests/DispatchGroupTests.swift b/Tests/DispatchAsyncTests/DispatchGroupTests.swift index d687703..eb1b4c3 100644 --- a/Tests/DispatchAsyncTests/DispatchGroupTests.swift +++ b/Tests/DispatchAsyncTests/DispatchGroupTests.swift @@ -12,9 +12,8 @@ // //===----------------------------------------------------------------------===// -import Testing - @_spi(DispatchAsync) import DispatchAsync +import Testing private typealias DispatchGroup = DispatchAsync.DispatchGroup diff --git a/Tests/DispatchAsyncTests/DispatchQueueTests.swift b/Tests/DispatchAsyncTests/DispatchQueueTests.swift index b2c6f34..721567e 100644 --- a/Tests/DispatchAsyncTests/DispatchQueueTests.swift +++ b/Tests/DispatchAsyncTests/DispatchQueueTests.swift @@ -12,9 +12,8 @@ // //===----------------------------------------------------------------------===// -import Testing - @_spi(DispatchAsync) import DispatchAsync +import Testing private typealias DispatchQueue = DispatchAsync.DispatchQueue diff --git a/Tests/DispatchAsyncTests/DispatchTimeTests.swift b/Tests/DispatchAsyncTests/DispatchTimeTests.swift index a29caa2..517b73f 100644 --- a/Tests/DispatchAsyncTests/DispatchTimeTests.swift +++ b/Tests/DispatchAsyncTests/DispatchTimeTests.swift @@ -12,9 +12,8 @@ // //===----------------------------------------------------------------------===// -import Testing - @_spi(DispatchAsync) import DispatchAsync +import Testing @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) private typealias DispatchTime = DispatchAsync.DispatchTime From 205d5a42fb03904c601e8275ed17f523eac592f8 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 14:52:17 -0600 Subject: [PATCH 45/53] chore: Add permalink to copy-pasted file. --- Sources/DispatchAsync/DispatchTime.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/DispatchAsync/DispatchTime.swift b/Sources/DispatchAsync/DispatchTime.swift index 5b05fef..d044e36 100644 --- a/Sources/DispatchAsync/DispatchTime.swift +++ b/Sources/DispatchAsync/DispatchTime.swift @@ -70,7 +70,10 @@ extension DispatchAsync { } } -// NOTE: The following was copied from swift-nio/Source/NIOCore/TimeAmount+Duration on June 27, 2025 +// NOTE: The following was copied from swift-nio/Source/NIOCore/TimeAmount+Duration on June 27, 2025. +// +// See https://github.com/apple/swift-nio/blob/83bc5b58440373a7678b56fa0d9cc22ca55297ee/Sources/NIOCore/TimeAmount%2BDuration.swift +// // It was copied rather than brought via dependencies to avoid introducing // a dependency on swift-nio for such a small piece of code. // From e507fa430f1339b0f8eb514ec68b93c38948bb73 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 15:33:35 -0600 Subject: [PATCH 46/53] docs: Add usage notes with plenty of warnings. --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 898693e..0e698ef 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ into the toolchain only for wasm targets. Ideally, with either approach, this repository would transfer ownership to the swiftlang organization. +In the interim, to move wasm support forward, portions of DispatchAsync may be inlined (copy-pasted) +into various libraries to enable wasm support. DispatchAsync is designed for this purpose, and has +special `#if` handling to ensure that existing temporary usages will be elided without breakage +the moment SwiftWasm adds support for `Dispatch` into the toolchain. + # DispatchSemaphore Limitations The current implementation of `DispatchSemaphore` has some limitations. Blocking threads goes against the design goals of Swift Concurrency. @@ -37,4 +42,94 @@ be needed for multi-threaded execution. This makes the implementation much easie - For wasm targets, calls to `signal` and `wait` must be balanced. An assertion triggers if `wait` is called more times than `signal`. - DispatchSemaphore is deprecated for wasm targets, and AsyncSemaphore is encouraged as the replacement. - For non-wasm targets, DispatchSemaphore is simply a typealias for `AsyncSemaphore`, and provides only a non-blocking async `wait` -function. This reduces potential issues that can arise from wait being a thread-blocking function. +function. This reduces potential issues that can arise from wait being a thread-blocking function. + +# Usage + +If you've scrolled this far, you probably saw the warning. But just to make sure… + +> ⚠️ WARNING - This is an πŸ§ͺexperimentalπŸ§ͺ repository and should not be adopted at large. + +PassiveLogic is [actively working](https://github.com/PassiveLogic/swift-web-examples/issues/1) to mainstream this into the SwiftWasm +toolchain. But if you can't wait, here are some tips. + +## 1. Only use this for WASI platforms, and only if Dispatch cannot be imported. + +Use `#if os(WASI) && !canImport(Dispatch)` to elide usages outside of WASI platforms: + +```swift +#if os(WASI) && !canImport(Dispatch) +import DispatchAsync +#else +import Dispatch +#endif + +// Use Dispatch API's the same way you normal would. +``` + +## 2. If you really want to use DispatchAsync as a pure swift Dispatch alternative for non-wasm targets + +Stop. Are you sure? If you do this, you'll need to be ' + +1. Add the dependency to your package: + +```swift +let package = Package( + name: "MyPackage", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "MyPackage", + targets: [ + "MyPackage" + ] + ), + ], + dependencies: [ + .package( + url: "https://github.com/PassiveLogic/dispatch-async.git", + from: "0.0.1" + ), + ], + targets: [ + .target( + name: "MyPackage" + dependencies: [ + "DispatchAsync" + ] + ), + ] +) +``` + +2. Import and use DispatchAsync in place of Dispatch like this: + +```swift +#if os(WASI) && !canImport(Dispatch) +import DispatchAsync +#else +// Non-WASI platforms have to explicitly bring in DispatchAsync +// by using `@_spi`. +@_spi(DispatchAsync) import DispatchAsync +#endif + +// Not allowed: +// import Dispatch + +// Also Not allowed: +// import Foundation + +// You'll need to use scoped Foundation imports: +import struct Foundation.URL // Ok. Doesn't bring in Dispatch + +// If you ignore the above notes, but do the following, be prepared for namespace +// collisions between the toolchain's Dispatch and DispatchAsync: + +private typealias DispatchQueue = DispatchAsync.DispatchQueue + +// Ok. If you followed everything above, you can now do the following, using pure swift +// under the hood! πŸŽ‰ +DispatchQueue.main.async { + // Run your code here… +} +``` From c008610890d1b18c8cd64d0005c2127b4b6cebdf Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 15:33:45 -0600 Subject: [PATCH 47/53] docs: Add info about license. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 0e698ef..4c02ddf 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,9 @@ DispatchQueue.main.async { // Run your code here… } ``` + +# LICENSE + +This project is distributed by PassiveLogic under the Apache-2.0 license. See +[LICENSE](https://github.com/PassiveLogic/dispatch-async/blob/main/LICENSE) for full terms of use. + From 29589c50710c4539f0da993936e237838f098ed9 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 15:41:36 -0600 Subject: [PATCH 48/53] ci: Disable license header check in CI for now. --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e41d131..ba8a384 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: - license_header_check_project_name: "Swift.org" + license_header_check_enabled: false api_breakage_check_enabled: false tests: From cc6c3873ba42935fe7bf88f982a262cdf5afb88c Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 15:41:53 -0600 Subject: [PATCH 49/53] chore: See if this address weird lint issue popping up in CI. --- Package.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index f3352e6..47c7e95 100644 --- a/Package.swift +++ b/Package.swift @@ -11,9 +11,7 @@ let package = Package( ) ], targets: [ - .target( - name: "DispatchAsync", - ), + .target(name: "DispatchAsync"), .testTarget( name: "DispatchAsyncTests", dependencies: [ From cfe3db385a267e52a2e5dd00689bcbb713ebd45c Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 15:44:22 -0600 Subject: [PATCH 50/53] chore: Address lint error for Package.swift:19:15: error: expected value in function call --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 47c7e95..369fd85 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( name: "DispatchAsyncTests", dependencies: [ "DispatchAsync" - ], + ] ), ] ) From bc50dff7cfd271f26dd5d3f10528a94f6ecbb257 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 16:12:15 -0600 Subject: [PATCH 51/53] ci: Use a later version of swift for the swift-format check. This shoudl resolve discrepancies between local swift-format and CI-based swift-format. --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ba8a384..4c5197c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,6 +9,7 @@ jobs: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: + format_check_container_image: swift:6.1.0-noble license_header_check_enabled: false api_breakage_check_enabled: false From e878eb94e01394400b0911eb992c38327e403b3c Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 16:16:26 -0600 Subject: [PATCH 52/53] ci: Updating swift-format rule for swift 6.1.0 instead of swift 6.2. --- .swift-format | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.swift-format b/.swift-format index a289d85..b66e4e1 100644 --- a/.swift-format +++ b/.swift-format @@ -21,13 +21,18 @@ ] }, "prioritizeKeepingFunctionOutputTogether" : false, - "reflowMultilineStringLiterals" : "never", + "reflowMultilineStringLiterals" : { + "never" : { + + } + }, "respectsExistingLineBreaks" : true, "rules" : { "AllPublicDeclarationsHaveDocumentation" : false, "AlwaysUseLiteralForEmptyCollectionInit" : false, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : true, + "AvoidRetroactiveConformances" : true, "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, "DontRepeatTypeInStaticProperties" : true, @@ -42,6 +47,7 @@ "NoAssignmentInExpressions" : true, "NoBlockComments" : true, "NoCasesWithOnlyFallthrough" : true, + "NoEmptyLinesOpeningClosingBraces" : false, "NoEmptyTrailingClosureParentheses" : true, "NoLabelsInCasePatterns" : true, "NoLeadingUnderscores" : false, From 0cfc4e3457af7cb65a676c44be43e8e33218146d Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 9 Jul 2025 17:01:00 -0600 Subject: [PATCH 53/53] chore: Update a few more file headers. --- format.sh | 2 +- lint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/format.sh b/format.sh index 41690c4..bf7af48 100755 --- a/format.sh +++ b/format.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the Swift.org open source project ## -## Copyright (c) 2025 Apple Inc. and the Swift.org project authors +## Copyright (c) 2025 PassiveLogic, Inc. ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information diff --git a/lint.sh b/lint.sh index ee8e084..bc33c5f 100755 --- a/lint.sh +++ b/lint.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the Swift.org open source project ## -## Copyright (c) 2025 Apple Inc. and the Swift.org project authors +## Copyright (c) 2025 PassiveLogic, Inc. ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information