Skip to content

Commit 41f71ae

Browse files
feat: Initial implementation of many GCD API's using Swift Concurrency (#1)
* chore: refine gitignore * chore: Set up initial swift package manifest file. * feat: Implement DispatchQueue using Swift Concurrency. * feat: Implement DispatchGroup using Swift Concurrency. * feat: Implement DispatchSemaphore using Swift Concurrency. * feat: Implement DispatchTime using Swift Concurrency. * feat: Implement DispatchTimeInterval using Swift Concurrency. * chore: Add some basic testing. * chore: Update Readme. * ci: Add pull request CI workflows. * chore: Silence some lint that is intentionally written this way to match constansts in libDispatch. * ci: Disable api breakage check for now. * ci: Don't test swift versions before 6.1 * chore: Update file headers * chore: Changing wording in readme. * chore: Ignore missing license header in Package.swift file. * chore: Clean up lint a different way. * ci: update test targets * ci: Add wasm sdk installation script. * chore: Fix license header format in bash script. * chore: Update swift-format rules. * chore: Add convenience scripts to run the same commands CI uses for swift-format and swift-lint. * chore: Fix license setup for soundness checks. * chore: Removing lint rule definitions not recognized by github online swift-format version. * ci: Don't run tests on Swift 5.10 either. Not supported. * fix: Fix potential main thread issue in DispatchQueue that currently only seems to show up in linux. * ci: Try a slightly different mechanism to get the swift version, to attempt to resolve issues showing up in CI for wasm builds. * chore: update scripting * 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. * ci: Use my own bash adapted from Yuta Saito's open MR to build wasm for now. * ci: move scripts inline to yml configuration to work around issues with script pathing in CI. * 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. * ci: Install jq for wasm builds. * fix: Fix unit test expectations for linux. Take two. * chore: lint * ci: wasm build needs to clone the code before it can build. * ci: use Swift 6.1 for wasm build. * ci: Specifical swift 6.1.0 for wasm builds, not swift 6.1.2. * chore: Fix incomplete comment. * fix: Fix a variety of issues found in DispatchAsync while implementing new tests. * chore: Use swift version 6.0 as the minimum rather than 6.1. * refactor: Update ifdefs and @_spi guards to allow development against traditional iOS targets, but to elide exact Dispatch api replacements for any current or future target that does not include Dispatch. * refactor: Change copyright owner to PassiveLogic for now. * chore: Run swift-format * chore: Add permalink to copy-pasted file. * docs: Add usage notes with plenty of warnings. * docs: Add info about license. * ci: Disable license header check in CI for now. * chore: See if this address weird lint issue popping up in CI. * chore: Address lint error for Package.swift:19:15: error: expected value in function call * ci: Use a later version of swift for the swift-format check. This shoudl resolve discrepancies between local swift-format and CI-based swift-format. * ci: Updating swift-format rule for swift 6.1.0 instead of swift 6.2. * chore: Update a few more file headers. * chore: Update swift to Swift in comments Co-authored-by: Mark Pospesel <[email protected]> * chore: update more swift vs Swift language * chore: Parameterize number of iterations for critical unit test. * chore: Fix typo. * refactor: rename package to follow existing patterns in Apple's Swift repositories, to make this as easy as possible for them to adopt. * chore: refine documentation.
1 parent 18e0338 commit 41f71ae

20 files changed

+1264
-62
lines changed

.github/workflows/pull_request.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Pull request
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, synchronize]
6+
7+
jobs:
8+
soundness:
9+
name: Soundness
10+
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
11+
with:
12+
format_check_container_image: swift:6.1.0-noble
13+
license_header_check_enabled: false
14+
api_breakage_check_enabled: false
15+
16+
tests:
17+
name: tests
18+
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
19+
with:
20+
enable_macos_checks: false
21+
linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"5.10.1\"}, {\"swift_version\": \"6.0\"}]"
22+
enable_windows_checks: false
23+
24+
wasm-sdk:
25+
name: WebAssembly SDK
26+
runs-on: ubuntu-latest
27+
container:
28+
image: "swift:6.1.0-noble"
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@v4
32+
- name: Swift version
33+
run: swift --version
34+
- name: WasmBuild
35+
# TODO: Update this to use swift-nio once https://github.com/apple/swift-nio/pull/3159/ is merged
36+
run: |
37+
apt-get update -y -q
38+
apt-get install -y -q curl
39+
apt-get install -y -q jq
40+
version="$(swift --version | head -n1)"
41+
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]')"
42+
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
43+
swift build --swift-sdk wasm32-unknown-wasi

