diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd9c68493..cf0224346 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,12 +21,12 @@ jobs: target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - 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 + 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 wasi-backend: Node target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - 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 + 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 wasi-backend: Node target: "wasm32-unknown-wasip1-threads" diff --git a/Examples/ActorOnWebWorker/Sources/MyApp.swift b/Examples/ActorOnWebWorker/Sources/MyApp.swift index 357956a7e..9b38fa30c 100644 --- a/Examples/ActorOnWebWorker/Sources/MyApp.swift +++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift @@ -255,7 +255,6 @@ enum OwnedExecutor { static func main() { JavaScriptEventLoop.installGlobalExecutor() - WebWorkerTaskExecutor.installGlobalExecutor() let useDedicatedWorker = !(JSObject.global.disableDedicatedWorker.boolean ?? false) Task { diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift index 9a1e09bb4..f9839ffde 100644 --- a/Examples/Multithreading/Sources/MyApp/main.swift +++ b/Examples/Multithreading/Sources/MyApp/main.swift @@ -3,7 +3,6 @@ import JavaScriptEventLoop import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -WebWorkerTaskExecutor.installGlobalExecutor() func renderInCanvas(ctx: JSObject, image: ImageView) { let imageData = ctx.createImageData!(image.width, image.height).object! diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index a2a6e2aac..5709c664c 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -2,7 +2,6 @@ import JavaScriptEventLoop import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -WebWorkerTaskExecutor.installGlobalExecutor() protocol CanvasRenderer { func render(canvas: JSObject, size: Int) async throws diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index f888b9d1c..9f6cf13a3 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -42,7 +42,12 @@ const harnesses = { let options = await nodePlatform.defaultNodeSetup({ args: testFrameworkArgs, onExit: (code) => { - if (code !== 0) { return } + if (code !== 0) { + const stack = new Error().stack + console.error(`Test failed with exit code ${code}`) + console.error(stack) + return + } // Extract the coverage file from the wasm module const filePath = "default.profraw" const destinationPath = args.values["coverage-file"] ?? filePath @@ -52,9 +57,9 @@ const harnesses = { writeFileSync(destinationPath, profraw); } }, - /* #if USE_SHARED_MEMORY */ +/* #if USE_SHARED_MEMORY */ spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript) - /* #endif */ +/* #endif */ }) if (preludeScript) { const prelude = await import(preludeScript) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift new file mode 100644 index 000000000..d008ea67a --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -0,0 +1,91 @@ +// Implementation of custom executors for JavaScript event loop +// This file implements the ExecutorFactory protocol to provide custom main and global executors +// for Swift concurrency in JavaScript environment. +// See: https://github.com/swiftlang/swift/pull/80266 +// See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437 + +import _CJavaScriptKit + +#if compiler(>=6.2) + +// MARK: - MainExecutor Implementation +// MainExecutor is used by the main actor to execute tasks on the main thread +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: MainExecutor { + public func run() throws { + // This method is called from `swift_task_asyncMainDrainQueueImpl`. + // https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/stdlib/public/Concurrency/ExecutorImpl.swift#L28 + // Yield control to the JavaScript event loop to skip the `exit(0)` + // call by `swift_task_asyncMainDrainQueueImpl`. + swjs_unsafe_event_loop_yield() + } + public func stop() {} +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension JavaScriptEventLoop: TaskExecutor {} + +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: SchedulableExecutor { + public func enqueue( + _ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C + ) { + let milliseconds = Self.delayInMilliseconds(from: delay, clock: clock) + self.enqueue( + UnownedJob(job), + withDelay: milliseconds + ) + } + + private static func delayInMilliseconds(from duration: C.Duration, clock: C) -> Double { + let swiftDuration = clock.convert(from: duration)! + let (seconds, attoseconds) = swiftDuration.components + return Double(seconds) * 1_000 + (Double(attoseconds) / 1_000_000_000_000_000) + } +} + +// MARK: - ExecutorFactory Implementation +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: ExecutorFactory { + // Forward all operations to the current thread's JavaScriptEventLoop instance + final class CurrentThread: TaskExecutor, SchedulableExecutor, MainExecutor, SerialExecutor { + func checkIsolated() {} + + func enqueue(_ job: consuming ExecutorJob) { + JavaScriptEventLoop.shared.enqueue(job) + } + + func enqueue( + _ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C + ) { + JavaScriptEventLoop.shared.enqueue( + job, + after: delay, + tolerance: tolerance, + clock: clock + ) + } + func run() throws { + try JavaScriptEventLoop.shared.run() + } + func stop() { + JavaScriptEventLoop.shared.stop() + } + } + + public static var mainExecutor: any MainExecutor { + CurrentThread() + } + + public static var defaultExecutor: any TaskExecutor { + CurrentThread() + } +} + +#endif // compiler(>=6.2) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift new file mode 100644 index 000000000..54d1c5dd1 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift @@ -0,0 +1,106 @@ +import _CJavaScriptEventLoop +import _CJavaScriptKit + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + + static func installByLegacyHook() { + #if compiler(>=5.9) + typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( + swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override + ) -> Void + let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in + swjs_unsafe_event_loop_yield() + } + swift_task_asyncMainDrainQueue_hook = unsafeBitCast( + swift_task_asyncMainDrainQueue_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + #endif + + typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) + -> Void + let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in + JavaScriptEventLoop.shared.unsafeEnqueue(job) + } + swift_task_enqueueGlobal_hook = unsafeBitCast( + swift_task_enqueueGlobal_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) ( + UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original + ) -> Void + let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { + nanoseconds, + job, + original in + let milliseconds = Double(nanoseconds / 1_000_000) + JavaScriptEventLoop.shared.enqueue(job, withDelay: milliseconds) + } + swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( + swift_task_enqueueGlobalWithDelay_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + #if compiler(>=5.7) + typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( + Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original + ) -> Void + let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { + sec, + nsec, + tsec, + tnsec, + clock, + job, + original in + JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) + } + swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast( + swift_task_enqueueGlobalWithDeadline_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + #endif + + typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( + UnownedJob, swift_task_enqueueMainExecutor_original + ) -> Void + let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in + JavaScriptEventLoop.shared.unsafeEnqueue(job) + } + swift_task_enqueueMainExecutor_hook = unsafeBitCast( + swift_task_enqueueMainExecutor_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + } +} + +#if compiler(>=5.7) +/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 +@_silgen_name("swift_get_time") +internal func swift_get_time( + _ seconds: UnsafeMutablePointer, + _ nanoseconds: UnsafeMutablePointer, + _ clock: CInt +) + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + fileprivate func enqueue( + _ job: UnownedJob, + withDelay seconds: Int64, + _ nanoseconds: Int64, + _ toleranceSec: Int64, + _ toleranceNSec: Int64, + _ clock: Int32 + ) { + var nowSec: Int64 = 0 + var nowNSec: Int64 = 0 + swift_get_time(&nowSec, &nowNSec, clock) + let delayMilliseconds = (seconds - nowSec) * 1_000 + (nanoseconds - nowNSec) / 1_000_000 + enqueue(job, withDelay: delayMilliseconds <= 0 ? 0 : Double(delayMilliseconds)) + } +} +#endif diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8948723d4..1cb90f8d8 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -105,7 +105,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return eventLoop } - @MainActor private static var didInstallGlobalExecutor = false + private nonisolated(unsafe) static var didInstallGlobalExecutor = false /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -113,89 +113,26 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// introduced officially. See also [a draft proposal for custom /// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor) public static func installGlobalExecutor() { - MainActor.assumeIsolated { - Self.installGlobalExecutorIsolated() - } + Self.installGlobalExecutorIsolated() } - @MainActor private static func installGlobalExecutorIsolated() { + private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } - - #if compiler(>=5.9) - typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( - swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override - ) -> Void - let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in - swjs_unsafe_event_loop_yield() - } - swift_task_asyncMainDrainQueue_hook = unsafeBitCast( - swift_task_asyncMainDrainQueue_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #endif - - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void - let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) - } - swift_task_enqueueGlobal_hook = unsafeBitCast( - swift_task_enqueueGlobal_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) ( - UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original - ) -> Void - let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { - delay, - job, - original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: delay) - } - swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( - swift_task_enqueueGlobalWithDelay_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - #if compiler(>=5.7) - typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( - Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original - ) -> Void - let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { - sec, - nsec, - tsec, - tnsec, - clock, - job, - original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) + didInstallGlobalExecutor = true + #if compiler(>=6.2) + if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { + // For Swift 6.2 and above, we can use the new `ExecutorFactory` API + _Concurrency._createExecutors(factory: JavaScriptEventLoop.self) } - swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast( - swift_task_enqueueGlobalWithDeadline_hook_impl, - to: UnsafeMutableRawPointer?.self - ) + #else + // For Swift 6.1 and below, we need to install the global executor by hook API + installByLegacyHook() #endif - - typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( - UnownedJob, swift_task_enqueueMainExecutor_original - ) -> Void - let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) - } - swift_task_enqueueMainExecutor_hook = unsafeBitCast( - swift_task_enqueueMainExecutor_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - didInstallGlobalExecutor = true } - private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { - let milliseconds = nanoseconds / 1_000_000 + internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) { setTimeout( - Double(milliseconds), + milliseconds, { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) @@ -206,7 +143,19 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { ) } - private func unsafeEnqueue(_ job: UnownedJob) { + internal func unsafeEnqueue(_ job: UnownedJob) { + #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) + guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else { + // Notify the main thread to execute the job when a job is + // enqueued from a Web Worker thread but without an executor preference. + // This is usually the case when hopping back to the main thread + // at the end of a task. + let jobBitPattern = unsafeBitCast(job, to: UInt.self) + swjs_send_job_to_main_thread(jobBitPattern) + return + } + // If the current thread is the main thread, do nothing special. + #endif insertJobQueue(job: job) } @@ -228,34 +177,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } } -#if compiler(>=5.7) -/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 -@_silgen_name("swift_get_time") -internal func swift_get_time( - _ seconds: UnsafeMutablePointer, - _ nanoseconds: UnsafeMutablePointer, - _ clock: CInt -) - -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension JavaScriptEventLoop { - fileprivate func enqueue( - _ job: UnownedJob, - withDelay seconds: Int64, - _ nanoseconds: Int64, - _ toleranceSec: Int64, - _ toleranceNSec: Int64, - _ clock: Int32 - ) { - var nowSec: Int64 = 0 - var nowNSec: Int64 = 0 - swift_get_time(&nowSec, &nowNSec, clock) - let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec) - enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec)) - } -} -#endif - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index d42c5adda..82cc593bd 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -34,7 +34,7 @@ import WASILibc /// /// - SeeAlso: ``WebWorkerTaskExecutor`` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public final class WebWorkerDedicatedExecutor: SerialExecutor { +public final class WebWorkerDedicatedExecutor: SerialExecutor, TaskExecutor { private let underlying: WebWorkerTaskExecutor diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index b51445cbd..1078244f9 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -412,8 +412,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { let unmanagedContext = Unmanaged.passRetained(context) contexts.append(unmanagedContext) let ptr = unmanagedContext.toOpaque() + var thread = pthread_t(bitPattern: 0) let ret = pthread_create( - nil, + &thread, nil, { ptr in // Cast to a optional pointer to absorb nullability variations between platforms. @@ -602,78 +603,8 @@ public final class WebWorkerTaskExecutor: TaskExecutor { internal func dumpStats() {} #endif - // MARK: Global Executor hack - - @MainActor private static var _mainThread: pthread_t? - @MainActor private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer? - @MainActor private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer? - @MainActor private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer? - - /// Installs a global executor that forwards jobs from Web Worker threads to the main thread. - /// - /// This method sets up the necessary hooks to ensure proper task scheduling between - /// the main thread and worker threads. It must be called once (typically at application - /// startup) before using any `WebWorkerTaskExecutor` instances. - /// - /// ## Example - /// - /// ```swift - /// // At application startup - /// WebWorkerTaskExecutor.installGlobalExecutor() - /// - /// // Later, create and use executor instances - /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) - /// ``` - /// - /// - Important: This method must be called from the main thread. - public static func installGlobalExecutor() { - MainActor.assumeIsolated { - installGlobalExecutorIsolated() - } - } - - @MainActor - static func installGlobalExecutorIsolated() { - #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) - // Ensure this function is called only once. - guard _mainThread == nil else { return } - - _mainThread = pthread_self() - assert(swjs_get_worker_thread_id() == -1, "\(#function) must be called on the main thread") - - _swift_task_enqueueGlobal_hook_original = swift_task_enqueueGlobal_hook - - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void - let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, base in - WebWorkerTaskExecutor.traceStatsIncrement(\.enqueueGlobal) - // Enter this block only if the current Task has no executor preference. - if pthread_equal(pthread_self(), WebWorkerTaskExecutor._mainThread) != 0 { - // If the current thread is the main thread, delegate the job - // execution to the original hook of JavaScriptEventLoop. - let original = unsafeBitCast( - WebWorkerTaskExecutor._swift_task_enqueueGlobal_hook_original, - to: swift_task_enqueueGlobal_hook_Fn.self - ) - original(job, base) - } else { - // Notify the main thread to execute the job when a job is - // enqueued from a Web Worker thread but without an executor preference. - // This is usually the case when hopping back to the main thread - // at the end of a task. - WebWorkerTaskExecutor.traceStatsIncrement(\.sendJobToMainThread) - let jobBitPattern = unsafeBitCast(job, to: UInt.self) - swjs_send_job_to_main_thread(jobBitPattern) - } - } - swift_task_enqueueGlobal_hook = unsafeBitCast( - swift_task_enqueueGlobal_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #else - fatalError("Unsupported platform") - #endif - } + @available(*, deprecated, message: "Not needed anymore, just use `JavaScriptEventLoop.installGlobalExecutor()`.") + public static func installGlobalExecutor() {} } /// Enqueue a job scheduled from a Web Worker thread to the main thread. diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 0582fe8c4..4c441f3c4 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -27,11 +27,6 @@ import JavaScriptEventLoop func swift_javascriptkit_activate_js_executor_impl() { MainActor.assumeIsolated { JavaScriptEventLoop.installGlobalExecutor() - #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) - if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { - WebWorkerTaskExecutor.installGlobalExecutor() - } - #endif } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 7502bb5f1..f0ef6da9a 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -84,21 +84,22 @@ public final class JSPromise: JSBridgedClass { } #endif - #if !hasFeature(Embedded) /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult - public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { let closure = JSOneshotClosure { success($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// Schedules the `success` closure to be invoked on successful completion of `self`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult - public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { + public func then( + success: sending @escaping (sending JSValue) async throws -> JSValue + ) -> JSPromise { let closure = JSOneshotClosure.async { try await success($0[0]).jsValue } @@ -109,8 +110,8 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then( - success: @escaping (sending JSValue) -> ConvertibleToJSValue, - failure: @escaping (sending JSValue) -> ConvertibleToJSValue + success: @escaping (sending JSValue) -> JSValue, + failure: @escaping (sending JSValue) -> JSValue ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue @@ -121,13 +122,13 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// Schedules the `success` closure to be invoked on successful completion of `self`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func then( - success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue, - failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue + success: sending @escaping (sending JSValue) async throws -> JSValue, + failure: sending @escaping (sending JSValue) async throws -> JSValue ) -> JSPromise { let successClosure = JSOneshotClosure.async { try await success($0[0]).jsValue @@ -141,19 +142,24 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult - public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func `catch`( + failure: @escaping (sending JSValue) -> JSValue + ) + -> JSPromise + { let closure = JSOneshotClosure { failure($0[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult - public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise - { + public func `catch`( + failure: sending @escaping (sending JSValue) async throws -> JSValue + ) -> JSPromise { let closure = JSOneshotClosure.async { try await failure($0[0]).jsValue } @@ -171,5 +177,4 @@ public final class JSPromise: JSBridgedClass { } return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } - #endif } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index fa713c3b9..7aaba9ed6 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -1,4 +1,7 @@ import _CJavaScriptKit +#if hasFeature(Embedded) && os(WASI) +import _Concurrency +#endif /// `JSClosureProtocol` wraps Swift closure objects for use in JavaScript. Conforming types /// are responsible for managing the lifetime of the closure they wrap, but can delegate that @@ -40,7 +43,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { fatalError("JSOneshotClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) && !hasFeature(Embedded) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure { @@ -132,7 +135,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { fatalError("JSClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) && !hasFeature(Embedded) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) @@ -148,7 +151,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #endif } -#if compiler(>=5.5) && !hasFeature(Embedded) +#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private func makeAsyncClosure( _ body: sending @escaping (sending [JSValue]) async throws -> JSValue diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 931b48f7a..d587478a5 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -326,6 +326,8 @@ IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) IMPORT_JS_FUNCTION(swjs_create_object, JavaScriptObjectRef, (void)) +#define SWJS_MAIN_THREAD_ID -1 + int swjs_get_worker_thread_id_cached(void); /// Requests sending a JavaScript object to another worker thread. diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift index 962b04421..c3429e8c9 100644 --- a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -9,14 +9,14 @@ final class JSPromiseTests: XCTestCase { p1 = p1.then { value in XCTAssertEqual(value, .null) continuation.resume() - return JSValue.number(1.0) + return JSValue.number(1.0).jsValue } } await withCheckedContinuation { continuation in p1 = p1.then { value in XCTAssertEqual(value, .number(1.0)) continuation.resume() - return JSPromise.resolve(JSValue.boolean(true)) + return JSPromise.resolve(JSValue.boolean(true)).jsValue } } await withCheckedContinuation { continuation in @@ -48,7 +48,7 @@ final class JSPromiseTests: XCTestCase { p2 = p2.then { value in XCTAssertEqual(value, .boolean(true)) continuation.resume() - return JSPromise.reject(JSValue.number(2.0)) + return JSPromise.reject(JSValue.number(2.0)).jsValue } } await withCheckedContinuation { continuation in diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 1da56e680..4224e2a65 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -151,13 +151,13 @@ final class JavaScriptEventLoopTests: XCTestCase { } let promise2 = promise.then { result in try await Task.sleep(nanoseconds: 100_000_000) - return String(result.number!) + return .string(String(result.number!)) } let thenDiff = try await measureTime { let result = try await promise2.value XCTAssertEqual(result, .string("3.0")) } - XCTAssertGreaterThanOrEqual(thenDiff, 200) + XCTAssertGreaterThanOrEqual(thenDiff, 150) } func testPromiseThenWithFailure() async throws { @@ -171,7 +171,7 @@ final class JavaScriptEventLoopTests: XCTestCase { 100 ) } - let failingPromise2 = failingPromise.then { _ in + let failingPromise2 = failingPromise.then { _ -> JSValue in throw MessageError("Should not be called", file: #file, line: #line, column: #column) } failure: { err in return err diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index acc6fccf9..f743d8ef0 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -90,9 +90,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } } let taskRunOnMainThread = await task.value - // FIXME: The block passed to `MainActor.run` should run on the main thread - // XCTAssertTrue(taskRunOnMainThread) - XCTAssertFalse(taskRunOnMainThread) + XCTAssertTrue(taskRunOnMainThread) // After the task is done, back to the main thread XCTAssertTrue(isMainThread())