quickjs-emscripten-sync 1.10.0 → 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 +45 -0
- package/dist/index.d.ts +97 -3
- package/dist/quickjs-emscripten-sync.js +715 -550
- package/dist/quickjs-emscripten-sync.umd.cjs +33 -14
- package/package.json +7 -7
- package/src/contextex.ts +19 -7
- package/src/faultinjection.test.ts +265 -0
- package/src/index.test.ts +282 -0
- package/src/index.ts +204 -42
- package/src/marshal/custom.ts +20 -11
- package/src/marshal/function.test.ts +36 -14
- package/src/marshal/function.ts +100 -51
- package/src/marshal/index.ts +10 -0
- 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/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 +14 -6
- package/src/vmutil.test.ts +2 -1
- package/src/wrapper.test.ts +5 -3
- package/src/wrapper.ts +144 -37
package/src/index.ts
CHANGED
|
@@ -6,13 +6,17 @@ import type {
|
|
|
6
6
|
SuccessOrFail,
|
|
7
7
|
VmCallResult,
|
|
8
8
|
Intrinsics,
|
|
9
|
+
ContextEvalOptions,
|
|
10
|
+
InterruptHandler,
|
|
9
11
|
} from "quickjs-emscripten";
|
|
10
12
|
|
|
11
|
-
import {
|
|
13
|
+
import type { QuickJSContextEx } from "./contextex";
|
|
14
|
+
import { wrapContext } from "./contextex";
|
|
12
15
|
import { defaultRegisteredObjects } from "./default";
|
|
13
16
|
import marshal from "./marshal";
|
|
14
17
|
import unmarshal from "./unmarshal";
|
|
15
18
|
import unmarshalHostRef from "./unmarshal/hostref";
|
|
19
|
+
import unmarshalPrimitive from "./unmarshal/primitive";
|
|
16
20
|
import { complexity, isES2015Class, isObject, walkObject } from "./util";
|
|
17
21
|
import VMMap from "./vmmap";
|
|
18
22
|
import {
|
|
@@ -26,7 +30,8 @@ import {
|
|
|
26
30
|
enableFnCache,
|
|
27
31
|
disposeFnCache,
|
|
28
32
|
} from "./vmutil";
|
|
29
|
-
import {
|
|
33
|
+
import type { Wrapped, WrapHandle } from "./wrapper";
|
|
34
|
+
import { wrap, createWrapHandle, unwrap, unwrapHandle } from "./wrapper";
|
|
30
35
|
|
|
31
36
|
export {
|
|
32
37
|
VMMap,
|
|
@@ -43,7 +48,7 @@ export {
|
|
|
43
48
|
consumeAll,
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
export type { Intrinsics };
|
|
51
|
+
export type { Intrinsics, ContextEvalOptions, InterruptHandler };
|
|
47
52
|
|
|
48
53
|
export type Options = {
|
|
49
54
|
/** A callback that returns a boolean value that determines whether an object is marshalled or not. If false, no marshaling will be done and undefined will be passed to the QuickJS VM, otherwise marshaling will be done. By default, all objects will be marshalled. */
|
|
@@ -69,6 +74,34 @@ export type Options = {
|
|
|
69
74
|
syncEnabled?: boolean;
|
|
70
75
|
/** 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. */
|
|
71
76
|
marshalByReference?: (target: any) => boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Marshal selected host functions as *asyncified* functions. Normally an async
|
|
79
|
+
* host function is marshalled like any other function, so the guest receives a
|
|
80
|
+
* marshalled `Promise`. When a function is asyncified, the QuickJS VM instead
|
|
81
|
+
* suspends until the host promise settles and the guest receives the resolved
|
|
82
|
+
* value synchronously (as if the call were blocking).
|
|
83
|
+
*
|
|
84
|
+
* - `true`: asyncify every host function that is *declared* async, auto-detected
|
|
85
|
+
* via `Object.prototype.toString.call(fn) === "[object AsyncFunction]"`. Note
|
|
86
|
+
* that functions transpiled to ES5 (e.g. by Babel/TypeScript targeting older
|
|
87
|
+
* runtimes) are plain functions returning a promise and are **not** detected —
|
|
88
|
+
* use the predicate form for those.
|
|
89
|
+
* - predicate: asyncify exactly the functions for which it returns `true`. It is
|
|
90
|
+
* called with the *unwrapped* target (the original function, not a sync proxy),
|
|
91
|
+
* like `isMarshalable`.
|
|
92
|
+
* - absent/`false`: current behavior; async functions yield a `Promise` in the guest.
|
|
93
|
+
*
|
|
94
|
+
* Constraints (Emscripten Asyncify):
|
|
95
|
+
* - Asyncified functions only work when evaluation is entered through an async
|
|
96
|
+
* entry point ({@link AsyncArena.evalCodeAsync}). Calling one during a
|
|
97
|
+
* synchronous `evalCode` fails with `Error: Function unexpectedly returned a Promise`.
|
|
98
|
+
* - Only ONE suspended call is allowed at a time; concurrent suspension throws a
|
|
99
|
+
* runtime error from quickjs-emscripten.
|
|
100
|
+
* - If the context does not support `newAsyncifiedFunction` (a plain sync context
|
|
101
|
+
* from `getQuickJS().newContext()`), this option is silently ignored and async
|
|
102
|
+
* functions behave as today (Promise).
|
|
103
|
+
*/
|
|
104
|
+
asyncify?: boolean | ((target: (...args: any[]) => any) => boolean);
|
|
72
105
|
};
|
|
73
106
|
|
|
74
107
|
/**
|
|
@@ -91,6 +124,7 @@ export class Arena {
|
|
|
91
124
|
_temporalSync = new Set<any>();
|
|
92
125
|
_symbol = Symbol();
|
|
93
126
|
_symbolHandle: QuickJSHandle;
|
|
127
|
+
_wrapHandleImpl: WrapHandle;
|
|
94
128
|
_options?: Options;
|
|
95
129
|
|
|
96
130
|
/** Constructs a new Arena instance. It requires a quickjs-emscripten context initialized with `quickjs.newContext()`. */
|
|
@@ -107,6 +141,17 @@ export class Arena {
|
|
|
107
141
|
enableFnCache(this.context);
|
|
108
142
|
this._options = options;
|
|
109
143
|
this._symbolHandle = ctx.unwrapResult(ctx.evalCode(`Symbol()`));
|
|
144
|
+
// One proxyFuncs handle shared by every wrapped handle for the Arena's
|
|
145
|
+
// lifetime, instead of allocating a fresh VM function per wrap.
|
|
146
|
+
this._wrapHandleImpl = createWrapHandle(
|
|
147
|
+
this.context,
|
|
148
|
+
this._symbol,
|
|
149
|
+
this._symbolHandle,
|
|
150
|
+
this._unmarshal,
|
|
151
|
+
this._syncMode,
|
|
152
|
+
this._options?.isHandleWrappable,
|
|
153
|
+
this._options?.syncEnabled ?? true,
|
|
154
|
+
);
|
|
110
155
|
this._map = new VMMap(ctx);
|
|
111
156
|
this._registeredMap = new VMMap(ctx);
|
|
112
157
|
this.registerAll(options?.registeredObjects ?? defaultRegisteredObjects);
|
|
@@ -122,6 +167,7 @@ export class Arena {
|
|
|
122
167
|
this._transientHandles.clear();
|
|
123
168
|
this._map.dispose();
|
|
124
169
|
this._registeredMap.dispose();
|
|
170
|
+
this._wrapHandleImpl.dispose();
|
|
125
171
|
this._symbolHandle.dispose();
|
|
126
172
|
disposeFnCache(this.context);
|
|
127
173
|
this.context.disposeEx?.();
|
|
@@ -134,9 +180,15 @@ export class Arena {
|
|
|
134
180
|
|
|
135
181
|
/**
|
|
136
182
|
* 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.
|
|
183
|
+
*
|
|
184
|
+
* @param code - The JS code to evaluate
|
|
185
|
+
* @param filename - Optional filename used in error stacks and for debugging
|
|
186
|
+
* @param options - Optional eval options forwarded to quickjs-emscripten's
|
|
187
|
+
* `evalCode`, either an `EvalFlags` bitmask (`number`) or a
|
|
188
|
+
* {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
|
|
137
189
|
*/
|
|
138
|
-
evalCode<T = any>(code: string): T {
|
|
139
|
-
const handle = this.context.evalCode(code);
|
|
190
|
+
evalCode<T = any>(code: string, filename?: string, options?: number | ContextEvalOptions): T {
|
|
191
|
+
const handle = this.context.evalCode(code, filename, options);
|
|
140
192
|
return this._unwrapResultAndUnmarshal(handle);
|
|
141
193
|
}
|
|
142
194
|
|
|
@@ -147,6 +199,10 @@ export class Arena {
|
|
|
147
199
|
*
|
|
148
200
|
* @param code - The ES module code to evaluate
|
|
149
201
|
* @param filename - Optional filename for debugging purposes (default: "module.js")
|
|
202
|
+
* @param options - Optional {@link ContextEvalOptions}. `type` is always forced
|
|
203
|
+
* to `"module"`, so any `type` in `options` is overridden. A numeric
|
|
204
|
+
* `EvalFlags` bitmask cannot be merged with the forced module type and is
|
|
205
|
+
* therefore not supported here; if a number is passed it is ignored.
|
|
150
206
|
* @returns The module's exports object, or a Promise resolving to exports if using top-level await
|
|
151
207
|
*
|
|
152
208
|
* @example
|
|
@@ -172,8 +228,15 @@ export class Arena {
|
|
|
172
228
|
* console.log(exports.data); // 123
|
|
173
229
|
* ```
|
|
174
230
|
*/
|
|
175
|
-
evalModule<T = any>(
|
|
176
|
-
|
|
231
|
+
evalModule<T = any>(
|
|
232
|
+
code: string,
|
|
233
|
+
filename = "module.js",
|
|
234
|
+
options?: number | ContextEvalOptions,
|
|
235
|
+
): T | Promise<T> {
|
|
236
|
+
const handle = this.context.evalCode(code, filename, {
|
|
237
|
+
...(typeof options === "number" ? {} : options),
|
|
238
|
+
type: "module",
|
|
239
|
+
});
|
|
177
240
|
return this._unwrapResultAndUnmarshal(handle);
|
|
178
241
|
}
|
|
179
242
|
|
|
@@ -236,6 +299,45 @@ export class Arena {
|
|
|
236
299
|
this.context.runtime.setMaxStackSize(stackSize);
|
|
237
300
|
}
|
|
238
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Set a callback which is regularly called by the QuickJS engine when it is
|
|
304
|
+
* executing code. This callback can be used to implement an execution
|
|
305
|
+
* timeout: return `true` from the handler to interrupt the running code.
|
|
306
|
+
*
|
|
307
|
+
* This is useful for preventing runaway CPU usage (e.g. infinite loops) in
|
|
308
|
+
* untrusted code.
|
|
309
|
+
*
|
|
310
|
+
* @param cb - The interrupt handler. Return `true` to interrupt execution.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```js
|
|
314
|
+
* // Interrupt evaluation after 1 second
|
|
315
|
+
* const deadline = Date.now() + 1000;
|
|
316
|
+
* arena.setInterruptHandler(() => Date.now() > deadline);
|
|
317
|
+
*
|
|
318
|
+
* try {
|
|
319
|
+
* arena.evalCode(`while (true) {}`);
|
|
320
|
+
* } catch (e) {
|
|
321
|
+
* console.log("Execution interrupted");
|
|
322
|
+
* }
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
setInterruptHandler(cb: InterruptHandler): void {
|
|
326
|
+
this.context.runtime.setInterruptHandler(cb);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Remove the interrupt handler previously set with `setInterruptHandler`.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```js
|
|
334
|
+
* arena.removeInterruptHandler();
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
removeInterruptHandler(): void {
|
|
338
|
+
this.context.runtime.removeInterruptHandler();
|
|
339
|
+
}
|
|
340
|
+
|
|
239
341
|
/**
|
|
240
342
|
* Get detailed memory usage statistics for this runtime.
|
|
241
343
|
*
|
|
@@ -407,6 +509,15 @@ export class Arena {
|
|
|
407
509
|
return !!this._options?.marshalByReference?.(this._unwrap(t));
|
|
408
510
|
};
|
|
409
511
|
|
|
512
|
+
_asyncify = (t: unknown): boolean => {
|
|
513
|
+
const a = this._options?.asyncify;
|
|
514
|
+
if (!a) return false;
|
|
515
|
+
const target = this._unwrap(t);
|
|
516
|
+
if (typeof a === "function") return typeof target === "function" && a(target as any);
|
|
517
|
+
// `a === true`: auto-detect functions declared with `async`.
|
|
518
|
+
return Object.prototype.toString.call(target) === "[object AsyncFunction]";
|
|
519
|
+
};
|
|
520
|
+
|
|
410
521
|
// Register an opaque HostRef handle by its host value without wrapping it.
|
|
411
522
|
_registerHostRef = (t: unknown, handle: QuickJSHandle): QuickJSHandle => {
|
|
412
523
|
const u = this._unwrap(t);
|
|
@@ -491,6 +602,8 @@ export class Arena {
|
|
|
491
602
|
disposeTransient: this._disposeTransient,
|
|
492
603
|
preApply: this._marshalPreApply,
|
|
493
604
|
prepareReturn: this._prepareMarshalReturn,
|
|
605
|
+
unwrap: t => this._unwrap(t),
|
|
606
|
+
asyncify: this._options?.asyncify ? this._asyncify : undefined,
|
|
494
607
|
custom: this._options?.customMarshaller,
|
|
495
608
|
});
|
|
496
609
|
|
|
@@ -499,7 +612,11 @@ export class Arena {
|
|
|
499
612
|
this._transientHandles.delete(handle);
|
|
500
613
|
|
|
501
614
|
const syncEnabled = this._options?.syncEnabled ?? true;
|
|
502
|
-
|
|
615
|
+
// A non-object (primitive) handle is never retained in `_map` (registered
|
|
616
|
+
// primitives already returned above via `_registeredMap`), so skip the
|
|
617
|
+
// `hasHandle` VM roundtrip and mark it disposable directly.
|
|
618
|
+
if (!syncEnabled || !isObject(target)) return [handle, true];
|
|
619
|
+
return [handle, !this._map.hasHandle(handle)];
|
|
503
620
|
};
|
|
504
621
|
|
|
505
622
|
_prepareMarshalReturn = (h: QuickJSHandle): QuickJSHandle => {
|
|
@@ -510,7 +627,12 @@ export class Arena {
|
|
|
510
627
|
// `x === fn()` identity across calls. Hand the VM a dup instead and keep
|
|
511
628
|
// ours alive. With sync off, handles are not retained, so this is a no-op.
|
|
512
629
|
const syncEnabled = this._options?.syncEnabled ?? true;
|
|
513
|
-
|
|
630
|
+
// Only object handles are retained in `_map`; a primitive return can never
|
|
631
|
+
// be in the map, so `isHandleObject` (a cheap typeof) skips the `hasHandle`
|
|
632
|
+
// VM roundtrip for primitive returns from host functions.
|
|
633
|
+
return syncEnabled && h.alive && isHandleObject(this.context, h) && this._map.hasHandle(h)
|
|
634
|
+
? h.dup()
|
|
635
|
+
: h;
|
|
514
636
|
};
|
|
515
637
|
|
|
516
638
|
_preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
|
|
@@ -522,6 +644,14 @@ export class Arena {
|
|
|
522
644
|
};
|
|
523
645
|
|
|
524
646
|
_unmarshal = (handle: QuickJSHandle): any => {
|
|
647
|
+
// Primitives (undefined/number/string/boolean/bigint/null) are resolved by a
|
|
648
|
+
// cheap host-side `ctx.typeof`, skipping the `_registeredMap` VM lookup,
|
|
649
|
+
// HostRef resolution, and `_wrapHandle`. The VMMap only ever keys objects and
|
|
650
|
+
// symbols, so a primitive handle can never match `getByHandle`; symbols fall
|
|
651
|
+
// through `unmarshalPrimitive` and still reach the lookup below.
|
|
652
|
+
const [primitive, ok] = unmarshalPrimitive(this.context, handle);
|
|
653
|
+
if (ok) return primitive;
|
|
654
|
+
|
|
525
655
|
const registered = this._registeredMap.getByHandle(handle);
|
|
526
656
|
if (typeof registered !== "undefined") {
|
|
527
657
|
return registered;
|
|
@@ -533,15 +663,29 @@ export class Arena {
|
|
|
533
663
|
if (ref) return ref.value;
|
|
534
664
|
}
|
|
535
665
|
|
|
536
|
-
const [wrappedHandle] = this._wrapHandle(handle);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
666
|
+
const [wrappedHandle, wrapped] = this._wrapHandle(handle);
|
|
667
|
+
let done = false;
|
|
668
|
+
try {
|
|
669
|
+
const result = unmarshal(wrappedHandle ?? handle, {
|
|
670
|
+
ctx: this.context,
|
|
671
|
+
marshal: this._marshal,
|
|
672
|
+
find: this._unmarshalFind,
|
|
673
|
+
pre: this._preUnmarshal,
|
|
674
|
+
custom: this._options?.customUnmarshaller,
|
|
675
|
+
hostRef: !!this._options?.marshalByReference,
|
|
676
|
+
});
|
|
677
|
+
done = true;
|
|
678
|
+
return result;
|
|
679
|
+
} finally {
|
|
680
|
+
// A mid-flight abort (e.g. an OOM inside the unmarshal chain) can throw
|
|
681
|
+
// before the freshly created proxy handle was registered in the map,
|
|
682
|
+
// orphaning it. Dispose it here; if it was already registered the map
|
|
683
|
+
// skips it (alive check) at dispose. Disposing a handle never allocates,
|
|
684
|
+
// so this is safe even under an active memory limit.
|
|
685
|
+
if (!done && wrapped && wrappedHandle?.alive && wrappedHandle !== handle) {
|
|
686
|
+
wrappedHandle.dispose();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
545
689
|
};
|
|
546
690
|
|
|
547
691
|
_register(
|
|
@@ -555,24 +699,41 @@ export class Arena {
|
|
|
555
699
|
}
|
|
556
700
|
|
|
557
701
|
let wrappedT = this._wrap(t);
|
|
558
|
-
|
|
702
|
+
// `wrappedHNew` is true when `_wrapHandle` allocated a fresh VM proxy (which
|
|
703
|
+
// we own) rather than returning the original handle. If registration fails
|
|
704
|
+
// or throws mid-flight (e.g. an OOM in `map.set`), that proxy would be
|
|
705
|
+
// orphaned, so the finally below disposes it.
|
|
706
|
+
const [wrappedH, wrappedHNew] = this._wrapHandle(h);
|
|
559
707
|
const isPromise = t instanceof Promise;
|
|
560
708
|
if (!wrappedH || (!wrappedT && !isPromise)) return; // t or h is not an object
|
|
561
709
|
if (isPromise) wrappedT = t;
|
|
562
710
|
|
|
563
711
|
const unwrappedT = this._unwrap(t);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
712
|
+
let unwrappedH: QuickJSHandle | undefined;
|
|
713
|
+
let unwrapped = false;
|
|
714
|
+
let done = false;
|
|
715
|
+
try {
|
|
716
|
+
[unwrappedH, unwrapped] = this._unwrapHandle(h);
|
|
717
|
+
|
|
718
|
+
const res = map.set(wrappedT, wrappedH, unwrappedT, unwrappedH);
|
|
719
|
+
if (!res) {
|
|
720
|
+
// already registered
|
|
721
|
+
throw new Error("already registered");
|
|
722
|
+
} else if (sync) {
|
|
723
|
+
this._sync.add(unwrappedT);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
done = true;
|
|
727
|
+
return [wrappedT, wrappedH];
|
|
728
|
+
} finally {
|
|
729
|
+
if (!done) {
|
|
730
|
+
// `map.set` did not take ownership (it threw or returned false): dispose
|
|
731
|
+
// the handles we created so they are not orphaned. A non-new `wrappedH`
|
|
732
|
+
// and a non-wrapped `unwrappedH` are the caller's handle `h`, left alone.
|
|
733
|
+
if (wrappedHNew && wrappedH.alive) wrappedH.dispose();
|
|
734
|
+
if (unwrapped && unwrappedH?.alive) unwrappedH.dispose();
|
|
735
|
+
}
|
|
573
736
|
}
|
|
574
|
-
|
|
575
|
-
return [wrappedT, wrappedH];
|
|
576
737
|
}
|
|
577
738
|
|
|
578
739
|
_syncMode = (obj: any): "both" | undefined => {
|
|
@@ -603,16 +764,7 @@ export class Arena {
|
|
|
603
764
|
};
|
|
604
765
|
|
|
605
766
|
_wrapHandle(handle: QuickJSHandle): [Wrapped<QuickJSHandle> | undefined, boolean] {
|
|
606
|
-
return wrapHandle(
|
|
607
|
-
this.context,
|
|
608
|
-
handle,
|
|
609
|
-
this._symbol,
|
|
610
|
-
this._symbolHandle,
|
|
611
|
-
this._unmarshal,
|
|
612
|
-
this._syncMode,
|
|
613
|
-
this._options?.isHandleWrappable,
|
|
614
|
-
this._options?.syncEnabled ?? true,
|
|
615
|
-
);
|
|
767
|
+
return this._wrapHandleImpl.wrapHandle(handle);
|
|
616
768
|
}
|
|
617
769
|
|
|
618
770
|
_unwrapHandle(target: QuickJSHandle): [QuickJSHandle, boolean] {
|
|
@@ -638,9 +790,19 @@ export class AsyncArena extends Arena {
|
|
|
638
790
|
* Evaluate JS code asynchronously in the VM and get the result on the host
|
|
639
791
|
* side. Like `evalCode`, it converts and re-throws errors thrown during
|
|
640
792
|
* evaluation. Use this when the code awaits host-provided promises.
|
|
793
|
+
*
|
|
794
|
+
* @param code - The JS code to evaluate
|
|
795
|
+
* @param filename - Optional filename used in error stacks and for debugging
|
|
796
|
+
* @param options - Optional eval options forwarded to quickjs-emscripten's
|
|
797
|
+
* `evalCodeAsync`, either an `EvalFlags` bitmask (`number`) or a
|
|
798
|
+
* {@link ContextEvalOptions} object (e.g. `{ strict: true }`)
|
|
641
799
|
*/
|
|
642
|
-
async evalCodeAsync<T = any>(
|
|
643
|
-
|
|
800
|
+
async evalCodeAsync<T = any>(
|
|
801
|
+
code: string,
|
|
802
|
+
filename?: string,
|
|
803
|
+
options?: number | ContextEvalOptions,
|
|
804
|
+
): Promise<T> {
|
|
805
|
+
const result = await this.asyncContext.evalCodeAsync(code, filename, options);
|
|
644
806
|
return this._unwrapResultAndUnmarshal(result);
|
|
645
807
|
}
|
|
646
808
|
}
|
package/src/marshal/custom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { call, consumeAll } from "../vmutil";
|
|
3
|
+
import { call, consume, consumeAll } from "../vmutil";
|
|
4
4
|
|
|
5
5
|
export default function marshalCustom(
|
|
6
6
|
ctx: QuickJSContext,
|
|
@@ -13,24 +13,33 @@ export default function marshalCustom(
|
|
|
13
13
|
handle = c(target, ctx);
|
|
14
14
|
if (handle) break;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
if (!handle) return undefined;
|
|
17
|
+
// Own the custom-marshalled handle until `preMarshal` registers it; dispose it
|
|
18
|
+
// if `preMarshal` throws mid-flight so it is not orphaned.
|
|
19
|
+
let owned = true;
|
|
20
|
+
try {
|
|
21
|
+
const result = preMarshal(target, handle) ?? handle;
|
|
22
|
+
owned = false;
|
|
23
|
+
return result;
|
|
24
|
+
} finally {
|
|
25
|
+
if (owned && handle.alive) handle.dispose();
|
|
26
|
+
}
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export function symbol(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
|
|
20
30
|
if (typeof target !== "symbol") return;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
undefined,
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
return handle;
|
|
31
|
+
// `call` does not dispose its arguments, so the description string handle must
|
|
32
|
+
// be consumed here or it leaks on every symbol marshal.
|
|
33
|
+
return target.description
|
|
34
|
+
? consume(ctx.newString(target.description), d => call(ctx, "d => Symbol(d)", undefined, d))
|
|
35
|
+
: call(ctx, "d => Symbol(d)", undefined, ctx.undefined);
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export function date(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
|
|
31
39
|
if (!(target instanceof Date)) return;
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
// `call` does not dispose its arguments, so the time number handle is consumed
|
|
41
|
+
// here rather than leaked.
|
|
42
|
+
return consume(ctx.newNumber(target.getTime()), d => call(ctx, "d => new Date(d)", undefined, d));
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export function arrayBuffer(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { QuickJSHandle } from "quickjs-emscripten";
|
|
2
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
2
3
|
import { expect, test, vi } from "vitest";
|
|
3
4
|
|
|
4
5
|
import { json, call } from "../vmutil";
|
|
@@ -8,8 +9,10 @@ import marshalFunction from "./function";
|
|
|
8
9
|
test("normal func", async () => {
|
|
9
10
|
const ctx = (await getQuickJS()).newContext();
|
|
10
11
|
|
|
11
|
-
const marshal = vi.fn(v => json(ctx, v));
|
|
12
|
-
const unmarshal = vi.fn(
|
|
12
|
+
const marshal = vi.fn((v) => json(ctx, v));
|
|
13
|
+
const unmarshal = vi.fn((v) =>
|
|
14
|
+
ctx.sameValue(v, ctx.global) ? undefined : ctx.dump(v),
|
|
15
|
+
);
|
|
13
16
|
const preMarshal = vi.fn((_, a) => a);
|
|
14
17
|
const innerfn = vi.fn((..._args: any[]) => "hoge");
|
|
15
18
|
const fn = (...args: any[]) => innerfn(...args);
|
|
@@ -43,7 +46,7 @@ test("normal func", async () => {
|
|
|
43
46
|
|
|
44
47
|
test("func which has properties", async () => {
|
|
45
48
|
const ctx = (await getQuickJS()).newContext();
|
|
46
|
-
const marshal = vi.fn(v => json(ctx, v));
|
|
49
|
+
const marshal = vi.fn((v) => json(ctx, v));
|
|
47
50
|
|
|
48
51
|
const fn = () => {};
|
|
49
52
|
fn.hoge = "foo";
|
|
@@ -52,7 +55,7 @@ test("func which has properties", async () => {
|
|
|
52
55
|
ctx,
|
|
53
56
|
fn,
|
|
54
57
|
marshal,
|
|
55
|
-
v => ctx.dump(v),
|
|
58
|
+
(v) => ctx.dump(v),
|
|
56
59
|
(_, a) => a,
|
|
57
60
|
);
|
|
58
61
|
if (!handle) throw new Error("handle is undefined");
|
|
@@ -93,16 +96,20 @@ test("class", async () => {
|
|
|
93
96
|
if (!handle) throw new Error("handle is undefined");
|
|
94
97
|
|
|
95
98
|
const newA = ctx.unwrapResult(ctx.evalCode(`A => new A(100)`));
|
|
96
|
-
const instance = ctx.unwrapResult(
|
|
99
|
+
const instance = ctx.unwrapResult(
|
|
100
|
+
ctx.callFunction(newA, ctx.undefined, handle),
|
|
101
|
+
);
|
|
97
102
|
|
|
98
103
|
expect(ctx.dump(ctx.getProp(handle, "name"))).toBe("A");
|
|
99
104
|
expect(ctx.dump(ctx.getProp(handle, "length"))).toBe(1);
|
|
100
|
-
expect(
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
expect(
|
|
106
|
+
ctx.dump(
|
|
107
|
+
call(ctx, "(cls, i) => i instanceof cls", undefined, handle, instance),
|
|
108
|
+
),
|
|
109
|
+
).toBe(true);
|
|
103
110
|
expect(ctx.dump(ctx.getProp(instance, "a"))).toBe(100);
|
|
104
111
|
|
|
105
|
-
disposables.forEach(d => d.dispose());
|
|
112
|
+
disposables.forEach((d) => d.dispose());
|
|
106
113
|
instance.dispose();
|
|
107
114
|
newA.dispose();
|
|
108
115
|
handle.dispose();
|
|
@@ -117,19 +124,34 @@ test("preApply", async () => {
|
|
|
117
124
|
if (typeof v === "number") return ctx.newNumber(v);
|
|
118
125
|
return ctx.null;
|
|
119
126
|
};
|
|
120
|
-
const unmarshal = (v: QuickJSHandle) =>
|
|
121
|
-
|
|
127
|
+
const unmarshal = (v: QuickJSHandle) =>
|
|
128
|
+
ctx.typeof(v) === "object" ? that : ctx.dump(v);
|
|
129
|
+
const preApply = vi.fn(
|
|
130
|
+
(a: (...args: any[]) => any, b: any, c: any[]) => a.apply(b, c) + "!",
|
|
131
|
+
);
|
|
122
132
|
const that = {};
|
|
123
133
|
const thatHandle = ctx.newObject();
|
|
124
134
|
|
|
125
135
|
const fn = () => "foo";
|
|
126
|
-
const handle = marshalFunction(
|
|
136
|
+
const handle = marshalFunction(
|
|
137
|
+
ctx,
|
|
138
|
+
fn,
|
|
139
|
+
marshal,
|
|
140
|
+
unmarshal,
|
|
141
|
+
(_, a) => a,
|
|
142
|
+
preApply,
|
|
143
|
+
);
|
|
127
144
|
if (!handle) throw new Error("handle is undefined");
|
|
128
145
|
|
|
129
146
|
expect(preApply).toBeCalledTimes(0);
|
|
130
147
|
|
|
131
148
|
const res = ctx.unwrapResult(
|
|
132
|
-
ctx.callFunction(
|
|
149
|
+
ctx.callFunction(
|
|
150
|
+
handle,
|
|
151
|
+
thatHandle,
|
|
152
|
+
ctx.newNumber(100),
|
|
153
|
+
ctx.newString("hoge"),
|
|
154
|
+
),
|
|
133
155
|
);
|
|
134
156
|
|
|
135
157
|
expect(preApply).toBeCalledTimes(1);
|