.gitignore

Lines changed: 7 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,8 @@
1-
# Xcode
2-
#
3-
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4-
5-
## User settings
1+
.DS_Store
2+
/.build
3+
/Packages
64
xcuserdata/
7-
8-
## Obj-C/Swift specific
9-
*.hmap
10-
11-
## App packaging
12-
*.ipa
13-
*.dSYM.zip
14-
*.dSYM
15-
16-
## Playgrounds
17-
timeline.xctimeline
18-
playground.xcworkspace
19-
20-
# Swift Package Manager
21-
#
22-
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
23-
# Packages/
24-
# Package.pins
25-
# Package.resolved
26-
# *.xcodeproj
27-
#
28-
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
29-
# hence it is not needed unless you have added a package configuration file to your project
30-
# .swiftpm
31-
32-
.build/
33-
34-
# CocoaPods
35-
#
36-
# We recommend against adding the Pods directory to your .gitignore. However
37-
# you should judge for yourself, the pros and cons are mentioned at:
38-
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
39-
#
40-
# Pods/
41-
#
42-
# Add this line if you want to avoid checking in source code from the Xcode workspace
43-
# *.xcworkspace
44-
45-
# Carthage
46-
#
47-
# Add this line if you want to avoid checking in source code from Carthage dependencies.
48-
# Carthage/Checkouts
49-
50-
Carthage/Build/
51-
52-
# fastlane
53-
#
54-
# It is recommended to not store the screenshots in the git repo.
55-
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
56-
# For more information about the recommended setup visit:
57-
# https://docs.fastlane.tools/best-practices/source-control/#source-control
58-
59-
fastlane/report.xml
60-
fastlane/Preview.html
61-
fastlane/screenshots/**/*.png
62-
fastlane/test_output
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

.licenseignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Package.swift

.swift-format

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"fileScopedDeclarationPrivacy" : {
3+
"accessLevel" : "private"
4+
},
5+
"indentConditionalCompilationBlocks" : false,
6+
"indentSwitchCaseLabels" : false,
7+
"indentation" : {
8+
"spaces" : 4
9+
},
10+
"lineBreakAroundMultilineExpressionChainComponents" : false,
11+
"lineBreakBeforeControlFlowKeywords" : false,
12+
"lineBreakBeforeEachArgument" : false,
13+
"lineBreakBeforeEachGenericRequirement" : false,
14+
"lineBreakBetweenDeclarationAttributes" : false,
15+
"lineLength" : 140,
16+
"maximumBlankLines" : 1,
17+
"multiElementCollectionTrailingCommas" : true,
18+
"noAssignmentInExpressions" : {
19+
"allowedFunctions" : [
20+
"XCTAssertNoThrow"
21+
]
22+
},
23+
"prioritizeKeepingFunctionOutputTogether" : false,
24+
"reflowMultilineStringLiterals" : {
25+
"never" : {
26+
27+
}
28+
},
29+
"respectsExistingLineBreaks" : true,
30+
"rules" : {
31+
"AllPublicDeclarationsHaveDocumentation" : false,
32+
"AlwaysUseLiteralForEmptyCollectionInit" : false,
33+
"AlwaysUseLowerCamelCase" : true,
34+
"AmbiguousTrailingClosureOverload" : true,
35+
"AvoidRetroactiveConformances" : true,
36+
"BeginDocumentationCommentWithOneLineSummary" : false,
37+
"DoNotUseSemicolons" : true,
38+
"DontRepeatTypeInStaticProperties" : true,
39+
"FileScopedDeclarationPrivacy" : true,
40+
"FullyIndirectEnum" : true,
41+
"GroupNumericLiterals" : true,
42+
"IdentifiersMustBeASCII" : true,
43+
"NeverForceUnwrap" : false,
44+
"NeverUseForceTry" : false,
45+
"NeverUseImplicitlyUnwrappedOptionals" : false,
46+
"NoAccessLevelOnExtensionDeclaration" : true,
47+
"NoAssignmentInExpressions" : true,
48+
"NoBlockComments" : true,
49+
"NoCasesWithOnlyFallthrough" : true,
50+
"NoEmptyLinesOpeningClosingBraces" : false,
51+
"NoEmptyTrailingClosureParentheses" : true,
52+
"NoLabelsInCasePatterns" : true,
53+
"NoLeadingUnderscores" : false,
54+
"NoParensAroundConditions" : true,
55+
"NoPlaygroundLiterals" : true,
56+
"NoVoidReturnOnFunctionSignature" : true,
57+
"OmitExplicitReturns" : false,
58+
"OneCasePerLine" : true,
59+
"OneVariableDeclarationPerLine" : true,
60+
"OnlyOneTrailingClosureArgument" : true,
61+
"OrderedImports" : true,
62+
"ReplaceForEachWithForLoop" : true,
63+
"ReturnVoidInsteadOfEmptyTuple" : true,
64+
"TypeNamesShouldBeCapitalized" : true,
65+
"UseEarlyExits" : false,
66+
"UseExplicitNilCheckInConditions" : true,
67+
"UseLetInEveryBoundCaseVariable" : true,
68+
"UseShorthandTypeNames" : true,
69+
"UseSingleLinePropertyGetter" : true,
70+
"UseSynthesizedInitializer" : true,
71+
"UseTripleSlashForDocumentationComments" : true,
72+
"UseWhereClausesInForLoops" : false,
73+
"ValidateDocumentationComments" : false
74+
},
75+
"spacesAroundRangeFormationOperators" : true,
76+
"spacesBeforeEndOfLineComments" : 1,
77+
"tabWidth" : 8,
78+
"version" : 1
79+
}
File renamed without changes.

