|
1 | 1 | @_spi(JSObject_id) import JavaScriptKit
|
2 | 2 | import _CJavaScriptKit
|
3 | 3 |
|
| 4 | +#if canImport(Synchronization) |
| 5 | + import Synchronization |
| 6 | +#endif |
| 7 | + |
4 | 8 | 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)) |
13 | 35 | }
|
14 | 36 |
|
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 { |
16 | 60 | #if compiler(>=6.1) && _runtime(_multithreaded)
|
17 | 61 | 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() |
21 | 65 | )
|
22 | 66 | 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 | + } |
24 | 74 | }
|
25 | 75 | #else
|
26 |
| - return JSObject(id: idInSource) |
| 76 | + return JSObject(id: storage.idInSource) |
27 | 77 | #endif
|
28 | 78 | }
|
29 | 79 | }
|
30 | 80 |
|
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. |
32 | 84 | ///
|
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. |
36 | 87 | public static func transfer(_ object: JSObject) -> Transferring {
|
37 | 88 | #if compiler(>=6.1) && _runtime(_multithreaded)
|
38 | 89 | Transferring(sourceTid: object.ownerTid, id: object.id)
|
39 | 90 | #else
|
| 91 | + // On single-threaded runtime, source and destination threads are always the main thread (TID = -1). |
40 | 92 | Transferring(sourceTid: -1, id: object.id)
|
41 | 93 | #endif
|
42 | 94 | }
|
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 |
| - } |
51 | 95 | }
|
52 | 96 |
|
| 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. |
53 | 104 | #if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
|
54 | 105 | @_expose(wasm, "swjs_receive_object")
|
55 | 106 | @_cdecl("swjs_receive_object")
|
56 | 107 | #endif
|
57 | 108 | 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 |
60 | 116 | }
|
0 commit comments