Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 71 additions & 5 deletions Sources/JavaScriptExecution/JavaScriptModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,33 @@ extension JavaScriptModule {
)
}
}

@MainActor extension JavaScriptModule {
/// Register an async callback that always runs on the `@MainActor`. If JavaScript code
/// calls it from a Web Worker, it will have to hop threads to run on the main actor, which
/// could be slow. However, it is efficient for JavaScript callers from the main thread, and
/// permits the passage of non-Sendable parameter types (such as ``JSObject``) to
/// asynchronous functions, as it is known that the callback will not leave the region where
/// the non-Sendable parameter is valid for.
@inlinable public subscript<each T, U>(
symbol: Symbol
) -> @MainActor (repeat each T) async throws -> U
where repeat each T: LoadableFromJSValue,
U: ConvertibleToJSValue & SendableMetatype {
get {
{ (_: repeat each T) in fatalError("no implementation provided") }
}
nonmutating set(yield) {
self.register(as: symbol) { (argument: repeat each T) -> JSValue in
try await yield(repeat each argument).jsValue
}
}
}

@inlinable public subscript<each T>(
symbol: Symbol
) -> (repeat each T) async throws -> ()
where repeat each T: LoadableFromJSValue & Sendable {
) -> @MainActor (repeat each T) async throws -> ()
where repeat each T: LoadableFromJSValue {
get {
{ (_: repeat each T) in fatalError("no implementation provided") }
}
Expand All @@ -70,23 +92,67 @@ extension JavaScriptModule {
}
}
}

@inlinable func register<each T>(
as symbol: Symbol,
operation: @MainActor @escaping (repeat each T) async throws -> JSValue
) where repeat each T: LoadableFromJSValue {
self.metatype[symbol.id] = .object(
JSClosure.async {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Under Swift 6 strict concurrency, passing non-Sendable parameters (each T) across isolation boundaries from a nonisolated context to the @MainActor operation closure will trigger compiler warnings or errors. Since the callback is registered to run on the @MainActor, the closure passed to JSClosure.async should be explicitly isolated to @MainActor as well. This ensures that argument extraction and the subsequent call to operation occur entirely on the main actor, preventing any unsafe crossing of isolation boundaries.

            JSClosure.async { @MainActor in

var arguments: JavaScriptArguments = .init(list: $0)
do {
return try await operation(
repeat try arguments.next(as: (each T).self)
)
} catch let error {
print("Error in '\(symbol.id)': \(error)")
dump(error)
//let error: JSObject = JSError.constructor!.new("\(error)")
return .undefined
}
}
)
}
}

@MainActor extension JavaScriptModule {
/// Register an async callback that always runs in the same region as the caller. If
/// JavaScript code calls it from a Web Worker, it will run on the worker thread. However,
/// the parameter types and the return type must be Sendable. The Swift compiler is not
/// currently able to prove statically that a non-Sendable value would not leave the region
/// where it is valid for.
@inlinable public subscript<each T, U>(
symbol: Symbol
nonisolated symbol: Symbol
) -> (repeat each T) async throws -> sending U
where repeat each T: LoadableFromJSValue & Sendable,
U: ConvertibleToJSValue & SendableMetatype {
get {
{ (_: repeat each T) in fatalError("no implementation provided") }
}
nonmutating set(yield) {
self.register(as: symbol) { (argument: repeat each T) -> sending JSValue in
self.register(nonisolated: symbol) { (argument: repeat each T) -> sending JSValue in
try await yield(repeat each argument).jsValue
}
}
}

@inlinable public subscript<each T>(
nonisolated symbol: Symbol
) -> (repeat each T) async throws -> ()
where repeat each T: LoadableFromJSValue & Sendable {
get {
{ (_: repeat each T) in fatalError("no implementation provided") }
}
nonmutating set(yield) {
self.register(nonisolated: symbol) { (argument: repeat each T) in
try await yield(repeat each argument)
return .undefined
}
}
}

@inlinable func register<each T>(
as symbol: Symbol,
nonisolated symbol: Symbol,
operation: sending @escaping (repeat each T) async throws -> sending JSValue
) where repeat each T: LoadableFromJSValue & Sendable {
self.metatype[symbol.id] = .object(
Expand Down