From 28d5ec060749d2ed386b554e282977a4ecee9a4a Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:21:50 +0000
Subject: [PATCH 01/19] 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.
---
Runtime/src/index.ts | 147 ++++++++++++++++--
Runtime/src/types.ts | 8 +
.../JSObject+Transferring.swift | 60 +++++++
.../FundamentalObjects/JSObject.swift | 16 +-
Sources/JavaScriptKit/Runtime/index.js | 111 ++++++++++++-
Sources/JavaScriptKit/Runtime/index.mjs | 111 ++++++++++++-
Sources/_CJavaScriptKit/_CJavaScriptKit.c | 8 +
.../_CJavaScriptKit/include/_CJavaScriptKit.h | 9 ++
8 files changed, 436 insertions(+), 34 deletions(-)
create mode 100644 Sources/JavaScriptEventLoop/JSObject+Transferring.swift
diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts
index 73f56411a..25d6e92f5 100644
--- a/Runtime/src/index.ts
+++ b/Runtime/src/index.ts
@@ -6,18 +6,45 @@ import {
pointer,
TypedArray,
ImportedFunctions,
+ MAIN_THREAD_TID,
} from "./types.js";
import * as JSValue from "./js-value.js";
import { Memory } from "./memory.js";
+type TransferMessage = {
+ type: "transfer";
+ data: {
+ object: any;
+ transferring: pointer;
+ destinationTid: number;
+ };
+};
+
+type RequestTransferMessage = {
+ type: "requestTransfer";
+ data: {
+ objectRef: ref;
+ objectSourceTid: number;
+ transferring: pointer;
+ destinationTid: number;
+ };
+};
+
+type TransferErrorMessage = {
+ type: "transferError";
+ data: {
+ error: string;
+ };
+};
+
type MainToWorkerMessage = {
type: "wake";
-};
+} | RequestTransferMessage | TransferMessage | TransferErrorMessage;
type WorkerToMainMessage = {
type: "job";
data: number;
-};
+} | RequestTransferMessage | TransferMessage | TransferErrorMessage;
/**
* A thread channel is a set of functions that are used to communicate between
@@ -60,8 +87,9 @@ export type SwiftRuntimeThreadChannel =
* This function is used to send messages from the worker thread to the main thread.
* The message submitted by this function is expected to be listened by `listenMessageFromWorkerThread`.
* @param message The message to be sent to the main thread.
+ * @param transfer The array of objects to be transferred to the main thread.
*/
- postMessageToMainThread: (message: WorkerToMainMessage) => void;
+ postMessageToMainThread: (message: WorkerToMainMessage, transfer: any[]) => void;
/**
* This function is expected to be set in the worker thread and should listen
* to messages from the main thread sent by `postMessageToWorkerThread`.
@@ -75,8 +103,9 @@ export type SwiftRuntimeThreadChannel =
* The message submitted by this function is expected to be listened by `listenMessageFromMainThread`.
* @param tid The thread ID of the worker thread.
* @param message The message to be sent to the worker thread.
+ * @param transfer The array of objects to be transferred to the worker thread.
*/
- postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage) => void;
+ postMessageToWorkerThread: (tid: number, message: MainToWorkerMessage, transfer: any[]) => void;
/**
* This function is expected to be set in the main thread and should listen
* to messages sent by `postMessageToMainThread` from the worker thread.
@@ -610,8 +639,37 @@ export class SwiftRuntime {
case "wake":
this.exports.swjs_wake_worker_thread();
break;
+ case "requestTransfer": {
+ const object = this.memory.getObject(message.data.objectRef);
+ const messageToMainThread: TransferMessage = {
+ type: "transfer",
+ data: {
+ object,
+ destinationTid: message.data.destinationTid,
+ transferring: message.data.transferring,
+ },
+ };
+ try {
+ this.postMessageToMainThread(messageToMainThread, [object]);
+ } catch (error) {
+ this.postMessageToMainThread({
+ type: "transferError",
+ data: { error: String(error) },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage: never = message.type;
+ const unknownMessage: never = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -632,8 +690,57 @@ export class SwiftRuntime {
case "job":
this.exports.swjs_enqueue_main_job_from_worker(message.data);
break;
+ case "requestTransfer": {
+ if (message.data.objectSourceTid == MAIN_THREAD_TID) {
+ const object = this.memory.getObject(message.data.objectRef);
+ if (message.data.destinationTid != tid) {
+ 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.");
+ }
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [object]);
+ } else {
+ // Proxy the transfer request to the worker thread that owns the object
+ this.postMessageToWorkerThread(message.data.objectSourceTid, {
+ type: "requestTransfer",
+ data: {
+ objectRef: message.data.objectRef,
+ objectSourceTid: tid,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ if (message.data.destinationTid == MAIN_THREAD_TID) {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ } else {
+ // Proxy the transfer response to the destination worker thread
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object: message.data.object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [message.data.object]);
+ }
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage: never = message.type;
+ const unknownMessage: never = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
},
@@ -649,27 +756,47 @@ export class SwiftRuntime {
// Main thread's tid is always -1
return this.tid || -1;
},
+ swjs_request_transferring_object: (
+ object_ref: ref,
+ object_source_tid: number,
+ transferring: pointer,
+ ) => {
+ if (this.tid == object_source_tid) {
+ // Fast path: The object is already in the same thread
+ this.exports.swjs_receive_object(object_ref, transferring);
+ return;
+ }
+ this.postMessageToMainThread({
+ type: "requestTransfer",
+ data: {
+ objectRef: object_ref,
+ objectSourceTid: object_source_tid,
+ transferring,
+ destinationTid: this.tid ?? MAIN_THREAD_TID,
+ },
+ });
+ },
};
}
- private postMessageToMainThread(message: WorkerToMainMessage) {
+ private postMessageToMainThread(message: WorkerToMainMessage, transfer: any[] = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
throw new Error(
"postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread."
);
}
- threadChannel.postMessageToMainThread(message);
+ threadChannel.postMessageToMainThread(message, transfer);
}
- private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage) {
+ private postMessageToWorkerThread(tid: number, message: MainToWorkerMessage, transfer: any[] = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
throw new Error(
"postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads."
);
}
- threadChannel.postMessageToWorkerThread(tid, message);
+ threadChannel.postMessageToWorkerThread(tid, message, transfer);
}
}
diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts
index dd638acc5..4e311ef80 100644
--- a/Runtime/src/types.ts
+++ b/Runtime/src/types.ts
@@ -22,6 +22,7 @@ export interface ExportedFunctions {
swjs_enqueue_main_job_from_worker(unowned_job: number): void;
swjs_wake_worker_thread(): void;
+ swjs_receive_object(object: ref, transferring: pointer): void;
}
export interface ImportedFunctions {
@@ -112,6 +113,11 @@ export interface ImportedFunctions {
swjs_listen_message_from_worker_thread: (tid: number) => void;
swjs_terminate_worker_thread: (tid: number) => void;
swjs_get_worker_thread_id: () => number;
+ swjs_request_transferring_object: (
+ object_ref: ref,
+ object_source_tid: number,
+ transferring: pointer,
+ ) => void;
}
export const enum LibraryFeatures {
@@ -133,3 +139,5 @@ export type TypedArray =
export function assertNever(x: never, message: string) {
throw new Error(message);
}
+
+export const MAIN_THREAD_TID = -1;
diff --git a/Sources/JavaScriptEventLoop/JSObject+Transferring.swift b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift
new file mode 100644
index 000000000..dce32d7ec
--- /dev/null
+++ b/Sources/JavaScriptEventLoop/JSObject+Transferring.swift
@@ -0,0 +1,60 @@
+@_spi(JSObject_id) import JavaScriptKit
+import _CJavaScriptKit
+
+extension JSObject {
+ public class Transferring: @unchecked Sendable {
+ fileprivate let sourceTid: Int32
+ fileprivate let idInSource: JavaScriptObjectRef
+ fileprivate var continuation: CheckedContinuation? = nil
+
+ init(sourceTid: Int32, id: JavaScriptObjectRef) {
+ self.sourceTid = sourceTid
+ self.idInSource = id
+ }
+
+ func receive(isolation: isolated (any Actor)?) async throws -> JSObject {
+ #if compiler(>=6.1) && _runtime(_multithreaded)
+ swjs_request_transferring_object(
+ idInSource,
+ sourceTid,
+ Unmanaged.passRetained(self).toOpaque()
+ )
+ return try await withCheckedThrowingContinuation { continuation in
+ self.continuation = continuation
+ }
+ #else
+ return JSObject(id: idInSource)
+ #endif
+ }
+ }
+
+ /// Transfers the ownership of a `JSObject` to be sent to another Worker.
+ ///
+ /// - Parameter object: The `JSObject` to be transferred.
+ /// - Returns: A `JSTransferring` instance that can be shared across worker threads.
+ /// - Note: The original `JSObject` should not be accessed after calling this method.
+ public static func transfer(_ object: JSObject) -> Transferring {
+ #if compiler(>=6.1) && _runtime(_multithreaded)
+ Transferring(sourceTid: object.ownerTid, id: object.id)
+ #else
+ Transferring(sourceTid: -1, id: object.id)
+ #endif
+ }
+
+ /// Receives a transferred `JSObject` from a Worker.
+ ///
+ /// - Parameter transferring: The `JSTransferring` instance received from other worker threads.
+ /// - Returns: The reconstructed `JSObject` that can be used in the receiving Worker.
+ public static func receive(_ transferring: Transferring, isolation: isolated (any Actor)? = #isolation) async throws -> JSObject {
+ try await transferring.receive(isolation: isolation)
+ }
+}
+
+#if compiler(>=6.1) // @_expose and @_extern are only available in Swift 6.1+
+@_expose(wasm, "swjs_receive_object")
+@_cdecl("swjs_receive_object")
+#endif
+func _swjs_receive_object(_ object: JavaScriptObjectRef, _ transferring: UnsafeRawPointer) {
+ let transferring = Unmanaged.fromOpaque(transferring).takeRetainedValue()
+ transferring.continuation?.resume(returning: JSObject(id: object))
+}
diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
index f74b337d8..18c683682 100644
--- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
+++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
@@ -1,13 +1,5 @@
import _CJavaScriptKit
-#if arch(wasm32)
- #if canImport(wasi_pthread)
- import wasi_pthread
- #endif
-#else
- import Foundation // for pthread_t on non-wasi platforms
-#endif
-
/// `JSObject` represents an object in JavaScript and supports dynamic member lookup.
/// Any member access like `object.foo` will dynamically request the JavaScript and Swift
/// runtime bridge library for a member with the specified name in this object.
@@ -31,14 +23,14 @@ public class JSObject: Equatable {
public var id: JavaScriptObjectRef
#if compiler(>=6.1) && _runtime(_multithreaded)
- private let ownerThread: pthread_t
+ package let ownerTid: Int32
#endif
@_spi(JSObject_id)
public init(id: JavaScriptObjectRef) {
self.id = id
#if compiler(>=6.1) && _runtime(_multithreaded)
- self.ownerThread = pthread_self()
+ self.ownerTid = swjs_get_worker_thread_id_cached()
#endif
}
@@ -51,14 +43,14 @@ public class JSObject: Equatable {
/// object spaces are not shared across threads backed by Web Workers.
private func assertOnOwnerThread(hint: @autoclosure () -> String) {
#if compiler(>=6.1) && _runtime(_multithreaded)
- precondition(pthread_equal(ownerThread, pthread_self()) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
+ precondition(ownerTid == swjs_get_worker_thread_id_cached(), "JSObject is being accessed from a thread other than the owner thread: \(hint())")
#endif
}
/// Asserts that the two objects being compared are owned by the same thread.
private static func assertSameOwnerThread(lhs: JSObject, rhs: JSObject, hint: @autoclosure () -> String) {
#if compiler(>=6.1) && _runtime(_multithreaded)
- precondition(pthread_equal(lhs.ownerThread, rhs.ownerThread) != 0, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
+ precondition(lhs.ownerTid == rhs.ownerTid, "JSObject is being accessed from a thread other than the owner thread: \(hint())")
#endif
}
diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js
index 223fed3e1..8027593e5 100644
--- a/Sources/JavaScriptKit/Runtime/index.js
+++ b/Sources/JavaScriptKit/Runtime/index.js
@@ -25,6 +25,7 @@
function assertNever(x, message) {
throw new Error(message);
}
+ const MAIN_THREAD_TID = -1;
const decode = (kind, payload1, payload2, memory) => {
switch (kind) {
@@ -512,8 +513,38 @@
case "wake":
this.exports.swjs_wake_worker_thread();
break;
+ case "requestTransfer": {
+ const object = this.memory.getObject(message.data.objectRef);
+ const messageToMainThread = {
+ type: "transfer",
+ data: {
+ object,
+ destinationTid: message.data.destinationTid,
+ transferring: message.data.transferring,
+ },
+ };
+ try {
+ this.postMessageToMainThread(messageToMainThread, [object]);
+ }
+ catch (error) {
+ this.postMessageToMainThread({
+ type: "transferError",
+ data: { error: String(error) },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -531,8 +562,59 @@
case "job":
this.exports.swjs_enqueue_main_job_from_worker(message.data);
break;
+ case "requestTransfer": {
+ if (message.data.objectSourceTid == MAIN_THREAD_TID) {
+ const object = this.memory.getObject(message.data.objectRef);
+ if (message.data.destinationTid != tid) {
+ 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.");
+ }
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [object]);
+ }
+ else {
+ // Proxy the transfer request to the worker thread that owns the object
+ this.postMessageToWorkerThread(message.data.objectSourceTid, {
+ type: "requestTransfer",
+ data: {
+ objectRef: message.data.objectRef,
+ objectSourceTid: tid,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ if (message.data.destinationTid == MAIN_THREAD_TID) {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ }
+ else {
+ // Proxy the transfer response to the destination worker thread
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object: message.data.object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [message.data.object]);
+ }
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -548,21 +630,38 @@
// Main thread's tid is always -1
return this.tid || -1;
},
+ swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => {
+ var _a;
+ if (this.tid == object_source_tid) {
+ // Fast path: The object is already in the same thread
+ this.exports.swjs_receive_object(object_ref, transferring);
+ return;
+ }
+ this.postMessageToMainThread({
+ type: "requestTransfer",
+ data: {
+ objectRef: object_ref,
+ objectSourceTid: object_source_tid,
+ transferring,
+ destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+ },
+ });
+ },
};
}
- postMessageToMainThread(message) {
+ postMessageToMainThread(message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
}
- threadChannel.postMessageToMainThread(message);
+ threadChannel.postMessageToMainThread(message, transfer);
}
- postMessageToWorkerThread(tid, message) {
+ postMessageToWorkerThread(tid, message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
}
- threadChannel.postMessageToWorkerThread(tid, message);
+ threadChannel.postMessageToWorkerThread(tid, message, transfer);
}
}
/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs
index 34e4dd13f..6a3df7477 100644
--- a/Sources/JavaScriptKit/Runtime/index.mjs
+++ b/Sources/JavaScriptKit/Runtime/index.mjs
@@ -19,6 +19,7 @@ class SwiftClosureDeallocator {
function assertNever(x, message) {
throw new Error(message);
}
+const MAIN_THREAD_TID = -1;
const decode = (kind, payload1, payload2, memory) => {
switch (kind) {
@@ -506,8 +507,38 @@ class SwiftRuntime {
case "wake":
this.exports.swjs_wake_worker_thread();
break;
+ case "requestTransfer": {
+ const object = this.memory.getObject(message.data.objectRef);
+ const messageToMainThread = {
+ type: "transfer",
+ data: {
+ object,
+ destinationTid: message.data.destinationTid,
+ transferring: message.data.transferring,
+ },
+ };
+ try {
+ this.postMessageToMainThread(messageToMainThread, [object]);
+ }
+ catch (error) {
+ this.postMessageToMainThread({
+ type: "transferError",
+ data: { error: String(error) },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -525,8 +556,59 @@ class SwiftRuntime {
case "job":
this.exports.swjs_enqueue_main_job_from_worker(message.data);
break;
+ case "requestTransfer": {
+ if (message.data.objectSourceTid == MAIN_THREAD_TID) {
+ const object = this.memory.getObject(message.data.objectRef);
+ if (message.data.destinationTid != tid) {
+ 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.");
+ }
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [object]);
+ }
+ else {
+ // Proxy the transfer request to the worker thread that owns the object
+ this.postMessageToWorkerThread(message.data.objectSourceTid, {
+ type: "requestTransfer",
+ data: {
+ objectRef: message.data.objectRef,
+ objectSourceTid: tid,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ });
+ }
+ break;
+ }
+ case "transfer": {
+ if (message.data.destinationTid == MAIN_THREAD_TID) {
+ const objectRef = this.memory.retain(message.data.object);
+ this.exports.swjs_receive_object(objectRef, message.data.transferring);
+ }
+ else {
+ // Proxy the transfer response to the destination worker thread
+ this.postMessageToWorkerThread(message.data.destinationTid, {
+ type: "transfer",
+ data: {
+ object: message.data.object,
+ transferring: message.data.transferring,
+ destinationTid: message.data.destinationTid,
+ },
+ }, [message.data.object]);
+ }
+ break;
+ }
+ case "transferError": {
+ console.error(message.data.error); // TODO: Handle the error
+ break;
+ }
default:
- const unknownMessage = message.type;
+ const unknownMessage = message;
throw new Error(`Unknown message type: ${unknownMessage}`);
}
});
@@ -542,21 +624,38 @@ class SwiftRuntime {
// Main thread's tid is always -1
return this.tid || -1;
},
+ swjs_request_transferring_object: (object_ref, object_source_tid, transferring) => {
+ var _a;
+ if (this.tid == object_source_tid) {
+ // Fast path: The object is already in the same thread
+ this.exports.swjs_receive_object(object_ref, transferring);
+ return;
+ }
+ this.postMessageToMainThread({
+ type: "requestTransfer",
+ data: {
+ objectRef: object_ref,
+ objectSourceTid: object_source_tid,
+ transferring,
+ destinationTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
+ },
+ });
+ },
};
}
- postMessageToMainThread(message) {
+ postMessageToMainThread(message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToMainThread" in threadChannel)) {
throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread.");
}
- threadChannel.postMessageToMainThread(message);
+ threadChannel.postMessageToMainThread(message, transfer);
}
- postMessageToWorkerThread(tid, message) {
+ postMessageToWorkerThread(tid, message, transfer = []) {
const threadChannel = this.options.threadChannel;
if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) {
throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads.");
}
- threadChannel.postMessageToWorkerThread(tid, message);
+ threadChannel.postMessageToWorkerThread(tid, message, transfer);
}
}
/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue`
diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
index ea8b5b43d..ed8240ca1 100644
--- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c
+++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c
@@ -59,5 +59,13 @@ __attribute__((export_name("swjs_library_features")))
int swjs_library_features(void) {
return _library_features();
}
+
+int swjs_get_worker_thread_id_cached(void) {
+ _Thread_local static int tid = 0;
+ if (tid == 0) {
+ tid = swjs_get_worker_thread_id();
+ }
+ return tid;
+}
#endif
#endif
diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
index 5cb6e6037..575c0e6fd 100644
--- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
+++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h
@@ -308,4 +308,13 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid))
IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void))
+int swjs_get_worker_thread_id_cached(void);
+
+/// Requests transferring a JavaScript object to another worker thread.
+///
+/// This must be called from the destination thread of the transfer.
+IMPORT_JS_FUNCTION(swjs_request_transferring_object, void, (JavaScriptObjectRef object,
+ int object_source_tid,
+ void * _Nonnull transferring))
+
#endif /* _CJavaScriptKit_h */
From e406cd3663255fe1761e8d8bb8287f7b75434bc8 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:23:56 +0000
Subject: [PATCH 02/19] Stop hardcoding the Swift toolchain version in the
Multithreading example
---
Examples/Multithreading/README.md | 16 ++++++++++++++--
Examples/Multithreading/build.sh | 2 +-
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/Examples/Multithreading/README.md b/Examples/Multithreading/README.md
index c95df2a8b..346f8cc8b 100644
--- a/Examples/Multithreading/README.md
+++ b/Examples/Multithreading/README.md
@@ -1,9 +1,21 @@
# Multithreading example
-Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` from [swift.org/install](https://www.swift.org/install/) and run the following commands:
+Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
```sh
-$ swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a/swift-wasm-DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads.artifactbundle.zip
+$ (
+ set -eo pipefail; \
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
+)
+$ export SWIFT_SDK_ID=$(
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
+)
$ ./build.sh
$ npx serve
```
diff --git a/Examples/Multithreading/build.sh b/Examples/Multithreading/build.sh
index 7d903b1f4..0f8670db1 100755
--- a/Examples/Multithreading/build.sh
+++ b/Examples/Multithreading/build.sh
@@ -1 +1 @@
-swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasip1-threads -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g
+swift build --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv -c release -Xswiftc -g
From cfa1b2ded3bf86b0fb6ca250a5674f2d2af9c5e6 Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:24:53 +0000
Subject: [PATCH 03/19] Update Multithreading example to support transferable
objects
---
Examples/Multithreading/Sources/JavaScript/index.js | 4 ++--
Examples/Multithreading/Sources/JavaScript/worker.js | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Examples/Multithreading/Sources/JavaScript/index.js b/Examples/Multithreading/Sources/JavaScript/index.js
index cc0c7e4e4..3cfc01a43 100644
--- a/Examples/Multithreading/Sources/JavaScript/index.js
+++ b/Examples/Multithreading/Sources/JavaScript/index.js
@@ -27,9 +27,9 @@ class ThreadRegistry {
};
}
- postMessageToWorkerThread(tid, data) {
+ postMessageToWorkerThread(tid, data, transfer) {
const worker = this.workers.get(tid);
- worker.postMessage(data);
+ worker.postMessage(data, transfer);
}
terminateWorkerThread(tid) {
diff --git a/Examples/Multithreading/Sources/JavaScript/worker.js b/Examples/Multithreading/Sources/JavaScript/worker.js
index eadd42bef..703df4407 100644
--- a/Examples/Multithreading/Sources/JavaScript/worker.js
+++ b/Examples/Multithreading/Sources/JavaScript/worker.js
@@ -5,9 +5,9 @@ self.onmessage = async (event) => {
const { instance, wasi, swiftRuntime } = await instantiate({
module,
threadChannel: {
- postMessageToMainThread: (message) => {
+ postMessageToMainThread: (message, transfer) => {
// Send the job to the main thread
- postMessage(message);
+ postMessage(message, transfer);
},
listenMessageFromMainThread: (listener) => {
self.onmessage = (event) => listener(event.data);
From 9d335a88d2048abca1dfd96e80a21c2e56c7311d Mon Sep 17 00:00:00 2001
From: Yuta Saito
Date: Mon, 10 Mar 2025 14:25:18 +0000
Subject: [PATCH 04/19] Add OffscreenCanvas example
---
Examples/OffscrenCanvas/.gitignore | 8 +
Examples/OffscrenCanvas/Package.swift | 20 ++
Examples/OffscrenCanvas/README.md | 21 +++
Examples/OffscrenCanvas/Sources/JavaScript | 1 +
.../OffscrenCanvas/Sources/MyApp/main.swift | 139 ++++++++++++++
.../OffscrenCanvas/Sources/MyApp/render.swift | 174 ++++++++++++++++++
Examples/OffscrenCanvas/build.sh | 1 +
Examples/OffscrenCanvas/index.html | 98 ++++++++++
Examples/OffscrenCanvas/serve.json | 1 +
9 files changed, 463 insertions(+)
create mode 100644 Examples/OffscrenCanvas/.gitignore
create mode 100644 Examples/OffscrenCanvas/Package.swift
create mode 100644 Examples/OffscrenCanvas/README.md
create mode 120000 Examples/OffscrenCanvas/Sources/JavaScript
create mode 100644 Examples/OffscrenCanvas/Sources/MyApp/main.swift
create mode 100644 Examples/OffscrenCanvas/Sources/MyApp/render.swift
create mode 100755 Examples/OffscrenCanvas/build.sh
create mode 100644 Examples/OffscrenCanvas/index.html
create mode 120000 Examples/OffscrenCanvas/serve.json
diff --git a/Examples/OffscrenCanvas/.gitignore b/Examples/OffscrenCanvas/.gitignore
new file mode 100644
index 000000000..0023a5340
--- /dev/null
+++ b/Examples/OffscrenCanvas/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/Examples/OffscrenCanvas/Package.swift b/Examples/OffscrenCanvas/Package.swift
new file mode 100644
index 000000000..7fc45ad1b
--- /dev/null
+++ b/Examples/OffscrenCanvas/Package.swift
@@ -0,0 +1,20 @@
+// swift-tools-version: 5.10
+
+import PackageDescription
+
+let package = Package(
+ name: "Example",
+ platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
+ dependencies: [
+ .package(path: "../../"),
+ ],
+ targets: [
+ .executableTarget(
+ name: "MyApp",
+ dependencies: [
+ .product(name: "JavaScriptKit", package: "JavaScriptKit"),
+ .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
+ ]
+ ),
+ ]
+)
diff --git a/Examples/OffscrenCanvas/README.md b/Examples/OffscrenCanvas/README.md
new file mode 100644
index 000000000..395b0c295
--- /dev/null
+++ b/Examples/OffscrenCanvas/README.md
@@ -0,0 +1,21 @@
+# OffscreenCanvas example
+
+Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
+
+```sh
+$ (
+ set -eo pipefail; \
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
+)
+$ export SWIFT_SDK_ID=$(
+ V="$(swiftc --version | head -n1)"; \
+ TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+ curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+ jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
+)
+$ ./build.sh
+$ npx serve
+```
diff --git a/Examples/OffscrenCanvas/Sources/JavaScript b/Examples/OffscrenCanvas/Sources/JavaScript
new file mode 120000
index 000000000..b24c2256e
--- /dev/null
+++ b/Examples/OffscrenCanvas/Sources/JavaScript
@@ -0,0 +1 @@
+../../Multithreading/Sources/JavaScript
\ No newline at end of file
diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift
new file mode 100644
index 000000000..ba660c6b2
--- /dev/null
+++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift
@@ -0,0 +1,139 @@
+import JavaScriptEventLoop
+import JavaScriptKit
+
+JavaScriptEventLoop.installGlobalExecutor()
+WebWorkerTaskExecutor.installGlobalExecutor()
+
+protocol CanvasRenderer {
+ func render(canvas: JSObject, size: Int) async throws
+}
+
+struct BackgroundRenderer: CanvasRenderer {
+ func render(canvas: JSObject, size: Int) async throws {
+ let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)
+ let transferringCanvas = JSObject.transfer(canvas)
+ let renderingTask = Task(executorPreference: executor) {
+ let canvas = try await JSObject.receive(transferringCanvas)
+ try await renderAnimation(canvas: canvas, size: size)
+ }
+ await withTaskCancellationHandler {
+ try? await renderingTask.value
+ } onCancel: {
+ renderingTask.cancel()
+ }
+ executor.terminate()
+ }
+}
+
+struct MainThreadRenderer: CanvasRenderer {
+ func render(canvas: JSObject, size: Int) async throws {
+ try await renderAnimation(canvas: canvas, size: size)
+ }
+}
+
+// FPS Counter for CSS animation
+func startFPSMonitor() {
+ let fpsCounterElement = JSObject.global.document.getElementById("fps-counter").object!
+
+ var lastTime = JSObject.global.performance.now().number!
+ var frames = 0
+
+ // Create a frame counter function
+ func countFrame() {
+ frames += 1
+ let currentTime = JSObject.global.performance.now().number!
+ let elapsed = currentTime - lastTime
+
+ if elapsed >= 1000 {
+ let fps = Int(Double(frames) * 1000 / elapsed)
+ fpsCounterElement.textContent = .string("FPS: \(fps)")
+ frames = 0
+ lastTime = currentTime
+ }
+
+ // Request next frame
+ _ = JSObject.global.requestAnimationFrame!(
+ JSClosure { _ in
+ countFrame()
+ return .undefined
+ })
+ }
+
+ // Start counting
+ countFrame()
+}
+
+@MainActor
+func onClick(renderer: CanvasRenderer) async throws {
+ let document = JSObject.global.document
+
+ let canvasContainerElement = document.getElementById("canvas-container").object!
+
+ // Remove all child elements from the canvas container
+ for i in 0..? = nil
+
+ // Start the FPS monitor for CSS animations
+ startFPSMonitor()
+
+ _ = renderButtonElement.addEventListener!(
+ "click",
+ JSClosure { _ in
+ renderingTask?.cancel()
+ renderingTask = Task {
+ let selectedValue = rendererSelectElement.value.string!
+ let renderer: CanvasRenderer =
+ selectedValue == "main" ? MainThreadRenderer() : BackgroundRenderer()
+ try await onClick(renderer: renderer)
+ }
+ return JSValue.undefined
+ })
+
+ _ = cancelButtonElement.addEventListener!(
+ "click",
+ JSClosure { _ in
+ renderingTask?.cancel()
+ return JSValue.undefined
+ })
+}
+
+Task {
+ try await main()
+}
+
+#if canImport(wasi_pthread)
+ import wasi_pthread
+ import WASILibc
+
+ /// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
+ /// the Swift concurrency runtime.
+ @_cdecl("pthread_mutex_lock")
+ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer) -> Int32 {
+ // DO NOT BLOCK MAIN THREAD
+ var ret: Int32
+ repeat {
+ ret = pthread_mutex_trylock(mutex)
+ } while ret == EBUSY
+ return ret
+ }
+#endif
diff --git a/Examples/OffscrenCanvas/Sources/MyApp/render.swift b/Examples/OffscrenCanvas/Sources/MyApp/render.swift
new file mode 100644
index 000000000..714cac184
--- /dev/null
+++ b/Examples/OffscrenCanvas/Sources/MyApp/render.swift
@@ -0,0 +1,174 @@
+import Foundation
+import JavaScriptKit
+
+func sleepOnThread(milliseconds: Int, isolation: isolated (any Actor)? = #isolation) async {
+ // Use the JavaScript setTimeout function to avoid hopping back to the main thread
+ await withCheckedContinuation(isolation: isolation) { continuation in
+ _ = JSObject.global.setTimeout!(
+ JSOneshotClosure { _ in
+ continuation.resume()
+ return JSValue.undefined
+ }, milliseconds
+ )
+ }
+}
+
+func renderAnimation(canvas: JSObject, size: Int, isolation: isolated (any Actor)? = #isolation)
+ async throws
+{
+ let ctx = canvas.getContext!("2d").object!
+
+ // Animation state variables
+ var time: Double = 0
+
+ // Create a large number of particles
+ let particleCount = 5000
+ var particles: [[Double]] = []
+
+ // Initialize particles with random positions and velocities
+ for _ in 0.. Double(size) {
+ particles[i][2] *= -0.8
+ }
+ if particles[i][1] < 0 || particles[i][1] > Double(size) {
+ particles[i][3] *= -0.8
+ }
+
+ // Calculate opacity based on lifespan
+ let opacity = particles[i][6] / particles[i][7]
+
+ // Get coordinates and properties
+ let x = particles[i][0]
+ let y = particles[i][1]
+ let size = particles[i][4]
+ let hue = (particles[i][5] + time * 10).truncatingRemainder(dividingBy: 360)
+
+ // Draw particle
+ _ = ctx.beginPath!()
+ ctx.fillStyle = .string("hsla(\(hue), 100%, 60%, \(opacity))")
+ _ = ctx.arc!(x, y, size, 0, 2 * Double.pi)
+ _ = ctx.fill!()
+
+ // Connect nearby particles with lines (only check some to save CPU)
+ if i % 20 == 0 {
+ for j in (i + 1)..
+
+
+
+ OffscreenCanvas Example
+
+
+
+
+
+ OffscreenCanvas Example
+
+
+
+
+
+
+
+
+ CSS Animation (Main Thread Performance Indicator)
+
+
+ FPS: 0
+
+
+
+
+