Skip to content

Commit 28d5ec0

Browse files
Add JSObject.transfer and JSObject.receive APIs
These APIs allow transferring a `JSObject` between worker threads. The `JSObject.transfer` method creates a `JSObject.Transferring` instance that is `Sendable` and can be sent to another worker thread. The `JSObject.receive` method requests the object from the source worker thread and postMessage it to the destination worker thread.
1 parent a732a0c commit 28d5ec0

File tree

8 files changed

+436
-34
lines changed

8 files changed

+436
-34
lines changed

Diff for: Runtime/src/index.ts

+137-10
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,45 @@ import {
66
pointer,
77
TypedArray,
88
ImportedFunctions,
9+
MAIN_THREAD_TID,
910
} from "./types.js";
1011
import * as JSValue from "./js-value.js";
1112
import { Memory } from "./memory.js";
1213

14+
type TransferMessage = {
15+
type: "transfer";
16+
data: {
17+
object: any;
18+
transferring: pointer;
19+
destinationTid: number;
20+
};
21+
};
22+
23+
type RequestTransferMessage = {
24+
type: "requestTransfer";
25+
data: {
26+
objectRef: ref;
27+
objectSourceTid: number;
28+
transferring: pointer;
29+
destinationTid: number;
30+
};
31+
};
32+
33+
type TransferErrorMessage = {
34+
type: "transferError";
35+
data: {
36+
error: string;
37+
};
38+
};
39+
1340
type MainToWorkerMessage = {
1441
type: "wake";
15-
};
42+
} | RequestTransferMessage | TransferMessage | TransferErrorMessage;
1643

1744
type WorkerToMainMessage = {
1845
type: "job";
1946
data: number;
20-
};
47+
} | RequestTransferMessage | TransferMessage | TransferErrorMessage;
2148

