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 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 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
33536b0
chore: Fix incomplete comment.
scottmarchant Jul 3, 2025
9df1a12
fix: Fix a variety of issues found in DispatchAsync while implementin…
scottmarchant Jul 8, 2025
2fb14f7
chore: Use swift version 6.0 as the minimum rather than 6.1.
scottmarchant Jul 8, 2025
5ba8280
refactor: Update ifdefs and @_spi guards to allow development against…
scottmarchant Jul 9, 2025
671380d
refactor: Change copyright owner to PassiveLogic for now.
scottmarchant Jul 9, 2025
375a973
chore: Run swift-format
scottmarchant Jul 9, 2025
205d5a4
chore: Add permalink to copy-pasted file.
scottmarchant Jul 9, 2025
e507fa4
docs: Add usage notes with plenty of warnings.
scottmarchant Jul 9, 2025
c008610
docs: Add info about license.
scottmarchant Jul 9, 2025
29589c5
ci: Disable license header check in CI for now.
scottmarchant Jul 9, 2025
cc6c387
chore: See if this address weird lint issue popping up in CI.
scottmarchant Jul 9, 2025
cfe3db3
chore: Address lint error for Package.swift:19:15: error: expected va…
scottmarchant Jul 9, 2025
bc50dff
ci: Use a later version of swift for the swift-format check. This sho…
scottmarchant Jul 9, 2025
e878eb9
ci: Updating swift-format rule for swift 6.1.0 instead of swift 6.2.
scottmarchant Jul 9, 2025
0cfc4e3
chore: Update a few more file headers.
scottmarchant Jul 9, 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
43 changes: 43 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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:
format_check_container_image: swift:6.1.0-noble
license_header_check_enabled: false
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
79 changes: 79 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"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
}
File renamed without changes.
22 changes: 22 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "dispatch-async",
products: [
.library(
name: "DispatchAsync",
targets: ["DispatchAsync"]
)
],
targets: [
.target(name: "DispatchAsync"),
.testTarget(
name: "DispatchAsyncTests",
dependencies: [
"DispatchAsync"
]
),
]
)
142 changes: 141 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,141 @@
# 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.

In the interim, to move wasm support forward, portions of DispatchAsync may be inlined (copy-pasted)
into various libraries to enable wasm support. DispatchAsync is designed for this purpose, and has
special `#if` handling to ensure that existing temporary usages will be elided without breakage
the moment SwiftWasm adds support for `Dispatch` into the toolchain.

# DispatchSemaphore Limitations

The current implementation of `DispatchSemaphore` has some limitations. Blocking threads goes against the design goals of Swift Concurrency.
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.

# Usage

If you've scrolled this far, you probably saw the warning. But just to make sure…

> ⚠️ WARNING - This is an 🧪experimental🧪 repository and should not be adopted at large.

PassiveLogic is [actively working](https://github.com/PassiveLogic/swift-web-examples/issues/1) to mainstream this into the SwiftWasm
toolchain. But if you can't wait, here are some tips.

## 1. Only use this for WASI platforms, and only if Dispatch cannot be imported.

Use `#if os(WASI) && !canImport(Dispatch)` to elide usages outside of WASI platforms:

```swift
#if os(WASI) && !canImport(Dispatch)
import DispatchAsync
#else
import Dispatch
#endif

// Use Dispatch API's the same way you normal would.
```

## 2. If you really want to use DispatchAsync as a pure swift Dispatch alternative for non-wasm targets

Stop. Are you sure? If you do this, you'll need to be '

1. Add the dependency to your package:

```swift
let package = Package(
name: "MyPackage",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "MyPackage",
targets: [
"MyPackage"
]
),
],
dependencies: [
.package(
url: "https://github.com/PassiveLogic/dispatch-async.git",
from: "0.0.1"
),
],
targets: [
.target(
name: "MyPackage"
dependencies: [
"DispatchAsync"
]
),
]
)
```

2. Import and use DispatchAsync in place of Dispatch like this:

```swift
#if os(WASI) && !canImport(Dispatch)
import DispatchAsync
#else
// Non-WASI platforms have to explicitly bring in DispatchAsync
// by using `@_spi`.
@_spi(DispatchAsync) import DispatchAsync
#endif

// Not allowed:
// import Dispatch

// Also Not allowed:
// import Foundation

// You'll need to use scoped Foundation imports:
import struct Foundation.URL // Ok. Doesn't bring in Dispatch

// If you ignore the above notes, but do the following, be prepared for namespace
// collisions between the toolchain's Dispatch and DispatchAsync:

private typealias DispatchQueue = DispatchAsync.DispatchQueue

// Ok. If you followed everything above, you can now do the following, using pure swift
// under the hood! 🎉
DispatchQueue.main.async {
// Run your code here…
}
```

# LICENSE

This project is distributed by PassiveLogic under the Apache-2.0 license. See
[LICENSE](https://github.com/PassiveLogic/dispatch-async/blob/main/LICENSE) for full terms of use.

Loading