Skip to content

Commit 652b448

Browse files
Use the new ExecutorFactory protocol to provide a default executor
1 parent 0896c7d commit 652b448

File tree

4 files changed

+114
-19
lines changed

4 files changed

+114
-19
lines changed
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)

Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift

+12-13
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import _CJavaScriptKit
33

44
@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
55
extension JavaScriptEventLoop {
6-
6+
77
static func installByLegacyHook() {
8-
#if compiler(>=5.9)
8+
#if compiler(>=5.9)
99
typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) (
1010
swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override
1111
) -> Void
@@ -16,10 +16,10 @@ extension JavaScriptEventLoop {
1616
swift_task_asyncMainDrainQueue_hook_impl,
1717
to: UnsafeMutableRawPointer?.self
1818
)
19-
#endif
19+
#endif
2020

2121
typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original)
22-
-> Void
22+
-> Void
2323
let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in
2424
JavaScriptEventLoop.shared.unsafeEnqueue(job)
2525
}
@@ -32,17 +32,18 @@ extension JavaScriptEventLoop {
3232
UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original
3333
) -> Void
3434
let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = {
35-
delay,
35+
nanoseconds,
3636
job,
3737
original in
38-
JavaScriptEventLoop.shared.enqueue(job, withDelay: delay)
38+
let milliseconds = Double(nanoseconds / 1_000_000)
39+
JavaScriptEventLoop.shared.enqueue(job, withDelay: milliseconds)
3940
}
4041
swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast(
4142
swift_task_enqueueGlobalWithDelay_hook_impl,
4243
to: UnsafeMutableRawPointer?.self
4344
)
44-
45-
#if compiler(>=5.7)
45+
46+
#if compiler(>=5.7)
4647
typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) (
4748
Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original
4849
) -> Void
@@ -60,8 +61,8 @@ extension JavaScriptEventLoop {
6061
swift_task_enqueueGlobalWithDeadline_hook_impl,
6162
to: UnsafeMutableRawPointer?.self
6263
)
63-
#endif
64-
64+
#endif
65+
6566
typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) (
6667
UnownedJob, swift_task_enqueueMainExecutor_original
6768
) -> Void
@@ -76,7 +77,6 @@ extension JavaScriptEventLoop {
7677
}
7778
}
7879

79-
8080
#if compiler(>=5.7)
8181
/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88
8282
@_silgen_name("swift_get_time")
@@ -100,8 +100,7 @@ extension JavaScriptEventLoop {
100100
var nowNSec: Int64 = 0
101101
swift_get_time(&nowSec, &nowNSec, clock)
102102
let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec)
103-
enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec))
103+
enqueue(job, withDelay: delayNanosec <= 0 ? 0 : Double(delayNanosec))
104104
}
105105
}
106106
#endif
107-

Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift

+10-3
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,21 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable {
120120

121121
@MainActor private static func installGlobalExecutorIsolated() {
122122
guard !didInstallGlobalExecutor else { return }
123+
#if compiler(>=6.2)
124+
if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) {
125+
// For Swift 6.2 and above, we can use the new `ExecutorFactory` API
126+
_Concurrency._createExecutors(factory: JavaScriptEventLoop.self)
127+
}
128+
#else
129+
// For Swift 6.1 and below, we need to install the global executor by hook API
123130
installByLegacyHook()
131+
#endif
124132
didInstallGlobalExecutor = true
125133
}
126134

127-
internal func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) {
128-
let milliseconds = nanoseconds / 1_000_000
135+
internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) {
129136
setTimeout(
130-
Double(milliseconds),
137+
milliseconds,
131138
{
132139
#if compiler(>=5.9)
133140
job.runSynchronously(on: self.asUnownedSerialExecutor())

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)