Package.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-dispatch-async",
7+
products: [
8+
.library(
9+
name: "DispatchAsync",
10+
targets: ["DispatchAsync"]
11+
)
12+
],
13+
targets: [
14+
.target(name: "DispatchAsync"),
15+
.testTarget(
16+
name: "DispatchAsyncTests",
17+
dependencies: [
18+
"DispatchAsync"
19+
]
20+
),
21+
]
22+
)

README.md

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,140 @@
1-
# dispatch-async
1+
# swift-dispatch-async
2+
3+
## ⚠️ WARNING - This is an 🧪experimental🧪 repository and should not be adopted at large.
4+
5+
DispatchAsync is a temporary experimental repository aimed at implementing missing Dispatch support in the SwiftWasm toolchain.
6+
Currently, [SwiftWasm doesn't include Dispatch](https://book.swiftwasm.org/getting-started/porting.html#swift-foundation-and-dispatch).
7+
But, SwiftWasm does support Swift Concurrency. DispatchAsync implements a number of common Dispatch API's using Swift Concurrency
8+
under the hood.
9+
10+
Dispatch Async does not provide blocking API's such as `DispatchQueue.sync`, primarily due to the intentional lack of blocking
11+
API's in Swift Concurrency.
12+
13+
# Toolchain Adoption Plans
14+
15+
DispatchAsync is not meant for consumption abroad directly as a new Swift Module. Rather, the intention is to provide eventual integration
16+
as a drop-in replacement for Dispatch when compiling to Wasm.
17+
18+
There are a few paths to adoption into the Swift toolchain
19+
20+
- DispatchAsync can be emplaced inside the [libDispatch repository](https://github.com/swiftlang/swift-corelibs-libdispatch), and compiled
21+
into the toolchain only for wasm targets.
22+
- DispatchAsync can be consumed in place of libDispatch when building the Swift toolchain.
23+
24+
Ideally, with either approach, this repository would transfer ownership to the swiftlang organization.
25+
26+
In the interim, to move wasm support forward, portions of DispatchAsync may be inlined (copy-pasted)
27+
into various libraries to enable wasm support. DispatchAsync is designed for this purpose, and has
28+
special `#if` handling to ensure that existing temporary usages will be elided without breakage
29+
the moment SwiftWasm adds support for `Dispatch` into the toolchain.
30+
31+
# DispatchSemaphore Limitations
32+
33+
The current implementation of `DispatchSemaphore` has some limitations. Blocking threads goes against the design goals of Swift Concurrency.
34+
The `wait` function on `DispatchSemaphore` goes against this goal. Furthermore, most wasm targets run on a single thread from the web
35+
browser, so any time the `wait` function ends up blocking the calling thread, it would almost certainly freeze the single-threaded wasm
36+
executable.
37+
38+
To navigate these issues, there are some limitations:
39+
40+
- For wasm compilation targets, `DispatchSemaphore` assumes single-threaded execution, and lacks various safeguards that would otherwise
41+
be needed for multi-threaded execution. This makes the implementation much easier.
42+
- For wasm targets, calls to `signal` and `wait` must be balanced. An assertion triggers if `wait` is called more times than `signal`.
43+
- DispatchSemaphore is deprecated for wasm targets, and AsyncSemaphore is encouraged as the replacement.
44+
- For non-wasm targets, DispatchSemaphore is simply a typealias for `AsyncSemaphore`, and provides only a non-blocking async `wait`
45+
function. This reduces potential issues that can arise from wait being a thread-blocking function.
46+
47+
# Usage
48+
49+
If you've scrolled this far, you probably saw the warning. But just to make sure…
50+
51+
> ⚠️ WARNING - This is an 🧪experimental🧪 repository and should not be adopted at large at the present time.
52+
53+
PassiveLogic is [actively working](https://github.com/PassiveLogic/swift-web-examples/issues/1) to mainstream this into the SwiftWasm
54+
toolchain. But if you can't wait, here are some tips.
55+
56+
## 1. Only use this for WASI platforms, and only if Dispatch cannot be imported.
57+
58+
Use `#if os(WASI) && !canImport(Dispatch)` to elide usages outside of WASI platforms:
59+
60+
```swift
61+
#if os(WASI) && !canImport(Dispatch)
62+
import DispatchAsync
63+
#else
64+
import Dispatch
65+
#endif
66+
67+
// Use Dispatch API's the same way you normal would.
68+
```
69+
70+
## 2. If you really want to use DispatchAsync as a pure Swift Dispatch alternative for non-wasm targets
71+
72+
Stop. Are you sure? If you do this, you'll need to be careful with all `import Dispatch`, `import Foundation`, and many other issues.
73+
74+
1. Add the dependency to your package:
75+
76+
```swift
77+
let package = Package(
78+
name: "MyPackage",
79+
products: [
80+
.library(
81+
name: "MyPackage",
82+
targets: [
83+
"MyPackage"
84+
]
85+
),
86+
],
87+
dependencies: [
88+
.package(
89+
url: "https://github.com/PassiveLogic/swift-dispatch-async.git",
90+
from: "0.0.1"
91+
),
92+
],
93+
targets: [
94+
.target(
95+
name: "MyPackage"
96+
dependencies: [
97+
.product(name: "DispatchAsync", package: "swift-dispatch-async", condition: .when(platforms: [.wasi])),
98+
]
99+
),
100+
]
101+
)
102+
```
103+
104+
2. Import and use DispatchAsync in place of Dispatch like this:
105+
106+
```swift
107+
#if os(WASI) && !canImport(Dispatch)
108+
import DispatchAsync
109+
#else
110+
// Non-WASI platforms have to explicitly bring in DispatchAsync
111+
// by using `@_spi`.
112+
@_spi(DispatchAsync) import DispatchAsync
113+
#endif
114+
115+
// Not allowed, brings in Dispatch, aka "the real GCD":
116+
// import Dispatch
117+
118+
// Also not allowed, brings in Dispatch
119+
// import Foundation
120+
121+
// You'll need to use scoped Foundation imports:
122+
import struct Foundation.URL // Ok. Doesn't bring in Dispatch
123+
124+
// If you ignore the above notes, but do the following, be prepared for namespace
125+
// collisions between the toolchain's Dispatch and DispatchAsync:
126+
127+
private typealias DispatchQueue = DispatchAsync.DispatchQueue // Ok as long as Dispatch isn't imported
128+
129+
// Ok. If you followed everything above, you can now do the following, using pure Swift
130+
// under the hood! 🎉
131+
DispatchQueue.main.async {
132+
// Run your code here…
133+
}
134+
```
135+
136+
# LICENSE
137+
138+
This project is distributed by PassiveLogic under the Apache-2.0 license. See
139+
[LICENSE](https://github.com/PassiveLogic/swift-dispatch-async/blob/main/LICENSE) for full terms of use.
140+

0 commit comments

Comments
 (0)