Skip to content

Commit 98cec71

Browse files
Rename JSObject.receive to JSObject.Transferring.receive
1 parent 9d335a8 commit 98cec71

File tree

2 files changed

+85
-29
lines changed

2 files changed

+85
-29
lines changed

Examples/OffscrenCanvas/Sources/MyApp/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct BackgroundRenderer: CanvasRenderer {
1313
let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
1414
let transferringCanvas = JSObject.transfer(canvas)
1515
let renderingTask = Task(executorPreference: executor) {
16-
let canvas = try await JSObject.receive(transferringCanvas)
16+
let canvas = try await transferringCanvas.receive()
1717
try await renderAnimation(canvas: canvas, size: size)
1818
}
1919
await withTaskCancellationHandler {
Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,116 @@
11
@_spi(JSObject_id) import JavaScriptKit
22
import _CJavaScriptKit
33

4+
#if canImport(Synchronization)
5+
import Synchronization
6+
#endif
7+
48
extension JSObject {
5-
public class Transferring: @unchecked Sendable {
6-
fileprivate let sourceTid: Int32
7-
fileprivate let idInSource: JavaScriptObjectRef
8-
fileprivate var continuation: CheckedContinuation<JSObject, Error>? = nil
9-
10-
init(sourceTid: Int32, id: JavaScriptObjectRef) {
11-
self.sourceTid = sourceTid
12-
self.idInSource = id
9+
10+
/// A temporary object intended to transfer a ``JSObject`` from one thread to another.
11+
///
12+
/// ``JSObject`` itself is not `Sendable`, but ``Transferring`` is `Sendable` because it's
13+
/// intended to be shared across threads.
14+
public struct Transferring: @unchecked Sendable {
15+
fileprivate struct CriticalState {
16+
var continuation: CheckedContinuation<JSObject, Error>?
17+
}
18+
fileprivate class Storage {
19+
let sourceTid: Int32
20+
let idInSource: JavaScriptObjectRef
21+
#if compiler(>=6.1) && _runtime(_multithreaded)
22+
let criticalState: Mutex<CriticalState> = .init(CriticalState())
23+
#endif
24+
25+
init(sourceTid: Int32, id: JavaScriptObjectRef) {
26+
self.sourceTid = sourceTid
27+
self.idInSource = id
28+
}
29+
}
30+
31+
private let storage: Storage
32+
33+
fileprivate init(sourceTid: Int32, id: JavaScriptObjectRef) {
34+
self.init(storage: Storage(sourceTid: sourceTid, id: id))
1335
}
1436

15-
func receive(isolation: isolated (any Actor)?) async throws -> JSObject {
37+
fileprivate init(storage: Storage) {
38+
self.storage = storage
39+
}
40+
41+
/// Receives a transferred ``JSObject`` from a thread.
42+
///
43+
/// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)
44+
/// to the receiving thread.
45+
///
46+
/// Note that this method should be called only once for each ``Transferring`` instance
47+
/// on the receiving thread.
48+
///
49+
/// ### Example
50+
///
51+
/// ```swift
52+
/// let canvas = JSObject.global.document.createElement("canvas").object!
53+
/// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!)
54+
/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
55+
/// Task(executorPreference: executor) {
56+
/// let canvas = try await transferring.receive()
57+
/// }
58+
/// ```
59+
public func receive(isolation: isolated (any Actor)? = #isolation, file: StaticString = #file, line: UInt = #line) async throws -> JSObject {
1660
#if compiler(>=6.1) && _runtime(_multithreaded)
1761
swjs_request_transferring_object(
18-
idInSource,
19-
sourceTid,
20-
Unmanaged.passRetained(self).toOpaque()
62+
self.storage.idInSource,
63+
self.storage.sourceTid,
64+
Unmanaged.passRetained(self.storage).toOpaque()
2165
)
2266
return try await withCheckedThrowingContinuation { continuation in
23-
self.continuation = continuation
67+
self.storage.criticalState.withLock { criticalState in
68+
guard criticalState.continuation == nil else {
69+
// This is a programming error, `receive` should be called only once.
70+
fatalError("JSObject.Transferring object is already received", file: file, line: line)
71+
}
72+
criticalState.continuation = continuation
73+
}
2474
}
2575
#else
26-
return JSObject(id: idInSource)
76+
return JSObject(id: storage.idInSource)
2777
#endif
2878
}
2979
}
3080

31-
/// Transfers the ownership of a `JSObject` to be sent to another Worker.
81+
/// Transfers the ownership of a `JSObject` to be sent to another thread.
82+
///
83+
/// Note that the original ``JSObject`` should not be accessed after calling this method.
3284
///
33-
/// - Parameter object: The `JSObject` to be transferred.
34-
/// - Returns: A `JSTransferring` instance that can be shared across worker threads.
35-
/// - Note: The original `JSObject` should not be accessed after calling this method.
85+
/// - Parameter object: The ``JSObject`` to be transferred.
86+
/// - Returns: A ``Transferring`` instance that can be shared across threads.
3687
public static func transfer(_ object: JSObject) -> Transferring {
3788
#if compiler(>=6.1) && _runtime(_multithreaded)
3889
Transferring(sourceTid: object.ownerTid, id: object.id)
3990
#else
91+
// On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
4092
Transferring(sourceTid: -1, id: object.id)
4193
#endif
4294
}
43-
44-
/// Receives a transferred `JSObject` from a Worker.
45-
///
46-
/// - Parameter transferring: The `JSTransferring` instance received from other worker threads.
47-
/// - Returns: The reconstructed `JSObject` that can be used in the receiving Worker.
48-
public static func receive(_ transferring: Transferring, isolation: isolated (any Actor)? = #isolation) async throws -> JSObject {
49-
try await transferring.receive(isolation: isolation)
50-
}
5195
}
5296

97+
98+
/// A function that should be called when an object source thread sends an object to a
99+
/// destination thread.
100+
///
101+
/// - Parameters:
102+
/// - object: The `JSObject` to be received.
103+
/// - transferring: A pointer to the `Transferring.Storage` instance.
53104
#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
54105
@_expose(wasm, "swjs_receive_object")
55106
@_cdecl("swjs_receive_object")
56107
#endif
57108
func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) {
58-
let transferring = Unmanaged<JSObject.Transferring>.fromOpaque(transferring).takeRetainedValue()
59-
transferring.continuation?.resume(returning: JSObject(id: object))
109+
#if compiler(>=6.1) && _runtime(_multithreaded)
110+
let storage = Unmanaged<JSObject.Transferring.Storage>.fromOpaque(transferring).takeRetainedValue()
111+
storage.criticalState.withLock { criticalState in
112+
assert(criticalState.continuation != nil, "JSObject.Transferring object is not yet received!?")
113+
criticalState.continuation?.resume(returning: JSObject(id: object))
114+
}
115+
#endif
60116
}

0 commit comments

Comments
 (0)