Skip to content

Commit 10d4813

Browse files
Merge pull request #353 from swiftwasm/yt/new-global-task-executor
Use the new `ExecutorFactory` protocol to provide a default executor
2 parents 8c8e0eb + 3e1107f commit 10d4813

File tree

5 files changed

+210
-103
lines changed

5 files changed

+210
-103
lines changed

.github/workflows/test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ jobs:
2121
target: "wasm32-unknown-wasi"
2222
- os: ubuntu-22.04
2323
toolchain:
24-
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
24+
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz
2525
wasi-backend: Node
2626
target: "wasm32-unknown-wasi"
2727
- os: ubuntu-22.04
2828
toolchain:
29-
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz
29+
download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a-ubuntu22.04.tar.gz
3030
wasi-backend: Node
3131
target: "wasm32-unknown-wasip1-threads"
3232

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Implementation of custom executors for JavaScript event loop
2+
// This file implements the ExecutorFactory protocol to provide custom main and global executors
3+
// for Swift concurrency in JavaScript environment.
4+
// See: https://github.com/swiftlang/swift/pull/80266
5+
// See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437
6+
7+
import _CJavaScriptKit
8+
9+
#if compiler(>=6.2)
10+
11+
// MARK: - MainExecutor Implementation
12+
// MainExecutor is used by the main actor to execute tasks on the main thread
13+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *)
14+
extension JavaScriptEventLoop: MainExecutor {
15+
public func run() throws {
16+
// This method is called from `swift_task_asyncMainDrainQueueImpl`.
17+
// https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/stdlib/public/Concurrency/ExecutorImpl.swift#L28
18+
// Yield control to the JavaScript event loop to skip the `exit(0)`
19+
// call by `swift_task_asyncMainDrainQueueImpl`.
20+
swjs_unsafe_event_loop_yield()
21+
}
22+
public func stop() {}
23+
}
24+
25+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
26+
extension JavaScriptEventLoop: TaskExecutor {}
27+
28+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *)
29+
extension JavaScriptEventLoop: SchedulableExecutor {
30+
public func enqueue<C: Clock>(
31+
_ job: consuming ExecutorJob,
32+
after delay: C.Duration,
33+
tolerance: C.Duration?,
34+
clock: C
35+
) {
36+
let milliseconds = Self.delayInMilliseconds(from: delay, clock: clock)
37+
self.enqueue(
38+
UnownedJob(job),
39+
withDelay: milliseconds
40+
)
41+
}
42+
43+
private static func delayInMilliseconds<C: Clock>(from duration: C.Duration, clock: C) -> Double {
44+
let swiftDuration = clock.convert(from: duration)!
45+
let (seconds, attoseconds) = swiftDuration.components
46+
return Double(seconds) * 1_000 + (Double(attoseconds) / 1_000_000_000_000_000)
47+
}
48+
}
49+
50+
// MARK: - ExecutorFactory Implementation
51+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *)
52+
extension JavaScriptEventLoop: ExecutorFactory {
53+
// Forward all operations to the current thread's JavaScriptEventLoop instance
54+
final class CurrentThread: TaskExecutor, SchedulableExecutor, MainExecutor, SerialExecutor {
55+
func checkIsolated() {}
56+
57+
func enqueue(_ job: consuming ExecutorJob) {
58+
JavaScriptEventLoop.shared.enqueue(job)
59+
}
60+
61+
func enqueue<C: Clock>(
62+
_ job: consuming ExecutorJob,
63+
after delay: C.Duration,
64+
tolerance: C.Duration?,
65+
clock: C
66+
) {
67+
JavaScriptEventLoop.shared.enqueue(
68+
job,
69+
after: delay,
70+
tolerance: tolerance,
71+
clock: clock
72+
)
73+
}
74+
func run() throws {
75+
try JavaScriptEventLoop.shared.run()
76+
}
77+
func stop() {
78+
JavaScriptEventLoop.shared.stop()
79+
}
80+
}
81+
82+
public static var mainExecutor: any MainExecutor {
83+
CurrentThread()
84+
}
85+
86+
public static var defaultExecutor: any TaskExecutor {
87+
CurrentThread()
88+
}
89+
}
90+
91+
#endif // compiler(>=6.2)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import _CJavaScriptEventLoop
2+
import _CJavaScriptKit
3+
4+
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
5+
extension JavaScriptEventLoop {
6+
7+
static func installByLegacyHook() {
8+
#if compiler(>=5.9)
9+
typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (
10+
swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override
11+
) -> Void
12+
let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in
13+
swjs_unsafe_event_loop_yield()
14+
}
15+
swift_task_asyncMainDrainQueue_hook = unsafeBitCast(
16+
swift_task_asyncMainDrainQueue_hook_impl,
17+
to: UnsafeMutableRawPointer?.self
18+
)
19+
#endif
20+
21+
typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original)
22+
-> Void
23+
let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
24+
JavaScriptEventLoop.shared.unsafeEnqueue(job)
25+
}
26+
swift_task_enqueueGlobal_hook = unsafeBitCast(
27+
swift_task_enqueueGlobal_hook_impl,
28+
to: UnsafeMutableRawPointer?.self
29+
)
30+
31+
typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) (
32+
UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original
33+
) -> Void
34+
let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = {
35+
nanoseconds,
36+
job,
37+
original in
38+
let milliseconds = Double(nanoseconds / 1_000_000)
39+
JavaScriptEventLoop.shared.enqueue(job, withDelay: milliseconds)
40+
}
41+
swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(
42+
swift_task_enqueueGlobalWithDelay_hook_impl,
43+
to: UnsafeMutableRawPointer?.self
44+
)
45+
46+
#if compiler(>=5.7)
47+
typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (
48+
Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original
49+
) -> Void
50+
let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = {
51+
sec,
52+
nsec,
53+
tsec,
54+
tnsec,
55+
clock,
56+
job,
57+
original in
58+
JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock)
59+
}
60+
swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(
61+
swift_task_enqueueGlobalWithDeadline_hook_impl,
62+
to: UnsafeMutableRawPointer?.self
63+
)
64+
#endif
65+
66+
typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (
67+
UnownedJob, swift_task_enqueueMainExecutor_original
68+
) -> Void
69+
let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
70+
JavaScriptEventLoop.shared.unsafeEnqueue(job)
71+
}
72+
swift_task_enqueueMainExecutor_hook = unsafeBitCast(
73+
swift_task_enqueueMainExecutor_hook_impl,
74+
to: UnsafeMutableRawPointer?.self
75+
)
76+
77+
}
78+
}
79+
80+
#if compiler(>=5.7)
81+
/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
82+
@_silgen_name("swift_get_time")
83+
internal func swift_get_time(
84+
_ seconds: UnsafeMutablePointer<Int64>,
85+
_ nanoseconds: UnsafeMutablePointer<Int64>,
86+
_ clock: CInt
87+
)
88+
89+
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
90+
extension JavaScriptEventLoop {
91+
fileprivate func enqueue(
92+
_ job: UnownedJob,
93+
withDelay seconds: Int64,
94+
_ nanoseconds: Int64,
95+
_ toleranceSec: Int64,
96+
_ toleranceNSec: Int64,
97+
_ clock: Int32
98+
) {
99+
var nowSec: Int64 = 0
100+
var nowNSec: Int64 = 0
101+
swift_get_time(&nowSec, &nowNSec, clock)
102+
let delayMilliseconds = (seconds - nowSec) * 1_000 + (nanoseconds - nowNSec) / 1_000_000
103+
enqueue(job, withDelay: delayMilliseconds <= 0 ? 0 : Double(delayMilliseconds))
104+
}
105+
}
106+
#endif

Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift

+10-98
Original file line numberDiff line numberDiff line change
@@ -119,80 +119,20 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
119119
private static func installGlobalExecutorIsolated() {
120120
guard !didInstallGlobalExecutor else { return }
121121
didInstallGlobalExecutor = true
122-
123-
#if compiler(>=5.9)
124-
typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (
125-
swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override
126-
) -> Void
127-
let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in
128-
swjs_unsafe_event_loop_yield()
129-
}
130-
swift_task_asyncMainDrainQueue_hook = unsafeBitCast(
131-
swift_task_asyncMainDrainQueue_hook_impl,
132-
to: UnsafeMutableRawPointer?.self
133-
)
134-
#endif
135-
136-
typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original)
137-
-> Void
138-
let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
139-
JavaScriptEventLoop.shared.unsafeEnqueue(job)
140-
}
141-
swift_task_enqueueGlobal_hook = unsafeBitCast(
142-
swift_task_enqueueGlobal_hook_impl,
143-
to: UnsafeMutableRawPointer?.self
144-
)
145-
146-
typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) (
147-
UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original
148-
) -> Void
149-
let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = {
150-
delay,
151-
job,
152-
original in
153-
JavaScriptEventLoop.shared.enqueue(job, withDelay: delay)
154-
}
155-
swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(
156-
swift_task_enqueueGlobalWithDelay_hook_impl,
157-
to: UnsafeMutableRawPointer?.self
158-
)
159-
160-
#if compiler(>=5.7)
161-
typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (
162-
Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original
163-
) -> Void
164-
let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = {
165-
sec,
166-
nsec,
167-
tsec,
168-
tnsec,
169-
clock,
170-
job,
171-
original in
172-
JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock)
122+
#if compiler(>=6.2)
123+
if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) {
124+
// For Swift 6.2 and above, we can use the new `ExecutorFactory` API
125+
_Concurrency._createExecutors(factory: JavaScriptEventLoop.self)
173126
}
174-
swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast(
175-
swift_task_enqueueGlobalWithDeadline_hook_impl,
176-
to: UnsafeMutableRawPointer?.self
177-
)
127+
#else
128+
// For Swift 6.1 and below, we need to install the global executor by hook API
129+
installByLegacyHook()
178130
#endif
179-
180-
typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (
181-
UnownedJob, swift_task_enqueueMainExecutor_original
182-
) -> Void
183-
let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in
184-
JavaScriptEventLoop.shared.unsafeEnqueue(job)
185-
}
186-
swift_task_enqueueMainExecutor_hook = unsafeBitCast(
187-
swift_task_enqueueMainExecutor_hook_impl,
188-
to: UnsafeMutableRawPointer?.self
189-
)
190131
}
191132

192-
private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) {
193-
let milliseconds = nanoseconds / 1_000_000
133+
internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) {
194134
setTimeout(
195-
Double(milliseconds),
135+
milliseconds,
196136
{
197137
#if compiler(>=5.9)
198138
job.runSynchronously(on: self.asUnownedSerialExecutor())
@@ -203,7 +143,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
203143
)
204144
}
205145

206-
private func unsafeEnqueue(_ job: UnownedJob) {
146+
internal func unsafeEnqueue(_ job: UnownedJob) {
207147
#if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
208148
guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else {
209149
// Notify the main thread to execute the job when a job is
@@ -237,34 +177,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
237177
}
238178
}
239179

240-
#if compiler(>=5.7)
241-
/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
242-
@_silgen_name("swift_get_time")
243-
internal func swift_get_time(
244-
_ seconds: UnsafeMutablePointer<Int64>,
245-
_ nanoseconds: UnsafeMutablePointer<Int64>,
246-
_ clock: CInt
247-
)
248-
249-
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
250-
extension JavaScriptEventLoop {
251-
fileprivate func enqueue(
252-
_ job: UnownedJob,
253-
withDelay seconds: Int64,
254-
_ nanoseconds: Int64,
255-
_ toleranceSec: Int64,
256-
_ toleranceNSec: Int64,
257-
_ clock: Int32
258-
) {
259-
var nowSec: Int64 = 0
260-
var nowNSec: Int64 = 0
261-
swift_get_time(&nowSec, &nowNSec, clock)
262-
let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec)
263-
enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec))
264-
}
265-
}
266-
#endif
267-
268180
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
269181
extension JSPromise {
270182
/// Wait for the promise to complete, returning (or throwing) its result.

Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift

+1-3
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase {
9090
}
9191
}
9292
let taskRunOnMainThread = await task.value
93-
// FIXME: The block passed to `MainActor.run` should run on the main thread
94-
// XCTAssertTrue(taskRunOnMainThread)
95-
XCTAssertFalse(taskRunOnMainThread)
93+
XCTAssertTrue(taskRunOnMainThread)
9694
// After the task is done, back to the main thread
9795
XCTAssertTrue(isMainThread())
9896

0 commit comments

Comments
 (0)