2249
/**
2350
* A thread channel is a set of functions that are used to communicate between
@@ -60,8 +87,9 @@ export type SwiftRuntimeThreadChannel =
6087
* This function is used to send messages from the worker thread to the main thread.
6188
* The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`.
6289
* @param message The message to be sent to the main thread.
90+
* @param transfer The array of objects to be transferred to the main thread.
6391
*/
64-
postMessageToMainThread: (message: WorkerToMainMessage) => void;
92+
postMessageToMainThread: (message: WorkerToMainMessage, transfer: any[]) => void;
6593
/**
6694
* This function is expected to be set in the worker thread and should listen
6795
* to messages from the main thread sent by `postMessageToWorkerThread`.
@@ -75,8 +103,9 @@ export type SwiftRuntimeThreadChannel =
75103
* The message submitted by this function is expected to be listened by `listenMessageFromMainThread`.
76104
* @param tid The thread ID of the worker thread.
77105
* @param message The message to be sent to the worker thread.
106+
* @param transfer The array of objects to be transferred to the worker thread.
78107
*/
79-
postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage) => void;
108+
postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage, transfer: any[]) => void;
80109
/**
81110
* This function is expected to be set in the main thread and should listen
82111
* to messages sent by `postMessageToMainThread` from the worker thread.
@@ -610,8 +639,37 @@ export class SwiftRuntime {
610639
case "wake":
611640
this.exports.swjs_wake_worker_thread();
612641
break;
642+
case "requestTransfer": {
643+
const object = this.memory.getObject(message.data.objectRef);
644+
const messageToMainThread: TransferMessage = {
645+
type: "transfer",
646+
data: {
647+
object,
648+
destinationTid: message.data.destinationTid,
649+
transferring: message.data.transferring,
650+
},
651+
};
652+
try {
653+
this.postMessageToMainThread(messageToMainThread, [object]);
654+
} catch (error) {
655+
this.postMessageToMainThread({
656+
type: "transferError",
657+
data: { error: String(error) },
658+
});
659+
}
660+
break;
661+
}
662+
case "transfer": {
663+
const objectRef = this.memory.retain(message.data.object);
664+
this.exports.swjs_receive_object(objectRef, message.data.transferring);
665+
break;
666+
}
667+
case "transferError": {
668+
console.error(message.data.error); // TODO: Handle the error
669+
break;
670+
}
613671
default:
614-
const unknownMessage: never = message.type;
672+
const unknownMessage: never = message;
615673
throw new Error(`Unknown message type: ${unknownMessage}`);
616674
}
617675
});
@@ -632,8 +690,57 @@ export class SwiftRuntime {
632690
case "job":
633691
this.exports.swjs_enqueue_main_job_from_worker(message.data);
634692
break;
693+
case "requestTransfer": {
694+
if (message.data.objectSourceTid == MAIN_THREAD_TID) {
695+
const object = this.memory.getObject(message.data.objectRef);
696+
if (message.data.destinationTid != tid) {
697+
throw new Error("Invariant violation: The destination tid of the transfer request must be the same as the tid of the worker thread that received the request.");
698+
}
699+
this.postMessageToWorkerThread(message.data.destinationTid, {
700+
type: "transfer",
701+
data: {
702+
object,
703+
transferring: message.data.transferring,
704+
destinationTid: message.data.destinationTid,
705+
},
706+
}, [object]);
707+
} else {
708+
// Proxy the transfer request to the worker thread that owns the object
709+
this.postMessageToWorkerThread(message.data.objectSourceTid, {
710+
type: "requestTransfer",
711+
data: {
712+
objectRef: message.data.objectRef,
713+
objectSourceTid: tid,
714+
transferring: message.data.transferring,
715+
destinationTid: message.data.destinationTid,
716+
},
717+
});
718+
}
719+
break;
720+
}
721+
case "transfer": {
722+
if (message.data.destinationTid == MAIN_THREAD_TID) {
723+
const objectRef = this.memory.retain(message.data.object);
724+
this.exports.swjs_receive_object(objectRef, message.data.transferring);
725+
} else {
726+
// Proxy the transfer response to the destination worker thread
727+
this.postMessageToWorkerThread(message.data.destinationTid, {
728+
type: "transfer",
729+
data: {
730+
object: message.data.object,
731+
transferring: message.data.transferring,
732+
destinationTid: message.data.destinationTid,
733+
},
734+
}, [message.data.object]);
735+
}
736+
break;
737+
}
738+
case "transferError": {
739+
console.error(message.data.error); // TODO: Handle the error
740+
break;
741+
}
635742
default:
636-
const unknownMessage: never = message.type;
743+
const unknownMessage: never = message;
637744
throw new Error(`Unknown message type: ${unknownMessage}`);
638745
}
639746
},
@@ -649,27 +756,47 @@ export class SwiftRuntime {
649756
// Main thread's tid is always -1
650757
return this.tid || -1;
651758
},
759+
swjs_request_transferring_object: (
760+
object_ref: ref,
761+
object_source_tid: number,
762+
transferring: pointer,
763+
) => {
764+
if (this.tid == object_source_tid) {
765+
// Fast path: The object is already in the same thread
766+
this.exports.swjs_receive_object(object_ref, transferring);
767+
return;
768+
}
769+
this.postMessageToMainThread({
770+
type: "requestTransfer",
771+
data: {
772+
objectRef: object_ref,
773+
objectSourceTid: object_source_tid,
774+
transferring,
775+
destinationTid: this.tid ?? MAIN_THREAD_TID,
776+
},
777+
});
778+
},
652779
};
653780
}
654781

655-
private postMessageToMainThread(message: WorkerToMainMessage) {
782+
private postMessageToMainThread(message: WorkerToMainMessage, transfer: any[] = []) {
656783
const threadChannel = this.options.threadChannel;
657784
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
658785
throw new Error(
659786
"postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread."
660787
);
661788
}
662-
threadChannel.postMessageToMainThread(message);
789+
threadChannel.postMessageToMainThread(message, transfer);
663790
}
664791

665-
private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage) {
792+
private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage, transfer: any[] = []) {
666793
const threadChannel = this.options.threadChannel;
667794
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
668795
throw new Error(
669796
"postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads."
670797
);
671798
}
672-
threadChannel.postMessageToWorkerThread(tid, message);
799+
threadChannel.postMessageToWorkerThread(tid, message, transfer);
673800
}
674801
}
675802

Diff for: Runtime/src/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ExportedFunctions {
2222

2323
swjs_enqueue_main_job_from_worker(unowned_job: number): void;
2424
swjs_wake_worker_thread(): void;
25+
swjs_receive_object(object: ref, transferring: pointer): void;
2526
}
2627

2728
export interface ImportedFunctions {
@@ -112,6 +113,11 @@ export interface ImportedFunctions {
112113
swjs_listen_message_from_worker_thread: (tid: number) => void;
113114
swjs_terminate_worker_thread: (tid: number) => void;
114115
swjs_get_worker_thread_id: () => number;
116+
swjs_request_transferring_object: (
117+
object_ref: ref,
118+
object_source_tid: number,
119+
transferring: pointer,
120+
) => void;
115121
}
116122

117123
export const enum LibraryFeatures {
@@ -133,3 +139,5 @@ export type TypedArray =
133139
export function assertNever(x: never, message: string) {
134140
throw new Error(message);
135141
}
142+
143+
export const MAIN_THREAD_TID = -1;
+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
@_spi(JSObject_id) import JavaScriptKit
2+
import _CJavaScriptKit
3+
4+
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
13+
}
14+
15+
func receive(isolation: isolated (any Actor)?) async throws -> JSObject {
16+
#if compiler(>=6.1) && _runtime(_multithreaded)
17+
swjs_request_transferring_object(
18+
idInSource,
19+
sourceTid,
20+
Unmanaged.passRetained(self).toOpaque()
21+
)
22+
return try await withCheckedThrowingContinuation { continuation in
23+
self.continuation = continuation
24+
}
25+
#else
26+
return JSObject(id: idInSource)
27+
#endif
28+
}
29+
}
30+
31+
/// Transfers the ownership of a `JSObject` to be sent to another Worker.
32+
///
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.
36+
public static func transfer(_ object: JSObject) -> Transferring {
37+
#if compiler(>=6.1) && _runtime(_multithreaded)
38+
Transferring(sourceTid: object.ownerTid, id: object.id)
39+
#else
40+
Transferring(sourceTid: -1, id: object.id)
41+
#endif
42+
}
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+
}
52+
53+
#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
54+
@_expose(wasm, "swjs_receive_object")
55+
@_cdecl("swjs_receive_object")
56+
#endif
57+
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))
60+
}

Diff for: Sources/JavaScriptKit/FundamentalObjects/JSObject.swift

+4-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import _CJavaScriptKit
22

3-
#if arch(wasm32)
4-
#if canImport(wasi_pthread)
5-
import wasi_pthread
6-
#endif
7-
#else
8-
import Foundation // for pthread_t on non-wasi platforms
9-
#endif
10-
113
/// `JSObject` represents an object in JavaScript and supports dynamic member lookup.
124
/// Any member access like `object.foo` will dynamically request the JavaScript and Swift
135
/// runtime bridge library for a member with the specified name in this object.
@@ -31,14 +23,14 @@ public class JSObject: Equatable {
3123
public var id: JavaScriptObjectRef
3224

3325
#if compiler(>=6.1) && _runtime(_multithreaded)
34-
private let ownerThread: pthread_t
26+
package let ownerTid: Int32
3527
#endif
3628

3729
@_spi(JSObject_id)
3830
public init(id: JavaScriptObjectRef) {
3931
self.id = id
4032
#if compiler(>=6.1) && _runtime(_multithreaded)
41-
self.ownerThread = pthread_self()
33+
self.ownerTid = swjs_get_worker_thread_id_cached()
4234
#endif
4335
}
4436

@@ -51,14 +43,14 @@ public class JSObject: Equatable {
5143
/// object spaces are not shared across threads backed by Web Workers.
5244
private func assertOnOwnerThread(hint: @autoclosure () -> String) {
5345
#if compiler(>=6.1) && _runtime(_multithreaded)
54-
precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
46+
precondition(ownerTid == swjs_get_worker_thread_id_cached(), "JSObject is being accessed from a thread other than the owner thread: \(hint())")
5547
#endif
5648
}
5749

5850
/// Asserts that the two objects being compared are owned by the same thread.
5951
private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) {
6052
#if compiler(>=6.1) && _runtime(_multithreaded)
61-
precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
53+
precondition(lhs.ownerTid == rhs.ownerTid, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
6254
#endif
6355
}
6456

0 commit comments

Comments
 (0)