From 28f34719df62d30655a9f81f6081aa8db9ce3d38 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Tue, 4 Mar 2025 01:39:40 +0000 Subject: [PATCH 01/19] Concurrency: Use `LazyThreadLocal` without @PW syntax Unfortunately, `@LazyThreadLocal static var` is considered as concurrency-unsafe in Swift 6 mode even though the underlying PW storage is read-only and concurrency-safe. Also Swift bans `static let` with `@propertyWrapper` syntax, so we need to use `LazyThreadLocal` directly. See the discussion in the Swift forum: https://forums.swift.org/t/static-property-wrappers-and-strict-concurrency-in-5-10/70116/27 --- Sources/JavaScriptKit/BasicObjects/JSArray.swift | 5 ++--- Sources/JavaScriptKit/BasicObjects/JSDate.swift | 5 ++--- Sources/JavaScriptKit/BasicObjects/JSError.swift | 5 ++--- .../JavaScriptKit/BasicObjects/JSTypedArray.swift | 10 ++++------ .../JavaScriptKit/FundamentalObjects/JSObject.swift | 13 +++++-------- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 95d14c637..56345d085 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -2,9 +2,8 @@ /// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) /// that exposes its properties in a type-safe and Swifty way. public class JSArray: JSBridgedClass { - public static var constructor: JSFunction? { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Array.function }) - private static var _constructor: JSFunction? + public static var constructor: JSFunction? { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Array.function }) static func isArray(_ object: JSObject) -> Bool { constructor!.isArray!(object).boolean! diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index da31aca06..c8a6623a1 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -8,9 +8,8 @@ */ public final class JSDate: JSBridgedClass { /// The constructor function used to create new `Date` objects. - public static var constructor: JSFunction? { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Date.function }) - private static var _constructor: JSFunction? + public static var constructor: JSFunction? { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Date.function }) /// The underlying JavaScript `Date` object. public let jsObject: JSObject diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 559618e15..937581d4b 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -4,9 +4,8 @@ */ public final class JSError: Error, JSBridgedClass { /// The constructor function used to create new JavaScript `Error` objects. - public static var constructor: JSFunction? { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Error.function }) - private static var _constructor: JSFunction? + public static var constructor: JSFunction? { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function }) /// The underlying JavaScript `Error` object. public let jsObject: JSObject diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index bc80cd25c..dec834bbd 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -143,19 +143,17 @@ func valueForBitWidth<T>(typeName: String, bitWidth: Int, when32: T) -> T { } extension Int: TypedArrayElement { - public static var typedArrayClass: JSFunction { _typedArrayClass } - @LazyThreadLocal(initialize: { + public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue } + private static let _typedArrayClass = LazyThreadLocal(initialize: { valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function! }) - private static var _typedArrayClass: JSFunction } extension UInt: TypedArrayElement { - public static var typedArrayClass: JSFunction { _typedArrayClass } - @LazyThreadLocal(initialize: { + public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue } + private static let _typedArrayClass = LazyThreadLocal(initialize: { valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! }) - private static var _typedArrayClass: JSFunction } extension Int8: TypedArrayElement { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index eb8fb643a..f74b337d8 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -24,9 +24,8 @@ import _CJavaScriptKit /// reference counting system. @dynamicMemberLookup public class JSObject: Equatable { - internal static var constructor: JSFunction { _constructor } - @LazyThreadLocal(initialize: { JSObject.global.Object.function! }) - internal static var _constructor: JSFunction + internal static var constructor: JSFunction { _constructor.wrappedValue } + private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! }) @_spi(JSObject_id) public var id: JavaScriptObjectRef @@ -206,12 +205,10 @@ public class JSObject: Equatable { /// A `JSObject` of the global scope object. /// This allows access to the global properties and global names by accessing the `JSObject` returned. - public static var global: JSObject { return _global } - - @LazyThreadLocal(initialize: { - return JSObject(id: _JS_Predef_Value_Global) + public static var global: JSObject { return _global.wrappedValue } + private static let _global = LazyThreadLocal(initialize: { + JSObject(id: _JS_Predef_Value_Global) }) - private static var _global: JSObject deinit { assertOnOwnerThread(hint: "deinitializing") From 917ab578aa4479055e87bbc59f17eeb90a4b6d3d Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Tue, 4 Mar 2025 01:45:10 +0000 Subject: [PATCH 02/19] Concurrency: Annotate `jsObject` property of `JSError` as `nonisolated(unsafe)` Even though `JSObject` is not a `Sendable` type, `JSError` must be `Sendable` because of `Error` conformance. For this reason, we need to annotate the `jsObject` property as `nonisolated(unsafe)` to suppress the compiler error. Accessing this property from a different isolation domain scheduled on a different thread will result in a runtime assertion failure, but better than corrupting memory. --- Sources/JavaScriptKit/BasicObjects/JSError.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 937581d4b..290838626 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -8,7 +8,11 @@ public final class JSError: Error, JSBridgedClass { private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function }) /// The underlying JavaScript `Error` object. - public let jsObject: JSObject + /// + /// NOTE: This property must be accessed from the thread that + /// the thrown `Error` object was created on. Otherwise, + /// it will result in a runtime assertion failure. + public nonisolated(unsafe) let jsObject: JSObject /// Creates a new instance of the JavaScript `Error` class with a given message. public init(message: String) { From 30f78ff7ebba29a7baca06a213918f13bfd6ff2b Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Tue, 4 Mar 2025 01:49:27 +0000 Subject: [PATCH 03/19] Concurrency: Update Package.swift tools version to 6.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f21a95cb5..4d4634b88 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version:6.0 import PackageDescription From 2642df9275f0b87cd6838960f8cfee9f0e53c5fa Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Tue, 4 Mar 2025 01:56:16 +0000 Subject: [PATCH 04/19] Concurrency: Replace `swjs_thread_local_closures` with `LazyThreadLocal` --- .../FundamentalObjects/JSClosure.swift | 22 +++++++------------ Sources/_CJavaScriptKit/_CJavaScriptKit.c | 2 -- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ----- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 5d367ba38..dafd4ce38 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -26,7 +26,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { } // 3. Retain the given body in static storage by `funcRef`. - JSClosure.sharedClosures[hostFuncRef] = (self, { + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = (self, { defer { self.release() } return body($0) }) @@ -42,7 +42,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { /// Release this function resource. /// After calling `release`, calling this function from JavaScript will fail. public func release() { - JSClosure.sharedClosures[hostFuncRef] = nil + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = nil } } @@ -74,14 +74,8 @@ public class JSClosure: JSFunction, JSClosureProtocol { } // Note: Retain the closure object itself also to avoid funcRef conflicts - fileprivate static var sharedClosures: SharedJSClosure { - if let swjs_thread_local_closures { - return Unmanaged<SharedJSClosure>.fromOpaque(swjs_thread_local_closures).takeUnretainedValue() - } else { - let shared = SharedJSClosure() - swjs_thread_local_closures = Unmanaged.passRetained(shared).toOpaque() - return shared - } + fileprivate static let sharedClosures = LazyThreadLocal { + SharedJSClosure() } private var hostFuncRef: JavaScriptHostFuncRef = 0 @@ -110,7 +104,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { } // 3. Retain the given body in static storage by `funcRef`. - Self.sharedClosures[hostFuncRef] = (self, body) + Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body) } #if compiler(>=5.5) && !hasFeature(Embedded) @@ -192,7 +186,7 @@ func _call_host_function_impl( _ argv: UnsafePointer<RawJSValue>, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { - guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { + guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { return true } let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue} @@ -232,7 +226,7 @@ extension JSClosure { @_cdecl("_free_host_function_impl") func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { - JSClosure.sharedClosures[hostFuncRef] = nil + JSClosure.sharedClosures.wrappedValue[hostFuncRef] = nil } #endif @@ -251,4 +245,4 @@ public func _swjs_call_host_function( public func _swjs_free_host_function(_ hostFuncRef: JavaScriptHostFuncRef) { _free_host_function_impl(hostFuncRef) } -#endif \ No newline at end of file +#endif diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 424e9081b..ea8b5b43d 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -61,5 +61,3 @@ int swjs_library_features(void) { } #endif #endif - -_Thread_local void *swjs_thread_local_closures; diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index aa0b978a2..5cb6e6037 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -308,9 +308,4 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid)) IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) -/// MARK: - thread local storage - -// TODO: Rewrite closure system without global storage -extern _Thread_local void * _Nullable swjs_thread_local_closures; - #endif /* _CJavaScriptKit_h */ From daa820960939fceef1aa243af9a1ac84dc724712 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 06:29:04 +0000 Subject: [PATCH 05/19] Concurrency: Remove `Error` conformance from `JSError` `Error` protocol now requires `Sendable` conformance, which is not possible for `JSError` because `JSObject` is not `Sendable`. --- Sources/JavaScriptKit/BasicObjects/JSError.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 290838626..0f87d3c67 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -2,17 +2,13 @@ class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that exposes its properties in a type-safe way. */ -public final class JSError: Error, JSBridgedClass { +public final class JSError: JSBridgedClass { /// The constructor function used to create new JavaScript `Error` objects. public static var constructor: JSFunction? { _constructor.wrappedValue } private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Error.function }) /// The underlying JavaScript `Error` object. - /// - /// NOTE: This property must be accessed from the thread that - /// the thrown `Error` object was created on. Otherwise, - /// it will result in a runtime assertion failure. - public nonisolated(unsafe) let jsObject: JSObject + public let jsObject: JSObject /// Creates a new instance of the JavaScript `Error` class with a given message. public init(message: String) { From 9f0197dc8f5c65ebe180712bbd753002cbb1c135 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 06:31:22 +0000 Subject: [PATCH 06/19] Concurrency: Isolate global executor installation by MainActor --- .../JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++-- .../WebWorkerTaskExecutor.swift | 9 +++++---- .../JavaScriptEventLoopTestSupport.swift | 4 +++- .../include/_CJavaScriptEventLoop.h | 14 ++++++++------ 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 765746bb1..af8738ef8 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -102,14 +102,14 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return eventLoop } - private static var didInstallGlobalExecutor = false + @MainActor private 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. /// This installation step will be unnecessary after custom executor are /// 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 public static func installGlobalExecutor() { guard !didInstallGlobalExecutor else { return } #if compiler(>=5.9) diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index 5110f60db..ac4769a82 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -426,14 +426,15 @@ public final class WebWorkerTaskExecutor: TaskExecutor { // MARK: Global Executor hack - private static var _mainThread: pthread_t? - private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer? - private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer? - private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer? + @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? /// Install a global executor that forwards jobs from Web Worker threads to the main thread. /// /// This function must be called once before using the Web Worker task executor. + @MainActor public static func installGlobalExecutor() { #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) // Ensure this function is called only once. diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 64e6776d4..4c441f3c4 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -25,7 +25,9 @@ import JavaScriptEventLoop @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) @_cdecl("swift_javascriptkit_activate_js_executor_impl") func swift_javascriptkit_activate_js_executor_impl() { - JavaScriptEventLoop.installGlobalExecutor() + MainActor.assumeIsolated { + JavaScriptEventLoop.installGlobalExecutor() + } } #endif diff --git a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h index 4f1b9470c..08efcb948 100644 --- a/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h +++ b/Sources/_CJavaScriptEventLoop/include/_CJavaScriptEventLoop.h @@ -9,6 +9,8 @@ #define SWIFT_EXPORT_FROM(LIBRARY) __attribute__((__visibility__("default"))) +#define SWIFT_NONISOLATED_UNSAFE __attribute__((swift_attr("nonisolated(unsafe)"))) + /// A schedulable unit /// Note that this type layout is a part of public ABI, so we expect this field layout won't break in the future versions. /// Current implementation refers the `swift-5.5-RELEASE` implementation. @@ -27,13 +29,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobal_original)( Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -extern void *_Nullable swift_task_enqueueGlobal_hook; +extern void *_Nullable swift_task_enqueueGlobal_hook SWIFT_NONISOLATED_UNSAFE; /// A hook to take over global enqueuing with delay. typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDelay_original)( unsigned long long delay, Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook; +extern void *_Nullable swift_task_enqueueGlobalWithDelay_hook SWIFT_NONISOLATED_UNSAFE; typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( long long sec, @@ -42,13 +44,13 @@ typedef SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_original)( long long tnsec, int clock, Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook; +extern void *_Nullable swift_task_enqueueGlobalWithDeadline_hook SWIFT_NONISOLATED_UNSAFE; /// A hook to take over main executor enqueueing. typedef SWIFT_CC(swift) void (*swift_task_enqueueMainExecutor_original)( Job *_Nonnull job); SWIFT_EXPORT_FROM(swift_Concurrency) -extern void *_Nullable swift_task_enqueueMainExecutor_hook; +extern void *_Nullable swift_task_enqueueMainExecutor_hook SWIFT_NONISOLATED_UNSAFE; /// A hook to override the entrypoint to the main runloop used to drive the /// concurrency runtime and drain the main queue. This function must not return. @@ -59,13 +61,13 @@ typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_original)(); typedef SWIFT_CC(swift) void (*swift_task_asyncMainDrainQueue_override)( swift_task_asyncMainDrainQueue_original _Nullable original); SWIFT_EXPORT_FROM(swift_Concurrency) -extern void *_Nullable swift_task_asyncMainDrainQueue_hook; +extern void *_Nullable swift_task_asyncMainDrainQueue_hook SWIFT_NONISOLATED_UNSAFE; /// MARK: - thread local storage extern _Thread_local void * _Nullable swjs_thread_local_event_loop; -extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker; +extern _Thread_local void * _Nullable swjs_thread_local_task_executor_worker SWIFT_NONISOLATED_UNSAFE; #endif From fa77908b7a9b5d6ac914bc886ee282ebb2403611 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:01:06 +0000 Subject: [PATCH 07/19] Concurrency: Remove `@Sendable` requirement from scheduling primitives They are accessed from a single thread, so there is no need to enforce `@Sendable` requirement on them. And also the following code is not working with `@Sendable` requirement because the captured `JSPromise` is not `Sendable`. ``` let promise = JSPromise(resolver: { resolver -> Void in resolver(.success(.undefined)) }) let setTimeout = JSObject.global.setTimeout.function! let eventLoop = JavaScriptEventLoop( queueTask: { job in // TODO(katei): Should prefer `queueMicrotask` if available? // We should measure if there is performance advantage. promise.then { _ in job() return JSValue.undefined } }, setTimeout: { delay, job in setTimeout(JSOneshotClosure { _ in job() return JSValue.undefined }, delay) } ) ``` --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index af8738ef8..867fb070a 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -40,17 +40,17 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// A function that queues a given closure as a microtask into JavaScript event loop. /// See also: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide - public var queueMicrotask: @Sendable (@escaping () -> Void) -> Void + public var queueMicrotask: (@escaping () -> Void) -> Void /// A function that invokes a given closure after a specified number of milliseconds. - public var setTimeout: @Sendable (Double, @escaping () -> Void) -> Void + public var setTimeout: (Double, @escaping () -> Void) -> Void /// A mutable state to manage internal job queue /// Note that this should be guarded atomically when supporting multi-threaded environment. var queueState = QueueState() private init( - queueTask: @Sendable @escaping (@escaping () -> Void) -> Void, - setTimeout: @Sendable @escaping (Double, @escaping () -> Void) -> Void + queueTask: @escaping (@escaping () -> Void) -> Void, + setTimeout: @escaping (Double, @escaping () -> Void) -> Void ) { self.queueMicrotask = queueTask self.setTimeout = setTimeout From 97aad009327a645d2296b43160da4ce9f3f6b933 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:07:39 +0000 Subject: [PATCH 08/19] Concurrency: Fix sendability errors around `JSClosure.async` --- .../BasicObjects/JSPromise.swift | 14 +++---- .../FundamentalObjects/JSClosure.swift | 40 +++++++++++++------ 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index a41a3e1ca..0580c23bb 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -90,7 +90,7 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { + public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { try await success($0[0]).jsValue } @@ -101,8 +101,8 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then( - success: @escaping (JSValue) -> ConvertibleToJSValue, - failure: @escaping (JSValue) -> ConvertibleToJSValue + success: @escaping (sending JSValue) -> ConvertibleToJSValue, + failure: @escaping (sending JSValue) -> ConvertibleToJSValue ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue @@ -117,8 +117,8 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async throws -> ConvertibleToJSValue, - failure: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise + public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue, + failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let successClosure = JSOneshotClosure.async { try await success($0[0]).jsValue @@ -132,7 +132,7 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult - public func `catch`(failure: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure { failure($0[0]).jsValue } @@ -143,7 +143,7 @@ public final class JSPromise: JSBridgedClass { /// 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: @escaping (JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { + public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { let closure = JSOneshotClosure.async { try await failure($0[0]).jsValue } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index dafd4ce38..81f2540b6 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -15,7 +15,7 @@ public protocol JSClosureProtocol: JSValueCompatible { public class JSOneshotClosure: JSObject, JSClosureProtocol { private var hostFuncRef: JavaScriptHostFuncRef = 0 - public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { + public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) @@ -34,7 +34,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { + public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) } #endif @@ -64,10 +64,10 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { public class JSClosure: JSFunction, JSClosureProtocol { class SharedJSClosure { - private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: ([JSValue]) -> JSValue)] = [:] + private var storage: [JavaScriptHostFuncRef: (object: JSObject, body: (sending [JSValue]) -> JSValue)] = [:] init() {} - subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: ([JSValue]) -> JSValue)? { + subscript(_ key: JavaScriptHostFuncRef) -> (object: JSObject, body: (sending [JSValue]) -> JSValue)? { get { storage[key] } set { storage[key] = newValue } } @@ -93,7 +93,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { }) } - public init(_ body: @escaping ([JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { + public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) { // 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`. super.init(id: 0) @@ -109,7 +109,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { + public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) } #endif @@ -125,18 +125,29 @@ public class JSClosure: JSFunction, JSClosureProtocol { #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { +private func makeAsyncClosure( + _ body: sending @escaping (sending [JSValue]) async throws -> JSValue +) -> ((sending [JSValue]) -> JSValue) { { arguments in JSPromise { resolver in + // NOTE: The context is fully transferred to the unstructured task + // isolation but the compiler can't prove it yet, so we need to + // use `@unchecked Sendable` to make it compile with the Swift 6 mode. + struct Context: @unchecked Sendable { + let resolver: (JSPromise.Result) -> Void + let arguments: [JSValue] + let body: (sending [JSValue]) async throws -> JSValue + } + let context = Context(resolver: resolver, arguments: arguments, body: body) Task { do { - let result = try await body(arguments) - resolver(.success(result)) + let result = try await context.body(context.arguments) + context.resolver(.success(result)) } catch { if let jsError = error as? JSError { - resolver(.failure(jsError.jsValue)) + context.resolver(.failure(jsError.jsValue)) } else { - resolver(.failure(JSError(message: String(describing: error)).jsValue)) + context.resolver(.failure(JSError(message: String(describing: error)).jsValue)) } } } @@ -183,13 +194,16 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa @_cdecl("_call_host_function_impl") func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, - _ argv: UnsafePointer<RawJSValue>, _ argc: Int32, + _ argv: sending UnsafePointer<RawJSValue>, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { return true } - let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue} + var arguments: [JSValue] = [] + for i in 0..<Int(argc) { + arguments.append(argv[i].jsValue) + } let result = hostFunc(arguments) let callbackFuncRef = JSFunction(id: callbackFuncRef) _ = callbackFuncRef(result) From 75b8cd3c2935f28d3e5bc21e28157ac4fff32211 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:17:37 +0000 Subject: [PATCH 09/19] Concurrency: Introduce `JSException` and remove `Error` conformance from `JSValue` This is a breaking change. It introduces a new `JSException` type to represent exceptions thrown from JavaScript code. This change is necessary to remove `Sendable` conformance from `JSValue`, which is derived from `Error` conformance. --- .../JavaScriptEventLoop.swift | 6 ++-- .../BasicObjects/JSPromise.swift | 10 +++++- .../FundamentalObjects/JSClosure.swift | 4 +-- .../JSThrowingFunction.swift | 6 ++-- Sources/JavaScriptKit/JSException.swift | 34 +++++++++++++++++++ Sources/JavaScriptKit/JSValue.swift | 2 -- 6 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 Sources/JavaScriptKit/JSException.swift diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 867fb070a..b9e89a375 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -218,7 +218,7 @@ public extension JSPromise { return JSValue.undefined }, failure: { - continuation.resume(throwing: $0) + continuation.resume(throwing: JSException($0)) return JSValue.undefined } ) @@ -227,7 +227,7 @@ public extension JSPromise { } /// Wait for the promise to complete, returning its result or exception as a Result. - var result: Result<JSValue, JSValue> { + var result: Swift.Result<JSValue, JSException> { get async { await withUnsafeContinuation { [self] continuation in self.then( @@ -236,7 +236,7 @@ public extension JSPromise { return JSValue.undefined }, failure: { - continuation.resume(returning: .failure($0)) + continuation.resume(returning: .failure(JSException($0))) return JSValue.undefined } ) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 0580c23bb..1aec5f4af 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -31,6 +31,14 @@ public final class JSPromise: JSBridgedClass { return Self(jsObject) } + /// The result of a promise. + public enum Result { + /// The promise resolved with a value. + case success(JSValue) + /// The promise rejected with a value. + case failure(JSValue) + } + /// Creates a new `JSPromise` instance from a given `resolver` closure. /// The closure is passed a completion handler. Passing a successful /// `Result` to the completion handler will cause the promise to resolve @@ -38,7 +46,7 @@ public final class JSPromise: JSBridgedClass { /// promise to reject with the corresponding value. /// Calling the completion handler more than once will have no effect /// (per the JavaScript specification). - public convenience init(resolver: @escaping (@escaping (Result<JSValue, JSValue>) -> Void) -> Void) { + public convenience init(resolver: @escaping (@escaping (Result) -> Void) -> Void) { let closure = JSOneshotClosure { arguments in // The arguments are always coming from the `Promise` constructor, so we should be // safe to assume their type here diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 81f2540b6..8c42d2ac4 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -144,8 +144,8 @@ private func makeAsyncClosure( let result = try await context.body(context.arguments) context.resolver(.success(result)) } catch { - if let jsError = error as? JSError { - context.resolver(.failure(jsError.jsValue)) + if let jsError = error as? JSException { + context.resolver(.failure(jsError.thrownValue)) } else { context.resolver(.failure(JSError(message: String(describing: error)).jsValue)) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 8b4fc7cde..17b61090f 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -37,7 +37,7 @@ public class JSThrowingFunction { /// - Parameter arguments: Arguments to be passed to this constructor function. /// - Returns: A new instance of this constructor. public func new(arguments: [ConvertibleToJSValue]) throws -> JSObject { - try arguments.withRawJSValues { rawValues -> Result<JSObject, JSValue> in + try arguments.withRawJSValues { rawValues -> Result<JSObject, JSException> in rawValues.withUnsafeBufferPointer { bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count @@ -52,7 +52,7 @@ public class JSThrowingFunction { let exceptionKind = JavaScriptValueKindAndFlags(bitPattern: exceptionRawKind) if exceptionKind.isException { let exception = RawJSValue(kind: exceptionKind.kind, payload1: exceptionPayload1, payload2: exceptionPayload2) - return .failure(exception.jsValue) + return .failure(JSException(exception.jsValue)) } return .success(JSObject(id: resultObj)) } @@ -92,7 +92,7 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV } } if isException { - throw result + throw JSException(result) } return result } diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift new file mode 100644 index 000000000..7f1959c70 --- /dev/null +++ b/Sources/JavaScriptKit/JSException.swift @@ -0,0 +1,34 @@ +/// `JSException` is a wrapper that handles exceptions thrown during JavaScript execution as Swift +/// `Error` objects. +/// When a JavaScript function throws an exception, it's wrapped as a `JSException` and propagated +/// through Swift's error handling mechanism. +/// +/// Example: +/// ```swift +/// do { +/// try jsFunction.throws() +/// } catch let error as JSException { +/// // Access the value thrown from JavaScript +/// let jsErrorValue = error.thrownValue +/// } +/// ``` +public struct JSException: Error { + /// The value thrown from JavaScript. + /// This can be any JavaScript value (error object, string, number, etc.). + public var thrownValue: JSValue { + return _thrownValue + } + + /// The actual JavaScript value that was thrown. + /// + /// Marked as `nonisolated(unsafe)` to satisfy `Sendable` requirement + /// from `Error` protocol. + private nonisolated(unsafe) let _thrownValue: JSValue + + /// Initializes a new JSException instance with a value thrown from JavaScript. + /// + /// Only available within the package. + package init(_ thrownValue: JSValue) { + self._thrownValue = thrownValue + } +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index ed44f50ea..1efffe484 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -124,8 +124,6 @@ public extension JSValue { } } -extension JSValue: Swift.Error {} - public extension JSValue { func fromJSValue<Type>() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) From d1781a8c596bb14819a80116bc8d13870e316145 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:21:52 +0000 Subject: [PATCH 10/19] CI: Remove Xcode 15.2 (Swift 5.9) from the matrix --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index daac3c50f..f87d3c5f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,8 +52,6 @@ jobs: strategy: matrix: include: - - os: macos-14 - xcode: Xcode_15.2 - os: macos-15 xcode: Xcode_16 runs-on: ${{ matrix.os }} From 39c207b4e45ad92137ef149fe9ea83c92e9cad14 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:42:53 +0000 Subject: [PATCH 11/19] Fix `JAVASCRIPTKIT_WITHOUT_WEAKREFS` build --- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 8c42d2ac4..c1f0361da 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -221,7 +221,7 @@ func _call_host_function_impl( extension JSClosure { public func release() { isReleased = true - Self.sharedClosures[hostFuncRef] = nil + Self.sharedClosures.wrappedValue[hostFuncRef] = nil } } From 0fc7f41c573c3ad25d4367bf591d3c0008bcc303 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:43:19 +0000 Subject: [PATCH 12/19] Concurrency: Use `JSPromise.Result` instead of `Swift.Result` for `JSPromise.result` To reduce burden type casting, it's better to remove the wrapper from the API. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index b9e89a375..c0141cd63 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -227,7 +227,7 @@ public extension JSPromise { } /// Wait for the promise to complete, returning its result or exception as a Result. - var result: Swift.Result<JSValue, JSException> { + var result: JSPromise.Result { get async { await withUnsafeContinuation { [self] continuation in self.then( @@ -236,7 +236,7 @@ public extension JSPromise { return JSValue.undefined }, failure: { - continuation.resume(returning: .failure(JSException($0))) + continuation.resume(returning: .failure($0)) return JSValue.undefined } ) From 899fa637f04d34728401cba2984073e95b802c20 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:44:36 +0000 Subject: [PATCH 13/19] Add `Equatable` conformances to new types --- Sources/JavaScriptKit/BasicObjects/JSPromise.swift | 2 +- Sources/JavaScriptKit/JSException.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 1aec5f4af..cfe32d515 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -32,7 +32,7 @@ public final class JSPromise: JSBridgedClass { } /// The result of a promise. - public enum Result { + public enum Result: Equatable { /// The promise resolved with a value. case success(JSValue) /// The promise rejected with a value. diff --git a/Sources/JavaScriptKit/JSException.swift b/Sources/JavaScriptKit/JSException.swift index 7f1959c70..393ae9615 100644 --- a/Sources/JavaScriptKit/JSException.swift +++ b/Sources/JavaScriptKit/JSException.swift @@ -12,7 +12,7 @@ /// let jsErrorValue = error.thrownValue /// } /// ``` -public struct JSException: Error { +public struct JSException: Error, Equatable { /// The value thrown from JavaScript. /// This can be any JavaScript value (error object, string, number, etc.). public var thrownValue: JSValue { From 042e26e8740fb084e52c58f3f34867b2795f25a4 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:45:20 +0000 Subject: [PATCH 14/19] Concurency: Remove `@MainActor` requirement from `JSEL.installGlobalExecutor` The installation of the global executor should be done before any job scheduling, so it should be able to be called at top-level immediately executed code. --- Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index c0141cd63..07eec2cd2 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -109,7 +109,13 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// This installation step will be unnecessary after custom executor are /// 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) - @MainActor public static func installGlobalExecutor() { + public static func installGlobalExecutor() { + MainActor.assumeIsolated { + Self.installGlobalExecutorIsolated() + } + } + + @MainActor private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } #if compiler(>=5.9) From 22572338eb7eed5624f7fcf76975dfa6f5c0d3e6 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:47:16 +0000 Subject: [PATCH 15/19] Concurrency: Adjust test cases for new exception handling --- .../Sources/ConcurrencyTests/main.swift | 4 +- .../Sources/PrimaryTests/UnitTestUtils.swift | 2 +- .../Sources/PrimaryTests/main.swift | 37 +++++++++---------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift index ece58b317..1f0764e14 100644 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift @@ -48,7 +48,7 @@ func entrypoint() async throws { resolve(.failure(.number(3))) }) let error = try await expectAsyncThrow(await p.value) - let jsValue = try expectCast(error, to: JSValue.self) + let jsValue = try expectCast(error, to: JSException.self).thrownValue try expectEqual(jsValue, 3) try await expectEqual(p.result, .failure(.number(3))) } @@ -157,7 +157,7 @@ func entrypoint() async throws { ) } let promise2 = promise.then { _ in - throw JSError(message: "should not succeed") + throw MessageError("Should not be called", file: #file, line: #line, column: #column) } failure: { err in return err } diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index c4f9a9fb1..0d51c6ff5 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -110,7 +110,7 @@ func expectThrow<T>(_ body: @autoclosure () throws -> T, file: StaticString = #f throw MessageError("Expect to throw an exception", file: file, line: line, column: column) } -func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { +func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSValue { JSObject.global.callThrowingClosure.function!(JSClosure { _ in body() return .undefined diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 67a51aa2e..12cc91cc9 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -263,8 +263,8 @@ try test("Closure Lifetime") { let c1Line = #line + 1 let c1 = JSClosure { $0[0] } c1.release() - let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSValue - try expect("Error message should contains definition location", error.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) + let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSException + try expect("Error message should contains definition location", error.thrownValue.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) } #endif @@ -275,8 +275,8 @@ try test("Closure Lifetime") { do { let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } - let error = try expectThrow(try evalClosure.throws(c1)) as! JSValue - try expectEqual(error.description, "RuntimeError: unreachable") + let error = try expectThrow(try evalClosure.throws(c1)) as! JSException + try expectEqual(error.thrownValue.description, "RuntimeError: unreachable") } } @@ -770,32 +770,32 @@ try test("Exception") { // MARK: Throwing method calls let error1 = try expectThrow(try prop_9.object!.throwing.func1!()) - try expectEqual(error1 is JSValue, true) - let errorObject = JSError(from: error1 as! JSValue) + try expectEqual(error1 is JSException, true) + let errorObject = JSError(from: (error1 as! JSException).thrownValue) try expectNotNil(errorObject) let error2 = try expectThrow(try prop_9.object!.throwing.func2!()) - try expectEqual(error2 is JSValue, true) - let errorString = try expectString(error2 as! JSValue) + try expectEqual(error2 is JSException, true) + let errorString = try expectString((error2 as! JSException).thrownValue) try expectEqual(errorString, "String Error") let error3 = try expectThrow(try prop_9.object!.throwing.func3!()) - try expectEqual(error3 is JSValue, true) - let errorNumber = try expectNumber(error3 as! JSValue) + try expectEqual(error3 is JSException, true) + let errorNumber = try expectNumber((error3 as! JSException).thrownValue) try expectEqual(errorNumber, 3.0) // MARK: Simple function calls let error4 = try expectThrow(try prop_9.func1.function!.throws()) - try expectEqual(error4 is JSValue, true) - let errorObject2 = JSError(from: error4 as! JSValue) + try expectEqual(error4 is JSException, true) + let errorObject2 = JSError(from: (error4 as! JSException).thrownValue) try expectNotNil(errorObject2) // MARK: Throwing constructor call let Animal = JSObject.global.Animal.function! _ = try Animal.throws.new("Tama", 3, true) let ageError = try expectThrow(try Animal.throws.new("Tama", -3, true)) - try expectEqual(ageError is JSValue, true) - let errorObject3 = JSError(from: ageError as! JSValue) + try expectEqual(ageError is JSException, true) + let errorObject3 = JSError(from: (ageError as! JSException).thrownValue) try expectNotNil(errorObject3) } @@ -824,18 +824,15 @@ try test("Unhandled Exception") { // MARK: Throwing method calls let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() } - try expectEqual(error1 is JSValue, true) - let errorObject = JSError(from: error1 as! JSValue) + let errorObject = JSError(from: error1) try expectNotNil(errorObject) let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() } - try expectEqual(error2 is JSValue, true) - let errorString = try expectString(error2 as! JSValue) + let errorString = try expectString(error2) try expectEqual(errorString, "String Error") let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() } - try expectEqual(error3 is JSValue, true) - let errorNumber = try expectNumber(error3 as! JSValue) + let errorNumber = try expectNumber(error3) try expectEqual(errorNumber, 3.0) } From 0c43cbfd67ae8bf0969da51c9d15d181cbe13f7f Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 07:58:09 +0000 Subject: [PATCH 16/19] CI: Update Swift toolchain to 2025-02-26-a Our new code htis assertion in 2024-10-30-a, but it's fixed in 2025-02-26-a. --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f87d3c5f5..1c8dae632 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,11 +15,11 @@ jobs: wasi-backend: Node - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + 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 wasi-backend: Node - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + 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 wasi-backend: Node runs-on: ${{ matrix.entry.os }} @@ -69,7 +69,7 @@ jobs: entry: - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a/swift-DEVELOPMENT-SNAPSHOT-2024-10-30-a-ubuntu22.04.tar.gz + 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 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/install-swift From 7a7acb44ea71c58a9ccdb2a6e6f95059d8e624d1 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 08:02:02 +0000 Subject: [PATCH 17/19] Concurrency: Remove unnecessary `sending` keyword --- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c1f0361da..c075c63e5 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -194,7 +194,7 @@ private func makeAsyncClosure( @_cdecl("_call_host_function_impl") func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, - _ argv: sending UnsafePointer<RawJSValue>, _ argc: Int32, + _ argv: UnsafePointer<RawJSValue>, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef ) -> Bool { guard let (_, hostFunc) = JSClosure.sharedClosures.wrappedValue[hostFuncRef] else { From 18ad4e3be8465167af62172b67d64da2fdaab3e2 Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 08:13:18 +0000 Subject: [PATCH 18/19] Swift 6.1 and later uses .xctest for XCTest bundle --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1b653315c..88f4e0795 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,18 @@ test: CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test +TEST_RUNNER := node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs .PHONY: unittest unittest: @echo Running unit tests swift build --build-tests -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export-if-defined=main -Xlinker --export-if-defined=__main_argc_argv --static-swift-stdlib -Xswiftc -static-stdlib $(SWIFT_BUILD_FLAGS) - node --experimental-wasi-unstable-preview1 scripts/test-harness.mjs ./.build/debug/JavaScriptKitPackageTests.wasm +# Swift 6.1 and later uses .xctest for XCTest bundle but earliers used .wasm +# See https://github.com/swiftlang/swift-package-manager/pull/8254 + if [ -f .build/debug/JavaScriptKitPackageTests.xctest ]; then \ + $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.xctest; \ + else \ + $(TEST_RUNNER) .build/debug/JavaScriptKitPackageTests.wasm; \ + fi .PHONY: benchmark_setup benchmark_setup: From 3f3b494adf034ec72b24c577f3bd3a11d7ae8a2b Mon Sep 17 00:00:00 2001 From: Yuta Saito <kateinoigakukun@gmail.com> Date: Wed, 5 Mar 2025 08:34:53 +0000 Subject: [PATCH 19/19] Concurrency: Explicitly mark `Sendable` conformance as unavailable for `JSValue` --- Sources/JavaScriptKit/JSValue.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 1efffe484..2562daac8 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -100,6 +100,13 @@ public enum JSValue: Equatable { } } +/// JSValue is intentionally not `Sendable` because accessing a JSValue living in a different +/// thread is invalid. Although there are some cases where Swift allows sending a non-Sendable +/// values to other isolation domains, not conforming `Sendable` is still useful to prevent +/// accidental misuse. +@available(*, unavailable) +extension JSValue: Sendable {} + public extension JSValue { #if !hasFeature(Embedded) /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?`