Skip to content

Commit 5887f87

Browse files
Add JSRemote<JSObject> API for accessing JSObjects without transfer
The new API allows creating a handle for a `JSObject` that remains on its original JavaScript thread and hopping back to that thread to access the object when needed. This is useful for cases where the object cannot be transferred to another thread, but occasional access is still required or when we want to guarantee that an object is always accessed on the same thread for safety (it should be statically guaranteed with strict Sendable checking, but modules with language mode 5 don't have that). Example: ```swift let document = JSObject.global.document.object! let remoteDocument = JSRemote(document) let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1) let title = try await Task(executorPreference: executor) { try await remoteDocument.withJSObject { document in document.title.string ?? "" } }.value ```
1 parent 7e242b0 commit 5887f87

File tree

11 files changed

+495
-38
lines changed

11 files changed

+495
-38
lines changed

Examples/ActorOnWebWorker/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let package = Package(
66
name: "Example",
77
platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
88
dependencies: [
9-
.package(path: "../../")
9+
.package(name: "JavaScriptKit", path: "../../")
1010
],
1111
targets: [
1212
.executableTarget(

Examples/Multithreading/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let package = Package(
66
name: "Example",
77
platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
88
dependencies: [
9-
.package(path: "../../"),
9+
.package(name: "JavaScriptKit", path: "../../"),
1010
.package(
1111
url: "https://github.com/kateinoigakukun/chibi-ray",
1212
revision: "c8cab621a3338dd2f8e817d3785362409d3b8cf1"

Examples/OffscrenCanvas/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let package = Package(
66
name: "Example",
77
platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
88
dependencies: [
9-
.package(path: "../../")
9+
.package(name: "JavaScriptKit", path: "../../")
1010
],
1111
targets: [
1212
.executableTarget(

Plugins/PackageToJS/Templates/runtime.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ declare class ITCInterface {
9797
sendingContext: pointer;
9898
transfer: Transferable[];
9999
};
100+
invokeRemoteJSObjectBody(invocationContext: pointer): {
101+
object: undefined;
102+
transfer: Transferable[];
103+
};
100104
release(objectRef: ref): {
101105
object: undefined;
102106
transfer: Transferable[];
@@ -140,6 +144,8 @@ type ResponseMessage = {
140144
sourceTid: number;
141145
/** The context pointer of the request */
142146
context: pointer;
147+
/** The request method this response corresponds to */
148+
requestMethod: keyof ITCInterface;
143149
/** The response content */
144150
response: {
145151
ok: true;

Plugins/PackageToJS/Templates/runtime.mjs

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ class ITCInterface {
135135
const transfer = transferringObjects.map((ref) => this.memory.getObject(ref));
136136
return { object: objects, sendingContext, transfer };
137137
}
138+
invokeRemoteJSObjectBody(invocationContext) {
139+
return { object: undefined, transfer: [] };
140+
}
138141
release(objectRef) {
139142
this.memory.release(objectRef);
140143
return { object: undefined, transfer: [] };
@@ -455,13 +458,51 @@ class SwiftRuntime {
455458
if (broker)
456459
return broker;
457460
const itcInterface = new ITCInterface(this.memory);
461+
const defaultRequestHandler = (message) => {
462+
const request = message.data.request;
463+
// @ts-ignore dynamic dispatch by method name
464+
const result = itcInterface[request.method].apply(itcInterface, request.parameters);
465+
return { ok: true, value: result };
466+
};
467+
const requestHandlers = {
468+
invokeRemoteJSObjectBody: (message) => {
469+
const invocationContext = message.data.request
470+
.parameters[0];
471+
const hasError = this.exports.swjs_invoke_remote_jsobject_body(invocationContext);
472+
return {
473+
ok: true,
474+
value: {
475+
object: hasError,
476+
sendingContext: message.data.context,
477+
transfer: [],
478+
},
479+
};
480+
},
481+
};
482+
const defaultResponseHandler = (message) => {
483+
if (message.data.response.ok) {
484+
const object = this.memory.retain(message.data.response.value.object);
485+
this.exports.swjs_receive_response(object, message.data.context);
486+
}
487+
else {
488+
const error = deserializeError(message.data.response.error);
489+
const errorObject = this.memory.retain(error);
490+
this.exports.swjs_receive_error(errorObject, message.data.context);
491+
}
492+
};
493+
const responseHandlers = {
494+
invokeRemoteJSObjectBody: (_message) => {
495+
// Swift continuation is resumed on the owner thread.
496+
},
497+
};
458498
const newBroker = new MessageBroker((_a = this.tid) !== null && _a !== void 0 ? _a : -1, threadChannel, {
459499
onRequest: (message) => {
500+
var _a;
460501
let returnValue;
461502
try {
462-
// @ts-ignore
463-
const result = itcInterface[message.data.request.method](...message.data.request.parameters);
464-
returnValue = { ok: true, value: result };
503+
const method = message.data.request.method;
504+
const handler = (_a = requestHandlers[method]) !== null && _a !== void 0 ? _a : defaultRequestHandler;
505+
returnValue = handler(message);
465506
}
466507
catch (error) {
467508
returnValue = {
@@ -474,6 +515,7 @@ class SwiftRuntime {
474515
data: {
475516
sourceTid: message.data.sourceTid,
476517
context: message.data.context,
518+
requestMethod: message.data.request.method,
477519
response: returnValue,
478520
},
479521
};
@@ -489,15 +531,10 @@ class SwiftRuntime {
489531
}
490532
},
491533
onResponse: (message) => {
492-
if (message.data.response.ok) {
493-
const object = this.memory.retain(message.data.response.value.object);
494-
this.exports.swjs_receive_response(object, message.data.context);
495-
}
496-
else {
497-
const error = deserializeError(message.data.response.error);
498-
const errorObject = this.memory.retain(error);
499-
this.exports.swjs_receive_error(errorObject, message.data.context);
500-
}
534+
var _a;
535+
const method = message.data.requestMethod;
536+
const handler = (_a = responseHandlers[method]) !== null && _a !== void 0 ? _a : defaultResponseHandler;
537+
handler(message);
501538
},
502539
});
503540
broker = newBroker;
@@ -842,6 +879,25 @@ class SwiftRuntime {
842879
},
843880
});
844881
},
882+
swjs_request_remote_jsobject_body: (object_source_tid, invocation_context) => {
883+
var _a;
884+
if (!this.options.threadChannel) {
885+
throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request remote JSObject access.");
886+
}
887+
const broker = getMessageBroker(this.options.threadChannel);
888+
broker.request({
889+
type: "request",
890+
data: {
891+
sourceTid: (_a = this.tid) !== null && _a !== void 0 ? _a : MAIN_THREAD_TID,
892+
targetTid: object_source_tid,
893+
context: invocation_context,
894+
request: {
895+
method: "invokeRemoteJSObjectBody",
896+
parameters: [invocation_context],
897+
},
898+
},
899+
});
900+
},
845901
};
846902
}
847903
postMessageToMainThread(message, transfer = []) {

Runtime/src/index.ts

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
deserializeError,
1212
MainToWorkerMessage,
1313
MessageBroker,
14+
RequestMessage,
1415
ResponseMessage,
1516
ITCInterface,
1617
serializeError,
@@ -265,29 +266,98 @@ export class SwiftRuntime {
265266
const getMessageBroker = (threadChannel: SwiftRuntimeThreadChannel) => {
266267
if (broker) return broker;
267268
const itcInterface = new ITCInterface(this.memory);
269+
type ITCMethodName = keyof ITCInterface;
270+
271+
const defaultRequestHandler = (
272+
message: RequestMessage,
273+
): ResponseMessage["data"]["response"] => {
274+
const request = message.data.request;
275+
// @ts-ignore dynamic dispatch by method name
276+
const result = itcInterface[request.method].apply(
277+
itcInterface,
278+
request.parameters as any[],
279+
);
280+
return { ok: true, value: result };
281+
};
282+
283+
const requestHandlers: Partial<
284+
Record<
285+
ITCMethodName,
286+
(
287+
message: RequestMessage,
288+
) => ResponseMessage["data"]["response"]
289+
>
290+
> = {
291+
invokeRemoteJSObjectBody: (message) => {
292+
const invocationContext = message.data.request
293+
.parameters[0] as pointer;
294+
const hasError =
295+
this.exports.swjs_invoke_remote_jsobject_body(
296+
invocationContext,
297+
);
298+
return {
299+
ok: true,
300+
value: {
301+
object: hasError,
302+
sendingContext: message.data.context,
303+
transfer: [],
304+
},
305+
};
306+
},
307+
};
308+
309+
const defaultResponseHandler = (message: ResponseMessage) => {
310+
if (message.data.response.ok) {
311+
const object = this.memory.retain(
312+
message.data.response.value.object,
313+
);
314+
this.exports.swjs_receive_response(
315+
object,
316+
message.data.context,
317+
);
318+
} else {
319+
const error = deserializeError(message.data.response.error);
320+
const errorObject = this.memory.retain(error);
321+
this.exports.swjs_receive_error(
322+
errorObject,
323+
message.data.context,
324+
);
325+
}
326+
};
327+
328+
const responseHandlers: Partial<
329+
Record<ITCMethodName, (message: ResponseMessage) => void>
330+
> = {
331+
invokeRemoteJSObjectBody: (_message) => {
332+
// Swift continuation is resumed on the owner thread.
333+
},
334+
};
335+
268336
const newBroker = new MessageBroker(this.tid ?? -1, threadChannel, {
269337
onRequest: (message) => {
270338
let returnValue: ResponseMessage["data"]["response"];
271339
try {
272-
// @ts-ignore
273-
const result = itcInterface[
274-
message.data.request.method
275-
](...message.data.request.parameters);
276-
returnValue = { ok: true, value: result };
340+
const method = message.data.request.method;
341+
const handler =
342+
requestHandlers[method] ?? defaultRequestHandler;
343+
returnValue = handler(message);
277344
} catch (error) {
278345
returnValue = {
279346
ok: false,
280347
error: serializeError(error),
281348
};
282349
}
350+
283351
const responseMessage: ResponseMessage = {
284352
type: "response",
285353
data: {
286354
sourceTid: message.data.sourceTid,
287355
context: message.data.context,
356+
requestMethod: message.data.request.method,
288357
response: returnValue,
289358
},
290359
};
360+
291361
try {
292362
newBroker.reply(responseMessage);
293363
} catch (error) {
@@ -303,24 +373,10 @@ export class SwiftRuntime {
303373
}
304374
},
305375
onResponse: (message) => {
306-
if (message.data.response.ok) {
307-
const object = this.memory.retain(
308-
message.data.response.value.object,
309-
);
310-
this.exports.swjs_receive_response(
311-
object,
312-
message.data.context,
313-
);
314-
} else {
315-
const error = deserializeError(
316-
message.data.response.error,
317-
);
318-
const errorObject = this.memory.retain(error);
319-
this.exports.swjs_receive_error(
320-
errorObject,
321-
message.data.context,
322-
);
323-
}
376+
const method = message.data.requestMethod;
377+
const handler =
378+
responseHandlers[method] ?? defaultResponseHandler;
379+
handler(message);
324380
},
325381
});
326382
broker = newBroker;
@@ -934,6 +990,29 @@ export class SwiftRuntime {
934990
},
935991
});
936992
},
993+
swjs_request_remote_jsobject_body: (
994+
object_source_tid: number,
995+
invocation_context: pointer,
996+
) => {
997+
if (!this.options.threadChannel) {
998+
throw new Error(
999+
"threadChannel is not set in options given to SwiftRuntime. Please set it to request remote JSObject access.",
1000+
);
1001+
}
1002+
const broker = getMessageBroker(this.options.threadChannel);
1003+
broker.request({
1004+
type: "request",
1005+
data: {
1006+
sourceTid: this.tid ?? MAIN_THREAD_TID,
1007+
targetTid: object_source_tid,
1008+
context: invocation_context,
1009+
request: {
1010+
method: "invokeRemoteJSObjectBody",
1011+
parameters: [invocation_context],
1012+
},
1013+
},
1014+
});
1015+
},
9371016
};
9381017
}
9391018

Runtime/src/itc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ export class ITCInterface {
117117
return { object: objects, sendingContext, transfer };
118118
}
119119

120+
invokeRemoteJSObjectBody(invocationContext: pointer): {
121+
object: undefined;
122+
transfer: Transferable[];
123+
} {
124+
return { object: undefined, transfer: [] };
125+
}
126+
120127
release(objectRef: ref): { object: undefined; transfer: Transferable[] } {
121128
this.memory.release(objectRef);
122129
return { object: undefined, transfer: [] };
@@ -163,6 +170,8 @@ export type ResponseMessage = {
163170
sourceTid: number;
164171
/** The context pointer of the request */
165172
context: pointer;
173+
/** The request method this response corresponds to */
174+
requestMethod: keyof ITCInterface;
166175
/** The response content */
167176
response:
168177
| {

Runtime/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ExportedFunctions {
2222
swjs_wake_worker_thread(): void;
2323
swjs_receive_response(object: ref, transferring: pointer): void;
2424
swjs_receive_error(error: ref, context: number): void;
25+
swjs_invoke_remote_jsobject_body(context: pointer): number;
2526
}
2627

2728
export const enum LibraryFeatures {

0 commit comments

Comments
 (0)