@@ -5,95 +5,109 @@ import _CJavaScriptKit
55 import Synchronization
66#endif
77
8- extension JSObject {
8+ /// A temporary object intended to transfer an object from one thread to another.
9+ ///
10+ /// ``JSTransferring`` is `Sendable` and it's intended to be shared across threads.
11+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
12+ public struct JSTransferring < T> : @unchecked Sendable {
13+ fileprivate struct Storage {
14+ /// The original object that is transferred.
15+ ///
16+ /// Retain it here to prevent it from being released before the transfer is complete.
17+ let sourceObject : T
18+ /// A function that constructs an object from a JavaScript object reference.
19+ let construct : ( _ id: JavaScriptObjectRef ) -> T
20+ /// The JavaScript object reference of the original object.
21+ let idInSource : JavaScriptObjectRef
22+ /// The TID of the thread that owns the original object.
23+ let sourceTid : Int32
924
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- @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
15- public struct Transferring : @unchecked Sendable {
16- fileprivate struct CriticalState {
17- var continuation : CheckedContinuation < JavaScriptObjectRef , Error > ?
18- }
19- fileprivate class Storage {
20- /// The original ``JSObject`` that is transferred.
21- ///
22- /// Retain it here to prevent it from being released before the transfer is complete.
23- let sourceObject : JSObject
24- #if compiler(>=6.1) && _runtime(_multithreaded)
25- let criticalState : Mutex < CriticalState > = . init( CriticalState ( ) )
26- #endif
25+ #if compiler(>=6.1) && _runtime(_multithreaded)
26+ /// A shared context for transferring objects across threads.
27+ let context : _JSTransferringContext = _JSTransferringContext ( )
28+ #endif
29+ }
2730
28- var idInSource : JavaScriptObjectRef {
29- sourceObject. id
30- }
31+ private let storage : Storage
3132
32- var sourceTid : Int32 {
33- #if compiler(>=6.1) && _runtime(_multithreaded)
34- sourceObject. ownerTid
35- #else
36- // On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
37- - 1
38- #endif
39- }
33+ fileprivate init (
34+ sourceObject: T ,
35+ construct: @escaping ( _ id: JavaScriptObjectRef ) -> T ,
36+ deconstruct: @escaping ( _ object: T ) -> JavaScriptObjectRef ,
37+ getSourceTid: @escaping ( _ object: T ) -> Int32
38+ ) {
39+ self . storage = Storage (
40+ sourceObject: sourceObject,
41+ construct: construct,
42+ idInSource: deconstruct ( sourceObject) ,
43+ sourceTid: getSourceTid ( sourceObject)
44+ )
45+ }
4046
41- init ( sourceObject: JSObject ) {
42- self . sourceObject = sourceObject
47+ /// Receives a transferred ``JSObject`` from a thread.
48+ ///
49+ /// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)
50+ /// to the receiving thread.
51+ ///
52+ /// Note that this method should be called only once for each ``Transferring`` instance
53+ /// on the receiving thread.
54+ ///
55+ /// ### Example
56+ ///
57+ /// ```swift
58+ /// let canvas = JSObject.global.document.createElement("canvas").object!
59+ /// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!)
60+ /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
61+ /// Task(executorPreference: executor) {
62+ /// let canvas = try await transferring.receive()
63+ /// }
64+ /// ```
65+ @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
66+ public func receive( isolation: isolated ( any Actor ) ? = #isolation, file: StaticString = #file, line: UInt = #line) async throws -> T {
67+ #if compiler(>=6.1) && _runtime(_multithreaded)
68+ // The following sequence of events happens when a `JSObject` is transferred from
69+ // the owner thread to the receiver thread:
70+ //
71+ // [Owner Thread] [Receiver Thread]
72+ // <-----requestTransfer------ swjs_request_transferring_object
73+ // ---------transfer---------> swjs_receive_object
74+ let idInDestination = try await withCheckedThrowingContinuation { continuation in
75+ self . storage. context. withLock { context in
76+ guard context. continuation == nil else {
77+ // This is a programming error, `receive` should be called only once.
78+ fatalError ( " JSObject.Transferring object is already received " , file: file, line: line)
79+ }
80+ // The continuation will be resumed by `swjs_receive_object`.
81+ context. continuation = continuation
4382 }
44- }
45-
46- private let storage : Storage
47-
48- fileprivate init ( sourceObject: JSObject ) {
49- self . init ( storage: Storage ( sourceObject: sourceObject) )
50- }
51-
52- fileprivate init ( storage: Storage ) {
53- self . storage = storage
54- }
55-
56- /// Receives a transferred ``JSObject`` from a thread.
57- ///
58- /// The original ``JSObject`` is ["Transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)
59- /// to the receiving thread.
60- ///
61- /// Note that this method should be called only once for each ``Transferring`` instance
62- /// on the receiving thread.
63- ///
64- /// ### Example
65- ///
66- /// ```swift
67- /// let canvas = JSObject.global.document.createElement("canvas").object!
68- /// let transferring = JSObject.transfer(canvas.transferControlToOffscreen().object!)
69- /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
70- /// Task(executorPreference: executor) {
71- /// let canvas = try await transferring.receive()
72- /// }
73- /// ```
74- @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
75- public func receive( isolation: isolated ( any Actor ) ? = #isolation, file: StaticString = #file, line: UInt = #line) async throws -> JSObject {
76- #if compiler(>=6.1) && _runtime(_multithreaded)
7783 swjs_request_transferring_object (
7884 self . storage. idInSource,
7985 self . storage. sourceTid,
80- Unmanaged . passRetained ( self . storage) . toOpaque ( )
86+ Unmanaged . passRetained ( self . storage. context ) . toOpaque ( )
8187 )
82- let idInDestination = try await withCheckedThrowingContinuation { continuation in
83- self . storage. criticalState. withLock { criticalState in
84- guard criticalState. continuation == nil else {
85- // This is a programming error, `receive` should be called only once.
86- fatalError ( " JSObject.Transferring object is already received " , file: file, line: line)
87- }
88- criticalState. continuation = continuation
89- }
90- }
91- return JSObject ( id: idInDestination)
92- #else
93- return JSObject ( id: storage. idInSource)
94- #endif
9588 }
89+ return storage. construct ( idInDestination)
90+ #else
91+ return storage. construct ( storage. idInSource)
92+ #endif
93+ }
94+ }
95+
96+ fileprivate final class _JSTransferringContext : Sendable {
97+ struct State {
98+ var continuation : CheckedContinuation < JavaScriptObjectRef , Error > ?
9699 }
100+ private let state : Mutex < State > = . init( State ( ) )
101+
102+ func withLock< R> ( _ body: ( inout State ) -> R ) -> R {
103+ return state. withLock { state in
104+ body ( & state)
105+ }
106+ }
107+ }
108+
109+
110+ extension JSTransferring where T == JSObject {
97111
98112 /// Transfers the ownership of a `JSObject` to be sent to another thread.
99113 ///
@@ -104,8 +118,21 @@ extension JSObject {
104118 /// - Parameter object: The ``JSObject`` to be transferred.
105119 /// - Returns: A ``Transferring`` instance that can be shared across threads.
106120 @available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
107- public static func transfer( _ object: JSObject ) -> Transferring {
108- return Transferring ( sourceObject: object)
121+ public init ( _ object: JSObject ) {
122+ self . init (
123+ sourceObject: object,
124+ construct: { JSObject ( id: $0) } ,
125+ deconstruct: { $0. id } ,
126+ getSourceTid: {
127+ #if compiler(>=6.1) && _runtime(_multithreaded)
128+ return $0. ownerTid
129+ #else
130+ _ = $0
131+ // On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
132+ return - 1
133+ #endif
134+ }
135+ )
109136 }
110137}
111138
@@ -123,10 +150,10 @@ extension JSObject {
123150@available ( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * )
124151func _swjs_receive_object( _ object: JavaScriptObjectRef , _ transferring: UnsafeRawPointer ) {
125152 #if compiler(>=6.1) && _runtime(_multithreaded)
126- let storage = Unmanaged < JSObject . Transferring . Storage > . fromOpaque ( transferring) . takeRetainedValue ( )
127- storage . criticalState . withLock { criticalState in
128- assert ( criticalState . continuation != nil , " JSObject.Transferring object is not yet received!? " )
129- criticalState . continuation? . resume ( returning: object)
153+ let context = Unmanaged < _JSTransferringContext > . fromOpaque ( transferring) . takeRetainedValue ( )
154+ context . withLock { state in
155+ assert ( state . continuation != nil , " JSObject.Transferring object is not yet received!? " )
156+ state . continuation? . resume ( returning: object)
130157 }
131158 #endif
132159}
0 commit comments