-
-
Notifications
You must be signed in to change notification settings - Fork 51
/
Copy pathJSSending.swift
402 lines (385 loc) · 16.2 KB
/
JSSending.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
@_spi(JSObject_id) import JavaScriptKit
import _CJavaScriptKit
#if canImport(Synchronization)
import Synchronization
#endif
/// A temporary object intended to send a JavaScript object from one thread to another.
///
/// `JSSending` provides a way to safely transfer or clone JavaScript objects between threads
/// in a multi-threaded WebAssembly environment.
///
/// There are two primary ways to use `JSSending`:
/// 1. Transfer an object (`JSSending.transfer`) - The original object becomes unusable
/// 2. Clone an object (`JSSending.init`) - Creates a copy, original remains usable
///
/// To receive a sent object on the destination thread, call the `receive()` method.
///
/// - Note: `JSSending` is `Sendable` and can be safely shared across thread boundaries.
///
/// ## Example
///
/// ```swift
/// // Transfer an object to another thread
/// let buffer = JSObject.global.Uint8Array.function!.new(100).buffer.object!
/// let transferring = JSSending.transfer(buffer)
///
/// // Receive the object on a worker thread
/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
/// Task(executorPreference: executor) {
/// let receivedBuffer = try await transferring.receive()
/// // Use the received buffer
/// }
///
/// // Clone an object for use in another thread
/// let object = JSObject.global.Object.function!.new()
/// object["test"] = "Hello, World!"
/// let cloning = JSSending(object)
///
/// Task(executorPreference: executor) {
/// let receivedObject = try await cloning.receive()
/// // Use the received object
/// }
/// ```
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct JSSending<T>: @unchecked Sendable {
// HACK: We need to make this Storage "class" instead of "struct" to avoid using
// outlined value operations in parameter-packed contexts, which leads to a
// compiler crash. https://github.com/swiftlang/swift/pull/79201
fileprivate class Storage {
/// The original object that is sent.
///
/// Retain it here to prevent it from being released before the sending is complete.
let sourceObject: JSObject
/// A function that constructs an object from a JavaScript object reference.
let construct: (_ object: JSObject) -> T
/// The JavaScript object reference of the original object.
let idInSource: JavaScriptObjectRef
/// The TID of the thread that owns the original object.
let sourceTid: Int32
/// Whether the object should be "transferred" or "cloned".
let transferring: Bool
init(
sourceObject: JSObject,
construct: @escaping (_ object: JSObject) -> T,
idInSource: JavaScriptObjectRef,
sourceTid: Int32,
transferring: Bool
) {
self.sourceObject = sourceObject
self.construct = construct
self.idInSource = idInSource
self.sourceTid = sourceTid
self.transferring = transferring
}
}
private let storage: Storage
fileprivate init(
sourceObject: T,
construct: @escaping (_ object: JSObject) -> T,
deconstruct: @escaping (_ object: T) -> JSObject,
getSourceTid: @escaping (_ object: T) -> Int32,
transferring: Bool
) {
let object = deconstruct(sourceObject)
self.storage = Storage(
sourceObject: object,
construct: construct,
idInSource: object.id,
sourceTid: getSourceTid(sourceObject),
transferring: transferring
)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension JSSending where T == JSObject {
private init(_ object: JSObject, transferring: Bool) {
self.init(
sourceObject: object,
construct: { $0 },
deconstruct: { $0 },
getSourceTid: {
#if compiler(>=6.1) && _runtime(_multithreaded)
return $0.ownerTid
#else
_ = $0
// On single-threaded runtime, source and destination threads are always the main thread (TID = -1).
return -1
#endif
},
transferring: transferring
)
}
/// Transfers a `JSObject` to another thread.
///
/// The original `JSObject` is ["transferred"](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)
/// to the receiving thread, which means its ownership is completely moved. After transferring,
/// the original object becomes neutered (unusable) in the source thread.
///
/// This is more efficient than cloning for large objects like `ArrayBuffer` because no copying
/// is involved, but the original object can no longer be accessed.
///
/// Only objects that implement the JavaScript [Transferable](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)
/// interface can be transferred. Common transferable objects include:
/// - `ArrayBuffer`
/// - `MessagePort`
/// - `ImageBitmap`
/// - `OffscreenCanvas`
///
/// ## Example
///
/// ```swift
/// let buffer = JSObject.global.Uint8Array.function!.new(100).buffer.object!
/// let transferring = JSSending.transfer(buffer)
///
/// // After transfer, the original buffer is neutered
/// // buffer.byteLength.number! will be 0
/// ```
///
/// - Precondition: The thread calling this method should have the ownership of the `JSObject`.
/// - Postcondition: The original `JSObject` is no longer owned by the thread, further access to it
/// on the thread that called this method is invalid and will result in undefined behavior.
///
/// - Parameter object: The `JSObject` to be transferred.
/// - Returns: A `JSSending` instance that can be shared across threads.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public static func transfer(_ object: JSObject) -> JSSending {
JSSending(object, transferring: true)
}
/// Clones a `JSObject` to another thread.
///
/// Creates a copy of the object that can be sent to another thread. The original object
/// remains usable in the source thread. This is safer than transferring when you need
/// to continue using the original object, but has higher memory overhead since it creates
/// a complete copy.
///
/// Most JavaScript objects can be cloned, but some complex objects including closures may
/// not be clonable.
///
/// ## Example
///
/// ```swift
/// let object = JSObject.global.Object.function!.new()
/// object["test"] = "Hello, World!"
/// let cloning = JSSending(object)
///
/// // Original object is still valid and usable
/// // object["test"].string! is still "Hello, World!"
/// ```
///
/// - Precondition: The thread calling this method should have the ownership of the `JSObject`.
/// - Parameter object: The `JSObject` to be cloned.
/// - Returns: A `JSSending` instance that can be shared across threads.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public init(_ object: JSObject) {
self.init(object, transferring: false)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension JSSending {
/// Receives a sent `JSObject` from a thread.
///
/// This method completes the transfer or clone operation, making the object available
/// in the receiving thread. It must be called on the destination thread where you want
/// to use the object.
///
/// - Important: This method should be called only once for each `JSSending` instance.
/// Attempting to receive the same object multiple times will result in an error.
///
/// ## Example - Transferring
///
/// ```swift
/// let canvas = JSObject.global.document.createElement("canvas").object!
/// let transferring = JSSending.transfer(canvas.transferControlToOffscreen().object!)
///
/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
/// Task(executorPreference: executor) {
/// let canvas = try await transferring.receive()
/// // Use the canvas in the worker thread
/// }
/// ```
///
/// ## Example - Cloning
///
/// ```swift
/// let data = JSObject.global.Object.function!.new()
/// data["value"] = 42
/// let cloning = JSSending(data)
///
/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
/// Task(executorPreference: executor) {
/// let data = try await cloning.receive()
/// print(data["value"].number!) // 42
/// }
/// ```
///
/// - Parameter isolation: The actor isolation context for this call, used in Swift concurrency.
/// - Returns: The received object of type `T`.
/// - Throws: `JSSendingError` if the sending operation fails, or `JSException` if a JavaScript error occurs.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public func receive(
isolation: isolated (any Actor)? = #isolation,
file: StaticString = #file,
line: UInt = #line
) async throws -> T {
#if compiler(>=6.1) && _runtime(_multithreaded)
let idInDestination = try await withCheckedThrowingContinuation { continuation in
let context = _JSSendingContext(continuation: continuation)
let idInSource = self.storage.idInSource
let transferring = self.storage.transferring ? [idInSource] : []
swjs_request_sending_object(
idInSource,
transferring,
Int32(transferring.count),
self.storage.sourceTid,
Unmanaged.passRetained(context).toOpaque()
)
}
return storage.construct(JSObject(id: idInDestination))
#else
return storage.construct(storage.sourceObject)
#endif
}
// 6.0 and below can't compile the following without a compiler crash.
#if compiler(>=6.1)
/// Receives multiple `JSSending` instances from a thread in a single operation.
///
/// This method is more efficient than receiving multiple objects individually, as it
/// batches the receive operations. It's especially useful when transferring or cloning
/// multiple related objects that need to be received together.
///
/// - Important: All objects being received must come from the same source thread.
///
/// ## Example
///
/// ```swift
/// // Create and transfer multiple objects
/// let buffer1 = Uint8Array.new(10).buffer.object!
/// let buffer2 = Uint8Array.new(20).buffer.object!
/// let transferring1 = JSSending.transfer(buffer1)
/// let transferring2 = JSSending.transfer(buffer2)
///
/// // Receive both objects in a single operation
/// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
/// Task(executorPreference: executor) {
/// let (receivedBuffer1, receivedBuffer2) = try await JSSending.receive(transferring1, transferring2)
/// // Use both buffers in the worker thread
/// }
/// ```
///
/// - Parameters:
/// - sendings: The `JSSending` instances to receive.
/// - isolation: The actor isolation context for this call, used in Swift concurrency.
/// - Returns: A tuple containing the received objects.
/// - Throws: `JSSendingError` if any sending operation fails, or `JSException` if a JavaScript error occurs.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public static func receive<each U>(
_ sendings: repeat JSSending<each U>,
isolation: isolated (any Actor)? = #isolation,
file: StaticString = #file,
line: UInt = #line
) async throws -> (repeat each U) where T == (repeat each U) {
#if compiler(>=6.1) && _runtime(_multithreaded)
var sendingObjects: [JavaScriptObjectRef] = []
var transferringObjects: [JavaScriptObjectRef] = []
var sourceTid: Int32?
for object in repeat each sendings {
sendingObjects.append(object.storage.idInSource)
if object.storage.transferring {
transferringObjects.append(object.storage.idInSource)
}
if sourceTid == nil {
sourceTid = object.storage.sourceTid
} else {
guard sourceTid == object.storage.sourceTid else {
throw JSSendingError("All objects sent at once must be from the same thread")
}
}
}
let objects = try await withCheckedThrowingContinuation { continuation in
let context = _JSSendingContext(continuation: continuation)
sendingObjects.withUnsafeBufferPointer { sendingObjects in
transferringObjects.withUnsafeBufferPointer { transferringObjects in
swjs_request_sending_objects(
sendingObjects.baseAddress!,
Int32(sendingObjects.count),
transferringObjects.baseAddress!,
Int32(transferringObjects.count),
sourceTid!,
Unmanaged.passRetained(context).toOpaque()
)
}
}
}
guard let objectsArray = JSArray(JSObject(id: objects)) else {
fatalError("Non-array object received!?")
}
var index = 0
func extract<R>(_ sending: JSSending<R>) -> R {
let result = objectsArray[index]
index += 1
return sending.storage.construct(result.object!)
}
return (repeat extract(each sendings))
#else
return try await (repeat (each sendings).receive())
#endif
}
#endif // compiler(>=6.1)
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
private final class _JSSendingContext: Sendable {
let continuation: CheckedContinuation<JavaScriptObjectRef, Error>
init(continuation: CheckedContinuation<JavaScriptObjectRef, Error>) {
self.continuation = continuation
}
}
/// Error type representing failures during JavaScript object sending operations.
///
/// This error is thrown when a problem occurs during object transfer or cloning
/// between threads, such as attempting to send objects from different threads
/// in a batch operation or other sending-related failures.
public struct JSSendingError: Error, CustomStringConvertible {
/// A description of the error that occurred.
public let description: String
init(_ message: String) {
self.description = message
}
}
/// A function that should be called when an object source thread sends an object to a
/// destination thread.
///
/// - Parameters:
/// - object: The `JSObject` to be received.
/// - contextPtr: A pointer to the `_JSSendingContext` instance.
// swift-format-ignore
#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
@_expose(wasm, "swjs_receive_response")
@_cdecl("swjs_receive_response")
#endif
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func _swjs_receive_response(_ object: JavaScriptObjectRef, _ contextPtr: UnsafeRawPointer?) {
#if compiler(>=6.1) && _runtime(_multithreaded)
guard let contextPtr = contextPtr else { return }
let context = Unmanaged<_JSSendingContext>.fromOpaque(contextPtr).takeRetainedValue()
context.continuation.resume(returning: object)
#endif
}
/// A function that should be called when an object source thread sends an error to a
/// destination thread.
///
/// - Parameters:
/// - error: The error to be received.
/// - contextPtr: A pointer to the `_JSSendingContext` instance.
// swift-format-ignore
#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
@_expose(wasm, "swjs_receive_error")
@_cdecl("swjs_receive_error")
#endif
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
func _swjs_receive_error(_ error: JavaScriptObjectRef, _ contextPtr: UnsafeRawPointer?) {
#if compiler(>=6.1) && _runtime(_multithreaded)
guard let contextPtr = contextPtr else { return }
let context = Unmanaged<_JSSendingContext>.fromOpaque(contextPtr).takeRetainedValue()
context.continuation.resume(throwing: JSException(JSObject(id: error).jsValue))
#endif
}