Skip to content

feat: Initial implementation of many GCD API's using Swift Concurrency #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1a8946b
chore: refine gitignore
scottmarchant Jun 25, 2025
940349b
chore: Set up initial swift package manifest file.
scottmarchant Jun 25, 2025
d90e8b8
feat: Implement DispatchQueue using Swift Concurrency.
scottmarchant Jun 27, 2025
2ecbb15
feat: Implement DispatchGroup using Swift Concurrency.
scottmarchant Jun 27, 2025
5108dec
feat: Implement DispatchSemaphore using Swift Concurrency.
scottmarchant Jun 27, 2025
d98950c
feat: Implement DispatchTime using Swift Concurrency.
scottmarchant Jun 27, 2025
64036b9
feat: Implement DispatchTimeInterval using Swift Concurrency.
scottmarchant Jun 27, 2025
40b798d
chore: Add some basic testing.
scottmarchant Jun 27, 2025
da07ce2
chore: Update Readme.
scottmarchant Jun 27, 2025
aed4dd6
ci: Add pull request CI workflows.
scottmarchant Jun 27, 2025
f8d723e
chore: Silence some lint that is intentionally written this way to ma…
scottmarchant Jun 27, 2025
dae1d1d
ci: Disable api breakage check for now.
scottmarchant Jun 27, 2025
e2e3b83
ci: Don't test swift versions before 6.1
scottmarchant Jun 27, 2025
b5abf14
chore: Update file headers
scottmarchant Jun 27, 2025
ec5c7da
chore: Changing wording in readme.
scottmarchant Jun 27, 2025
c87843c
chore: Ignore missing license header in Package.swift file.
scottmarchant Jun 27, 2025
1a3e651
chore: Clean up lint a different way.
scottmarchant Jun 27, 2025
91599e9
ci: update test targets
scottmarchant Jun 27, 2025
f78c0d5
ci: Add wasm sdk installation script.
scottmarchant Jun 27, 2025
e3af2d2
chore: Fix license header format in bash script.
scottmarchant Jun 27, 2025
1e8b322
chore: Update swift-format rules.
scottmarchant Jun 27, 2025
f64e771
chore: Add convenience scripts to run the same commands CI uses for s…
scottmarchant Jun 27, 2025
f38c234
chore: Fix license setup for soundness checks.
scottmarchant Jun 30, 2025
716c184
chore: Removing lint rule definitions not recognized by github online…
scottmarchant Jun 30, 2025
b698b1b
ci: Don't run tests on Swift 5.10 either. Not supported.
scottmarchant Jun 30, 2025
fac6c98
fix: Fix potential main thread issue in DispatchQueue that currently …
scottmarchant Jun 30, 2025
2c92ea9
ci: Try a slightly different mechanism to get the swift version, to a…
scottmarchant Jun 30, 2025
67011b5
chore: update scripting
scottmarchant Jun 30, 2025
1f4c5c8
chore: Fix lint that CI wants one way, and local install wants a diff…
scottmarchant Jun 30, 2025
aa4565b
ci: Use my own bash adapted from Yuta Saito's open MR to build wasm f…
scottmarchant Jun 30, 2025
cada3e8
ci: move scripts inline to yml configuration to work around issues wi…
scottmarchant Jun 30, 2025
69e8aee
test: Update unit tests to adjust expectations for linux targets. Lin…
scottmarchant Jun 30, 2025
e2ee220
ci: Install jq for wasm builds.
scottmarchant Jun 30, 2025
9ad3a89
fix: Fix unit test expectations for linux. Take two.
scottmarchant Jun 30, 2025
4ac2494
chore: lint
scottmarchant Jun 30, 2025
e271490
ci: wasm build needs to clone the code before it can build.
scottmarchant Jun 30, 2025
0950a4f
ci: use Swift 6.1 for wasm build.
scottmarchant Jun 30, 2025
a29c5aa
ci: Specifical swift 6.1.0 for wasm builds, not swift 6.1.2.
scottmarchant Jun 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Pull request
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, I plan to add much more extensive testing in my next PR for this repo coming the in the next week or two. I did add some basic tests just for sanity, though.

I've also tested this manually in consumption from several different repositories, and in the browser runtime in a wasm build.

It works.


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"
api_breakage_check_enabled: false

tests:
name: tests
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\"}, {\"swift_version\": \"5.10.1\"}, {\"swift_version\": \"6.0\"}]"
enable_windows_checks: false

wasm-sdk:
name: WebAssembly SDK
runs-on: ubuntu-latest
container:
image: "swift:6.1.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: |
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
swift build --swift-sdk wasm32-unknown-wasi
68 changes: 7 additions & 61 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .licenseignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Package.swift
73 changes: 73 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"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,
"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,
"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
}
File renamed without changes.
20 changes: 20 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"]
),
]
)
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
# dispatch-async
# dispatch-async
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be a good idea to read this readme first, before reviewing the rest of the code in this PR. I explain a bit here.


## ⚠️ 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 freeze the 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.
56 changes: 56 additions & 0 deletions Sources/DispatchAsync/AsyncSemaphore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// 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: [CheckedContinuation<Void, Never>] = []

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<T: Sendable>(_ 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()
}
}
Loading