Skip to content

Commit cc62d17

Browse files
Merge pull request #383 from swiftwasm/yt/add-async-closure-executor-pref
Add JSClosure APIs to support specifying TaskExecutor and TaskPriority
2 parents 83e2335 + a3a3868 commit cc62d17

File tree

4 files changed

+288
-90
lines changed

4 files changed

+288
-90
lines changed

Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public protocol JSClosureProtocol: JSValueCompatible {
1818
public class JSOneshotClosure: JSObject, JSClosureProtocol {
1919
private var hostFuncRef: JavaScriptHostFuncRef = 0
2020

21-
public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
21+
public init(file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue) {
2222
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
2323
super.init(id: 0)
2424

@@ -44,11 +44,40 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol {
4444
}
4545

4646
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
47+
/// Creates a new `JSOneshotClosure` that calls the given Swift function asynchronously.
48+
///
49+
/// - Parameters:
50+
/// - priority: The priority of the new unstructured Task created under the hood.
51+
/// - body: The Swift function to call asynchronously.
4752
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
4853
public static func async(
54+
priority: TaskPriority? = nil,
55+
file: String = #fileID,
56+
line: UInt32 = #line,
4957
_ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue
5058
) -> JSOneshotClosure {
51-
JSOneshotClosure(makeAsyncClosure(body))
59+
JSOneshotClosure(file: file, line: line, makeAsyncClosure(priority: priority, body))
60+
}
61+
62+
/// Creates a new `JSOneshotClosure` that calls the given Swift function asynchronously.
63+
///
64+
/// - Parameters:
65+
/// - taskExecutor: The executor preference of the new unstructured Task created under the hood.
66+
/// - priority: The priority of the new unstructured Task created under the hood.
67+
/// - body: The Swift function to call asynchronously.
68+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
69+
public static func async(
70+
executorPreference taskExecutor: (any TaskExecutor)? = nil,
71+
priority: TaskPriority? = nil,
72+
file: String = #fileID,
73+
line: UInt32 = #line,
74+
_ body: @Sendable @escaping (sending [JSValue]) async throws(JSException) -> JSValue
75+
) -> JSOneshotClosure {
76+
JSOneshotClosure(
77+
file: file,
78+
line: line,
79+
makeAsyncClosure(executorPreference: taskExecutor, priority: priority, body)
80+
)
5281
}
5382
#endif
5483

@@ -117,7 +146,7 @@ public class JSClosure: JSFunction, JSClosureProtocol {
117146
})
118147
}
119148

120-
public init(_ body: @escaping (sending [JSValue]) -> JSValue, file: String = #fileID, line: UInt32 = #line) {
149+
public init(file: String = #fileID, line: UInt32 = #line, _ body: @escaping (sending [JSValue]) -> JSValue) {
121150
// 1. Fill `id` as zero at first to access `self` to get `ObjectIdentifier`.
122151
super.init(id: 0)
123152

@@ -137,11 +166,36 @@ public class JSClosure: JSFunction, JSClosureProtocol {
137166
}
138167

139168
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
169+
/// Creates a new `JSClosure` that calls the given Swift function asynchronously.
170+
///
171+
/// - Parameters:
172+
/// - priority: The priority of the new unstructured Task created under the hood.
173+
/// - body: The Swift function to call asynchronously.
140174
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
141175
public static func async(
142-
_ body: @Sendable @escaping (sending [JSValue]) async throws(JSException) -> JSValue
176+
priority: TaskPriority? = nil,
177+
file: String = #fileID,
178+
line: UInt32 = #line,
179+
_ body: sending @escaping @isolated(any) (sending [JSValue]) async throws(JSException) -> JSValue
180+
) -> JSClosure {
181+
JSClosure(file: file, line: line, makeAsyncClosure(priority: priority, body))
182+
}
183+
184+
/// Creates a new `JSClosure` that calls the given Swift function asynchronously.
185+
///
186+
/// - Parameters:
187+
/// - taskExecutor: The executor preference of the new unstructured Task created under the hood.
188+
/// - priority: The priority of the new unstructured Task created under the hood.
189+
/// - body: The Swift function to call asynchronously.
190+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
191+
public static func async(
192+
executorPreference taskExecutor: (any TaskExecutor)? = nil,
193+
priority: TaskPriority? = nil,
194+
file: String = #fileID,
195+
line: UInt32 = #line,
196+
_ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue
143197
) -> JSClosure {
144-
JSClosure(makeAsyncClosure(body))
198+
JSClosure(file: file, line: line, makeAsyncClosure(executorPreference: taskExecutor, priority: priority, body))
145199
}
146200
#endif
147201

@@ -157,6 +211,36 @@ public class JSClosure: JSFunction, JSClosureProtocol {
157211
#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI))
158212
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
159213
private func makeAsyncClosure(
214+
priority: TaskPriority?,
215+
_ body: sending @escaping @isolated(any) (sending [JSValue]) async throws(JSException) -> JSValue
216+
) -> ((sending [JSValue]) -> JSValue) {
217+
{ arguments in
218+
JSPromise { resolver in
219+
// NOTE: The context is fully transferred to the unstructured task
220+
// isolation but the compiler can't prove it yet, so we need to
221+
// use `@unchecked Sendable` to make it compile with the Swift 6 mode.
222+
struct Context: @unchecked Sendable {
223+
let resolver: (JSPromise.Result) -> Void
224+
let arguments: [JSValue]
225+
let body: (sending [JSValue]) async throws(JSException) -> JSValue
226+
}
227+
let context = Context(resolver: resolver, arguments: arguments, body: body)
228+
Task(priority: priority) {
229+
do throws(JSException) {
230+
let result = try await context.body(context.arguments)
231+
context.resolver(.success(result))
232+
} catch {
233+
context.resolver(.failure(error.thrownValue))
234+
}
235+
}
236+
}.jsValue()
237+
}
238+
}
239+
240+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
241+
private func makeAsyncClosure(
242+
executorPreference taskExecutor: (any TaskExecutor)?,
243+
priority: TaskPriority?,
160244
_ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue
161245
) -> ((sending [JSValue]) -> JSValue) {
162246
{ arguments in
@@ -170,7 +254,7 @@ private func makeAsyncClosure(
170254
let body: (sending [JSValue]) async throws(JSException) -> JSValue
171255
}
172256
let context = Context(resolver: resolver, arguments: arguments, body: body)
173-
Task {
257+
Task(executorPreference: taskExecutor, priority: priority) {
174258
do throws(JSException) {
175259
let result = try await context.body(context.arguments)
176260
context.resolver(.success(result))
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import JavaScriptKit
2+
import XCTest
3+
4+
class JSClosureAsyncTests: XCTestCase {
5+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
6+
final class AnyTaskExecutor: TaskExecutor {
7+
func enqueue(_ job: UnownedJob) {
8+
job.runSynchronously(on: asUnownedTaskExecutor())
9+
}
10+
}
11+
12+
final class UnsafeSendableBox<T>: @unchecked Sendable {
13+
var value: T
14+
init(_ value: T) {
15+
self.value = value
16+
}
17+
}
18+
19+
func testAsyncClosure() async throws {
20+
let closure = JSClosure.async { _ in
21+
return (42.0).jsValue
22+
}.jsValue
23+
let result = try await JSPromise(from: closure.function!())!.value()
24+
XCTAssertEqual(result, 42.0)
25+
}
26+
27+
func testAsyncClosureWithPriority() async throws {
28+
let priority = UnsafeSendableBox<TaskPriority?>(nil)
29+
let closure = JSClosure.async(priority: .high) { _ in
30+
priority.value = Task.currentPriority
31+
return (42.0).jsValue
32+
}.jsValue
33+
let result = try await JSPromise(from: closure.function!())!.value()
34+
XCTAssertEqual(result, 42.0)
35+
XCTAssertEqual(priority.value, .high)
36+
}
37+
38+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
39+
func testAsyncClosureWithTaskExecutor() async throws {
40+
let executor = AnyTaskExecutor()
41+
let closure = JSClosure.async(executorPreference: executor) { _ in
42+
return (42.0).jsValue
43+
}.jsValue
44+
let result = try await JSPromise(from: closure.function!())!.value()
45+
XCTAssertEqual(result, 42.0)
46+
}
47+
48+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
49+
func testAsyncClosureWithTaskExecutorPreference() async throws {
50+
let executor = AnyTaskExecutor()
51+
let priority = UnsafeSendableBox<TaskPriority?>(nil)
52+
let closure = JSClosure.async(executorPreference: executor, priority: .high) { _ in
53+
priority.value = Task.currentPriority
54+
return (42.0).jsValue
55+
}.jsValue
56+
let result = try await JSPromise(from: closure.function!())!.value()
57+
XCTAssertEqual(result, 42.0)
58+
XCTAssertEqual(priority.value, .high)
59+
}
60+
61+
// TODO: Enable the following tests once:
62+
// - Make JSObject a final-class
63+
// - Unify JSFunction and JSObject into JSValue
64+
// - Make JS(Oneshot)Closure as a wrapper of JSObject, not a subclass
65+
/*
66+
func testAsyncOneshotClosure() async throws {
67+
let closure = JSOneshotClosure.async { _ in
68+
return (42.0).jsValue
69+
}.jsValue
70+
let result = try await JSPromise(
71+
from: closure.function!()
72+
)!.value()
73+
XCTAssertEqual(result, 42.0)
74+
}
75+
76+
func testAsyncOneshotClosureWithPriority() async throws {
77+
let priority = UnsafeSendableBox<TaskPriority?>(nil)
78+
let closure = JSOneshotClosure.async(priority: .high) { _ in
79+
priority.value = Task.currentPriority
80+
return (42.0).jsValue
81+
}.jsValue
82+
let result = try await JSPromise(from: closure.function!())!.value()
83+
XCTAssertEqual(result, 42.0)
84+
XCTAssertEqual(priority.value, .high)
85+
}
86+
87+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
88+
func testAsyncOneshotClosureWithTaskExecutor() async throws {
89+
let executor = AnyTaskExecutor()
90+
let closure = JSOneshotClosure.async(executorPreference: executor) { _ in
91+
return (42.0).jsValue
92+
}.jsValue
93+
let result = try await JSPromise(from: closure.function!())!.value()
94+
XCTAssertEqual(result, 42.0)
95+
}
96+
97+
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
98+
func testAsyncOneshotClosureWithTaskExecutorPreference() async throws {
99+
let executor = AnyTaskExecutor()
100+
let priority = UnsafeSendableBox<TaskPriority?>(nil)
101+
let closure = JSOneshotClosure.async(executorPreference: executor, priority: .high) { _ in
102+
priority.value = Task.currentPriority
103+
return (42.0).jsValue
104+
}.jsValue
105+
let result = try await JSPromise(from: closure.function!())!.value()
106+
XCTAssertEqual(result, 42.0)
107+
XCTAssertEqual(priority.value, .high)
108+
}
109+
*/
110+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import JavaScriptKit
2+
import XCTest
3+
4+
class JSClosureTests: XCTestCase {
5+
func testClosureLifetime() {
6+
let evalClosure = JSObject.global.globalObject1.eval_closure.function!
7+
8+
do {
9+
let c1 = JSClosure { arguments in
10+
return arguments[0]
11+
}
12+
XCTAssertEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0))
13+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
14+
c1.release()
15+
#endif
16+
}
17+
18+
do {
19+
let array = JSObject.global.Array.function!.new()
20+
let c1 = JSClosure { _ in .number(3) }
21+
_ = array.push!(c1)
22+
XCTAssertEqual(array[0].function!().number, 3.0)
23+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
24+
c1.release()
25+
#endif
26+
}
27+
28+
do {
29+
let c1 = JSClosure { _ in .undefined }
30+
XCTAssertEqual(c1(), .undefined)
31+
}
32+
33+
do {
34+
let c1 = JSClosure { _ in .number(4) }
35+
XCTAssertEqual(c1(), .number(4))
36+
}
37+
}
38+
39+
func testHostFunctionRegistration() {
40+
// ```js
41+
// global.globalObject1 = {
42+
// ...
43+
// "prop_6": {
44+
// "call_host_1": function() {
45+
// return global.globalObject1.prop_6.host_func_1()
46+
// }
47+
// }
48+
// }
49+
// ```
50+
let globalObject1 = getJSValue(this: .global, name: "globalObject1")
51+
let globalObject1Ref = try! XCTUnwrap(globalObject1.object)
52+
let prop_6 = getJSValue(this: globalObject1Ref, name: "prop_6")
53+
let prop_6Ref = try! XCTUnwrap(prop_6.object)
54+
55+
var isHostFunc1Called = false
56+
let hostFunc1 = JSClosure { (_) -> JSValue in
57+
isHostFunc1Called = true
58+
return .number(1)
59+
}
60+
61+
setJSValue(this: prop_6Ref, name: "host_func_1", value: .object(hostFunc1))
62+
63+
let call_host_1 = getJSValue(this: prop_6Ref, name: "call_host_1")
64+
let call_host_1Func = try! XCTUnwrap(call_host_1.function)
65+
XCTAssertEqual(call_host_1Func(), .number(1))
66+
XCTAssertEqual(isHostFunc1Called, true)
67+
68+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
69+
hostFunc1.release()
70+
#endif
71+
72+
let evalClosure = JSObject.global.globalObject1.eval_closure.function!
73+
let hostFunc2 = JSClosure { (arguments) -> JSValue in
74+
if let input = arguments[0].number {
75+
return .number(input * 2)
76+
} else {
77+
return .string(String(describing: arguments[0]))
78+
}
79+
}
80+
81+
XCTAssertEqual(evalClosure(hostFunc2, 3), .number(6))
82+
XCTAssertTrue(evalClosure(hostFunc2, true).string != nil)
83+
84+
#if JAVASCRIPTKIT_WITHOUT_WEAKREFS
85+
hostFunc2.release()
86+
#endif
87+
}
88+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy