quickjs-emscripten-sync 1.9.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/index.d.ts +99 -3
- package/dist/quickjs-emscripten-sync.js +742 -559
- package/dist/quickjs-emscripten-sync.umd.cjs +33 -14
- package/package.json +7 -7
- package/src/contextex.ts +19 -7
- package/src/edge.test.ts +44 -2
- package/src/faultinjection.test.ts +265 -0
- package/src/identity.test.ts +57 -0
- package/src/index.test.ts +302 -0
- package/src/index.ts +215 -41
- package/src/marshal/custom.ts +20 -11
- package/src/marshal/function.test.ts +42 -18
- package/src/marshal/function.ts +106 -40
- package/src/marshal/index.ts +25 -1
- package/src/marshal/mapset.ts +30 -16
- package/src/marshal/object.ts +23 -14
- package/src/marshal/promise.test.ts +13 -11
- package/src/marshal/promise.ts +14 -6
- package/src/marshal/properties.test.ts +14 -5
- package/src/marshal/properties.ts +60 -12
- package/src/remarshalleak.test.ts +50 -0
- package/src/unmarshal/custom.test.ts +6 -3
- package/src/unmarshal/function.test.ts +46 -20
- package/src/unmarshal/index.test.ts +19 -10
- package/src/unmarshal/mapset.ts +24 -9
- package/src/unmarshal/object.test.ts +18 -9
- package/src/unmarshal/promise.test.ts +5 -4
- package/src/unmarshal/promise.ts +6 -2
- package/src/unmarshal/properties.test.ts +3 -2
- package/src/unmarshal/properties.ts +70 -36
- package/src/vmmap.ts +19 -7
- package/src/vmutil.test.ts +2 -1
- package/src/wrapper.test.ts +5 -3
- package/src/wrapper.ts +144 -37
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
quickjs-emscripten-sync wraps [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) and keeps object state in sync between the host (browser or Node.js) and a sandboxed QuickJS VM, so you can exchange values across the boundary as if they were plain JavaScript objects.
|
|
8
8
|
|
|
9
|
+
> [!TIP]
|
|
10
|
+
> **Want a higher-level, batteries-included plugin system?** Check out [**@reearth/zushi**](https://github.com/reearth/zushi) — a framework-agnostic plugin runtime that runs untrusted plugin code in an isolated WASM backend (QuickJS built in), exposes a host-defined API, and renders plugin UI in sandboxed iframes. quickjs-emscripten-sync provides the low-level host↔VM sync primitives; zushi builds a full plugin runtime on top.
|
|
11
|
+
|
|
9
12
|
## Features
|
|
10
13
|
|
|
11
14
|
- Exchange and synchronize values between the host and QuickJS seamlessly:
|
|
@@ -226,6 +229,14 @@ type Options = {
|
|
|
226
229
|
* "Passing objects by reference" below.
|
|
227
230
|
*/
|
|
228
231
|
marshalByReference?: (target: any) => boolean;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Marshal selected async host functions as *asyncified* functions, so the VM
|
|
235
|
+
* suspends until the host promise settles and the guest receives the resolved
|
|
236
|
+
* value synchronously. Only effective with an `AsyncArena`. See "Asyncified
|
|
237
|
+
* host functions" below.
|
|
238
|
+
*/
|
|
239
|
+
asyncify?: boolean | ((target: (...args: any[]) => any) => boolean);
|
|
229
240
|
};
|
|
230
241
|
```
|
|
231
242
|
|
|
@@ -369,6 +380,40 @@ ctx.dispose();
|
|
|
369
380
|
|
|
370
381
|
Evaluate JS code asynchronously and return the result on the host. Like `evalCode`, it converts and re-throws errors thrown during evaluation.
|
|
371
382
|
|
|
383
|
+
#### Asyncified host functions
|
|
384
|
+
|
|
385
|
+
Normally an async host function is marshalled like any other function, so the guest receives a marshalled `Promise`:
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
const ctx = await newAsyncContext();
|
|
389
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true });
|
|
390
|
+
|
|
391
|
+
arena.expose({ fetchSync: async () => "result" });
|
|
392
|
+
await arena.evalCodeAsync(`typeof fetchSync()`); // "object" (a Promise)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
With the `asyncify` option, selected async functions are marshalled through QuickJS's [Asyncify](https://emscripten.org/docs/porting/asyncify.html) support instead. When the guest calls one, the VM suspends until the host promise settles and the guest receives the **resolved value synchronously**, as if the call were blocking:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
|
|
399
|
+
|
|
400
|
+
arena.expose({ fetchSync: async () => "result" });
|
|
401
|
+
await arena.evalCodeAsync(`const v = fetchSync(); typeof v`); // "string"
|
|
402
|
+
await arena.evalCodeAsync(`fetchSync()`); // "result"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`asyncify` accepts:
|
|
406
|
+
|
|
407
|
+
- `true` — asyncify every host function that is *declared* async, auto-detected via `Object.prototype.toString.call(fn) === "[object AsyncFunction]"`. Functions transpiled to ES5 (e.g. Babel/TypeScript targeting older runtimes) are plain functions that return a promise and are **not** detected — use the predicate form for those.
|
|
408
|
+
- `(target) => boolean` — asyncify exactly the functions for which it returns `true`. Called with the unwrapped target, like `isMarshalable`.
|
|
409
|
+
- absent / `false` — current behavior; async functions yield a `Promise` in the guest.
|
|
410
|
+
|
|
411
|
+
Constraints (imposed by Emscripten Asyncify):
|
|
412
|
+
|
|
413
|
+
- Asyncified functions only work when evaluation is entered through an async entry point (`evalCodeAsync`). Calling one during a synchronous `evalCode` throws `Error: Function unexpectedly returned a Promise`.
|
|
414
|
+
- Only **one** suspended call is allowed at a time. Concurrent suspension (e.g. two asyncified calls awaiting simultaneously) throws a runtime error from quickjs-emscripten. Sequential calls are fine.
|
|
415
|
+
- The option requires a `QuickJSAsyncContext` (from `newAsyncContext()`). On a plain synchronous context it is silently ignored and async functions behave as before (Promise).
|
|
416
|
+
|
|
372
417
|
### `defaultRegisteredObjects: [any, string][]`
|
|
373
418
|
|
|
374
419
|
The default value of the `registeredObjects` option.
|
|
@@ -402,6 +447,15 @@ Only the `set`, `deleteProperty`, and `defineProperty` operations on objects are
|
|
|
402
447
|
|
|
403
448
|
`Date`, `Map`, `Set`, `ArrayBuffer`, and typed arrays are marshalled by value (a snapshot copy is created on the other side). They are not proxied, so later mutations are not synchronized, and self-referential `Map`/`Set` are not supported.
|
|
404
449
|
|
|
450
|
+
### `this` on plain calls
|
|
451
|
+
|
|
452
|
+
When an exposed host function is called plainly from the VM (`fn()`, not `obj.fn()`), the host function receives `this === undefined`. This differs from plain JavaScript, where a non-strict function called this way would see `this === globalThis`. The VM global object is intentionally **not** marshalled to the host: doing so would eagerly deep-copy the entire global graph on the first call and would leak `globalThis` across the boundary. Method calls are unaffected — `obj.fn()` still receives `obj` as `this`.
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
arena.expose({ whoAmI() { return this; } });
|
|
456
|
+
arena.evalCode(`whoAmI()`); // undefined (not globalThis)
|
|
457
|
+
```
|
|
458
|
+
|
|
405
459
|
## License
|
|
406
460
|
|
|
407
461
|
[MIT License](LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ContextEvalOptions } from 'quickjs-emscripten';
|
|
2
|
+
import { InterruptHandler } from 'quickjs-emscripten';
|
|
1
3
|
import { Intrinsics } from 'quickjs-emscripten';
|
|
2
4
|
import { QuickJSAsyncContext } from 'quickjs-emscripten';
|
|
3
5
|
import { QuickJSContext } from 'quickjs-emscripten';
|
|
@@ -19,6 +21,7 @@ export declare class Arena {
|
|
|
19
21
|
_temporalSync: Set<any>;
|
|
20
22
|
_symbol: symbol;
|
|
21
23
|
_symbolHandle: QuickJSHandle;
|
|
24
|
+
_wrapHandleImpl: WrapHandle;
|
|
22
25
|
_options?: Options;
|
|
23
26
|
/** Constructs a new Arena instance. It requires a quickjs-emscripten context initialized with `quickjs.newContext()`. */
|
|
24
27
|
constructor(ctx: QuickJSContext, options?: Options);
|
|
@@ -30,8 +33,14 @@ export declare class Arena {
|
|
|
30
33
|
[Symbol.dispose](): void;
|
|
31
34
|
/**
|
|
32
35
|
* Evaluate JS code in the VM and get the result as an object on the host side. It also converts and re-throws error objects when an error is thrown during evaluation.
|
|
36
|
+
*
|
|
37
|
+
* @param code - The JS code to evaluate
|
|
38
|
+
* @param filename - Optional filename used in error stacks and for debugging
|
|
39
|
+
* @param options - Optional eval options forwarded to quickjs-emscripten's
|
|
40
|
+
* `evalCode`, either an `EvalFlags` bitmask (`number`) or a
|
|
41
|
+
* {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
|
|
33
42
|
*/
|
|
34
|
-
evalCode<T = any>(code: string): T;
|
|
43
|
+
evalCode<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): T;
|
|
35
44
|
/**
|
|
36
45
|
* Evaluate ES module code in the VM and get the module's exports.
|
|
37
46
|
*
|
|
@@ -39,6 +48,10 @@ export declare class Arena {
|
|
|
39
48
|
*
|
|
40
49
|
* @param code - The ES module code to evaluate
|
|
41
50
|
* @param filename - Optional filename for debugging purposes (default: "module.js")
|
|
51
|
+
* @param options - Optional {@link ContextEvalOptions}. `type` is always forced
|
|
52
|
+
* to `"module"`, so any `type` in `options` is overridden. A numeric
|
|
53
|
+
* `EvalFlags` bitmask cannot be merged with the forced module type and is
|
|
54
|
+
* therefore not supported here; if a number is passed it is ignored.
|
|
42
55
|
* @returns The module's exports object, or a Promise resolving to exports if using top-level await
|
|
43
56
|
*
|
|
44
57
|
* @example
|
|
@@ -64,7 +77,7 @@ export declare class Arena {
|
|
|
64
77
|
* console.log(exports.data); // 123
|
|
65
78
|
* ```
|
|
66
79
|
*/
|
|
67
|
-
evalModule<T = any>(code: string, filename?: string): T | Promise<T>;
|
|
80
|
+
evalModule<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): T | Promise<T>;
|
|
68
81
|
/**
|
|
69
82
|
* Almost same as `vm.executePendingJobs()`, but it converts and re-throws error objects when an error is thrown during evaluation.
|
|
70
83
|
*/
|
|
@@ -111,6 +124,39 @@ export declare class Arena {
|
|
|
111
124
|
* ```
|
|
112
125
|
*/
|
|
113
126
|
setMaxStackSize(stackSize: number): void;
|
|
127
|
+
/**
|
|
128
|
+
* Set a callback which is regularly called by the QuickJS engine when it is
|
|
129
|
+
* executing code. This callback can be used to implement an execution
|
|
130
|
+
* timeout: return `true` from the handler to interrupt the running code.
|
|
131
|
+
*
|
|
132
|
+
* This is useful for preventing runaway CPU usage (e.g. infinite loops) in
|
|
133
|
+
* untrusted code.
|
|
134
|
+
*
|
|
135
|
+
* @param cb - The interrupt handler. Return `true` to interrupt execution.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```js
|
|
139
|
+
* // Interrupt evaluation after 1 second
|
|
140
|
+
* const deadline = Date.now() + 1000;
|
|
141
|
+
* arena.setInterruptHandler(() => Date.now() > deadline);
|
|
142
|
+
*
|
|
143
|
+
* try {
|
|
144
|
+
* arena.evalCode(`while (true) {}`);
|
|
145
|
+
* } catch (e) {
|
|
146
|
+
* console.log("Execution interrupted");
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
setInterruptHandler(cb: InterruptHandler): void;
|
|
151
|
+
/**
|
|
152
|
+
* Remove the interrupt handler previously set with `setInterruptHandler`.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```js
|
|
156
|
+
* arena.removeInterruptHandler();
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
removeInterruptHandler(): void;
|
|
114
160
|
/**
|
|
115
161
|
* Get detailed memory usage statistics for this runtime.
|
|
116
162
|
*
|
|
@@ -204,6 +250,7 @@ export declare class Arena {
|
|
|
204
250
|
_unwrapResultAndUnmarshal(result: VmCallResult<QuickJSHandle> | undefined): any;
|
|
205
251
|
_isMarshalable: (t: unknown) => boolean | "json";
|
|
206
252
|
_marshalByReference: (t: unknown) => boolean;
|
|
253
|
+
_asyncify: (t: unknown) => boolean;
|
|
207
254
|
_registerHostRef: (t: unknown, handle: QuickJSHandle) => QuickJSHandle;
|
|
208
255
|
_marshalFind: (t: unknown) => QuickJSHandle | undefined;
|
|
209
256
|
_marshalPre: (t: unknown, h: QuickJSHandle | QuickJSDeferredPromise, mode: true | "json" | undefined) => Wrapped<QuickJSHandle> | undefined;
|
|
@@ -211,6 +258,7 @@ export declare class Arena {
|
|
|
211
258
|
_disposeTransient: (handle: QuickJSHandle) => void;
|
|
212
259
|
_marshalPreApply: (target: (...args: any[]) => any, that: unknown, args: unknown[]) => void;
|
|
213
260
|
_marshal: (target: any) => [QuickJSHandle, boolean];
|
|
261
|
+
_prepareMarshalReturn: (h: QuickJSHandle) => QuickJSHandle;
|
|
214
262
|
_preUnmarshal: (t: any, h: QuickJSHandle) => Wrapped<any>;
|
|
215
263
|
_unmarshalFind: (h: QuickJSHandle) => unknown;
|
|
216
264
|
_unmarshal: (handle: QuickJSHandle) => any;
|
|
@@ -236,8 +284,14 @@ export declare class AsyncArena extends Arena {
|
|
|
236
284
|
* Evaluate JS code asynchronously in the VM and get the result on the host
|
|
237
285
|
* side. Like `evalCode`, it converts and re-throws errors thrown during
|
|
238
286
|
* evaluation. Use this when the code awaits host-provided promises.
|
|
287
|
+
*
|
|
288
|
+
* @param code - The JS code to evaluate
|
|
289
|
+
* @param filename - Optional filename used in error stacks and for debugging
|
|
290
|
+
* @param options - Optional eval options forwarded to quickjs-emscripten's
|
|
291
|
+
* `evalCodeAsync`, either an `EvalFlags` bitmask (`number`) or a
|
|
292
|
+
* {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
|
|
239
293
|
*/
|
|
240
|
-
evalCodeAsync<T = any>(code: string, filename?: string): Promise<T>;
|
|
294
|
+
evalCodeAsync<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): Promise<T>;
|
|
241
295
|
}
|
|
242
296
|
|
|
243
297
|
export declare function call(ctx: QuickJSContext, code: string, thisArg?: QuickJSHandle, ...args: QuickJSHandle[]): QuickJSHandle;
|
|
@@ -249,11 +303,15 @@ export declare function complexity(value: any, max?: number): number;
|
|
|
249
303
|
|
|
250
304
|
export declare function consumeAll<T extends QuickJSHandle[], K>(handles: T, cb: (handles: T) => K): K;
|
|
251
305
|
|
|
306
|
+
export { ContextEvalOptions }
|
|
307
|
+
|
|
252
308
|
/**
|
|
253
309
|
* Default value of registeredObjects option of the Arena class constructor.
|
|
254
310
|
*/
|
|
255
311
|
export declare const defaultRegisteredObjects: [any, string][];
|
|
256
312
|
|
|
313
|
+
export { InterruptHandler }
|
|
314
|
+
|
|
257
315
|
export { Intrinsics }
|
|
258
316
|
|
|
259
317
|
export declare function isES2015Class(cls: any): cls is new (...args: any[]) => any;
|
|
@@ -290,6 +348,34 @@ export declare type Options = {
|
|
|
290
348
|
syncEnabled?: boolean;
|
|
291
349
|
/** A callback that returns whether an object should be passed to the VM by reference (as an opaque HostRef) instead of being marshalled by value/proxy. The guest cannot read such objects, but can hold them and pass them back to the host, where they resolve to the original object. */
|
|
292
350
|
marshalByReference?: (target: any) => boolean;
|
|
351
|
+
/**
|
|
352
|
+
* Marshal selected host functions as *asyncified* functions. Normally an async
|
|
353
|
+
* host function is marshalled like any other function, so the guest receives a
|
|
354
|
+
* marshalled `Promise`. When a function is asyncified, the QuickJS VM instead
|
|
355
|
+
* suspends until the host promise settles and the guest receives the resolved
|
|
356
|
+
* value synchronously (as if the call were blocking).
|
|
357
|
+
*
|
|
358
|
+
* - `true`: asyncify every host function that is *declared* async, auto-detected
|
|
359
|
+
* via `Object.prototype.toString.call(fn) === "[object AsyncFunction]"`. Note
|
|
360
|
+
* that functions transpiled to ES5 (e.g. by Babel/TypeScript targeting older
|
|
361
|
+
* runtimes) are plain functions returning a promise and are **not** detected —
|
|
362
|
+
* use the predicate form for those.
|
|
363
|
+
* - predicate: asyncify exactly the functions for which it returns `true`. It is
|
|
364
|
+
* called with the *unwrapped* target (the original function, not a sync proxy),
|
|
365
|
+
* like `isMarshalable`.
|
|
366
|
+
* - absent/`false`: current behavior; async functions yield a `Promise` in the guest.
|
|
367
|
+
*
|
|
368
|
+
* Constraints (Emscripten Asyncify):
|
|
369
|
+
* - Asyncified functions only work when evaluation is entered through an async
|
|
370
|
+
* entry point ({@link AsyncArena.evalCodeAsync}). Calling one during a
|
|
371
|
+
* synchronous `evalCode` fails with `Error: Function unexpectedly returned a Promise`.
|
|
372
|
+
* - Only ONE suspended call is allowed at a time; concurrent suspension throws a
|
|
373
|
+
* runtime error from quickjs-emscripten.
|
|
374
|
+
* - If the context does not support `newAsyncifiedFunction` (a plain sync context
|
|
375
|
+
* from `getQuickJS().newContext()`), this option is silently ignored and async
|
|
376
|
+
* functions behave as today (Promise).
|
|
377
|
+
*/
|
|
378
|
+
asyncify?: boolean | ((target: (...args: any[]) => any) => boolean);
|
|
293
379
|
};
|
|
294
380
|
|
|
295
381
|
declare type Options_2 = {
|
|
@@ -303,6 +389,9 @@ declare type Options_2 = {
|
|
|
303
389
|
disposeTransient?: (handle: QuickJSHandle) => void;
|
|
304
390
|
pre: (target: unknown, handle: QuickJSHandle | QuickJSDeferredPromise, mode: true | "json" | undefined) => QuickJSHandle | undefined;
|
|
305
391
|
preApply?: (target: (...args: any[]) => any, thisArg: unknown, args: unknown[]) => any;
|
|
392
|
+
prepareReturn?: (handle: QuickJSHandle) => QuickJSHandle;
|
|
393
|
+
unwrap?: (target: unknown) => unknown;
|
|
394
|
+
asyncify?: (target: unknown) => boolean;
|
|
306
395
|
custom?: Iterable<(obj: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined>;
|
|
307
396
|
};
|
|
308
397
|
|
|
@@ -364,6 +453,13 @@ export declare class VMMap {
|
|
|
364
453
|
|
|
365
454
|
export declare function walkObject(value: any, callback?: (target: any, set: Set<any>) => boolean | void): Set<any>;
|
|
366
455
|
|
|
456
|
+
declare type WrapHandle = {
|
|
457
|
+
/** Wrap a VM handle with the shared proxyFuncs, like the old `wrapHandle`. */
|
|
458
|
+
wrapHandle: (handle: QuickJSHandle) => [Wrapped<QuickJSHandle> | undefined, boolean];
|
|
459
|
+
/** Dispose the shared proxyFuncs handle. Call exactly once, at Arena dispose. */
|
|
460
|
+
dispose: () => void;
|
|
461
|
+
};
|
|
462
|
+
|
|
367
463
|
declare type Wrapped<T> = T & {
|
|
368
464
|
__qes_wrapped: never;
|
|
369
465
|
};
|