From 1a8946bb68398f09d8670b0d961c13e2f5936f2f Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Wed, 25 Jun 2025 15:27:37 -0600 Subject: [PATCH 01/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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