quickjs-emscripten-sync 1.8.4 → 1.9.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 +231 -150
- package/dist/index.d.ts +45 -7
- package/dist/quickjs-emscripten-sync.js +573 -342
- package/dist/quickjs-emscripten-sync.umd.cjs +17 -8
- package/package.json +4 -2
- package/src/index.test.ts +250 -5
- package/src/index.ts +85 -7
- package/src/marshal/custom.ts +16 -2
- package/src/marshal/hostref.ts +18 -0
- package/src/marshal/index.ts +12 -0
- package/src/marshal/mapset.ts +28 -0
- package/src/marshal/primitive.ts +2 -11
- package/src/marshal/properties.ts +8 -6
- package/src/unmarshal/custom.ts +35 -4
- package/src/unmarshal/function.ts +2 -2
- package/src/unmarshal/hostref.ts +21 -0
- package/src/unmarshal/index.ts +21 -1
- package/src/unmarshal/mapset.ts +43 -0
- package/src/unmarshal/object.ts +16 -20
- package/src/unmarshal/primitive.ts +6 -17
- package/src/unmarshal/properties.ts +7 -6
- package/src/vmmap.ts +59 -69
- package/src/vmutil.ts +94 -11
- package/src/wrapper.ts +97 -30
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
QuickJSDeferredPromise,
|
|
3
3
|
QuickJSHandle,
|
|
4
4
|
QuickJSContext,
|
|
5
|
+
QuickJSAsyncContext,
|
|
5
6
|
SuccessOrFail,
|
|
6
7
|
VmCallResult,
|
|
7
8
|
Intrinsics,
|
|
@@ -11,9 +12,20 @@ import { wrapContext, QuickJSContextEx } from "./contextex";
|
|
|
11
12
|
import { defaultRegisteredObjects } from "./default";
|
|
12
13
|
import marshal from "./marshal";
|
|
13
14
|
import unmarshal from "./unmarshal";
|
|
15
|
+
import unmarshalHostRef from "./unmarshal/hostref";
|
|
14
16
|
import { complexity, isES2015Class, isObject, walkObject } from "./util";
|
|
15
17
|
import VMMap from "./vmmap";
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
call,
|
|
20
|
+
isHandleObject,
|
|
21
|
+
json,
|
|
22
|
+
consume,
|
|
23
|
+
consumeAll,
|
|
24
|
+
mayConsume,
|
|
25
|
+
handleFrom,
|
|
26
|
+
enableFnCache,
|
|
27
|
+
disposeFnCache,
|
|
28
|
+
} from "./vmutil";
|
|
17
29
|
import { wrap, wrapHandle, unwrap, unwrapHandle, Wrapped } from "./wrapper";
|
|
18
30
|
|
|
19
31
|
export {
|
|
@@ -53,6 +65,10 @@ export type Options = {
|
|
|
53
65
|
compat?: boolean;
|
|
54
66
|
/** Experimental: use QuickJSContextEx, which wraps existing QuickJSContext. */
|
|
55
67
|
experimentalContextEx?: boolean;
|
|
68
|
+
/** Globally enable sync mode (default `true`). When `false`, objects are not wrapped with proxies and marshalled handles are disposed after use, so `arena.sync` has no effect but objects are not retained for their whole lifetime. Useful to avoid memory growth when frequently exchanging short-lived objects. */
|
|
69
|
+
syncEnabled?: boolean;
|
|
70
|
+
/** 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
|
+
marshalByReference?: (target: any) => boolean;
|
|
56
72
|
};
|
|
57
73
|
|
|
58
74
|
/**
|
|
@@ -80,6 +96,7 @@ export class Arena {
|
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
this.context = options?.experimentalContextEx ? wrapContext(ctx) : ctx;
|
|
99
|
+
enableFnCache(this.context);
|
|
83
100
|
this._options = options;
|
|
84
101
|
this._symbolHandle = ctx.unwrapResult(ctx.evalCode(`Symbol()`));
|
|
85
102
|
this._map = new VMMap(ctx);
|
|
@@ -94,9 +111,15 @@ export class Arena {
|
|
|
94
111
|
this._map.dispose();
|
|
95
112
|
this._registeredMap.dispose();
|
|
96
113
|
this._symbolHandle.dispose();
|
|
114
|
+
disposeFnCache(this.context);
|
|
97
115
|
this.context.disposeEx?.();
|
|
98
116
|
}
|
|
99
117
|
|
|
118
|
+
/** Allows `using arena = new Arena(...)` to dispose the arena automatically. */
|
|
119
|
+
[Symbol.dispose]() {
|
|
120
|
+
this.dispose();
|
|
121
|
+
}
|
|
122
|
+
|
|
100
123
|
/**
|
|
101
124
|
* 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.
|
|
102
125
|
*/
|
|
@@ -150,7 +173,7 @@ export class Arena {
|
|
|
150
173
|
if ("value" in result) {
|
|
151
174
|
return result.value;
|
|
152
175
|
}
|
|
153
|
-
throw this._unwrapIfNotSynced(result.error
|
|
176
|
+
throw this._unwrapIfNotSynced(consume(result.error, this._unmarshal));
|
|
154
177
|
}
|
|
155
178
|
|
|
156
179
|
/**
|
|
@@ -355,12 +378,12 @@ export class Arena {
|
|
|
355
378
|
if ("value" in result) {
|
|
356
379
|
return result.value;
|
|
357
380
|
}
|
|
358
|
-
throw this._unwrapIfNotSynced(result.error
|
|
381
|
+
throw this._unwrapIfNotSynced(consume(result.error, this._unmarshal));
|
|
359
382
|
}
|
|
360
383
|
|
|
361
384
|
_unwrapResultAndUnmarshal(result: VmCallResult<QuickJSHandle> | undefined): any {
|
|
362
385
|
if (!result) return;
|
|
363
|
-
return this._unwrapIfNotSynced(this._unwrapResult(result)
|
|
386
|
+
return this._unwrapIfNotSynced(consume(this._unwrapResult(result), this._unmarshal));
|
|
364
387
|
}
|
|
365
388
|
|
|
366
389
|
_isMarshalable = (t: unknown): boolean | "json" => {
|
|
@@ -368,6 +391,19 @@ export class Arena {
|
|
|
368
391
|
return (typeof im === "function" ? im(this._unwrap(t)) : im) ?? "json";
|
|
369
392
|
};
|
|
370
393
|
|
|
394
|
+
_marshalByReference = (t: unknown): boolean => {
|
|
395
|
+
return !!this._options?.marshalByReference?.(this._unwrap(t));
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Register an opaque HostRef handle by its host value without wrapping it.
|
|
399
|
+
_registerHostRef = (t: unknown, handle: QuickJSHandle): QuickJSHandle => {
|
|
400
|
+
const u = this._unwrap(t);
|
|
401
|
+
const existing = this._map.get(u);
|
|
402
|
+
if (existing) return existing;
|
|
403
|
+
this._map.set(u, handle);
|
|
404
|
+
return handle;
|
|
405
|
+
};
|
|
406
|
+
|
|
371
407
|
_marshalFind = (t: unknown) => {
|
|
372
408
|
const unwrappedT = this._unwrap(t);
|
|
373
409
|
const handle =
|
|
@@ -405,21 +441,29 @@ export class Arena {
|
|
|
405
441
|
return [registered, false];
|
|
406
442
|
}
|
|
407
443
|
|
|
408
|
-
|
|
444
|
+
// Pass-by-reference objects must reference the original, not a proxy wrapper.
|
|
445
|
+
const marshalTarget = this._marshalByReference(target)
|
|
446
|
+
? this._unwrap(target)
|
|
447
|
+
: this._wrap(target) ?? target;
|
|
448
|
+
|
|
449
|
+
const handle = marshal(marshalTarget, {
|
|
409
450
|
ctx: this.context,
|
|
410
451
|
unmarshal: this._unmarshal,
|
|
411
452
|
isMarshalable: this._isMarshalable,
|
|
453
|
+
marshalByReference: this._marshalByReference,
|
|
454
|
+
registerHostRef: this._registerHostRef,
|
|
412
455
|
find: this._marshalFind,
|
|
413
456
|
pre: this._marshalPre,
|
|
414
457
|
preApply: this._marshalPreApply,
|
|
415
458
|
custom: this._options?.customMarshaller,
|
|
416
459
|
});
|
|
417
460
|
|
|
418
|
-
|
|
461
|
+
const syncEnabled = this._options?.syncEnabled ?? true;
|
|
462
|
+
return [handle, !syncEnabled || !this._map.hasHandle(handle)];
|
|
419
463
|
};
|
|
420
464
|
|
|
421
465
|
_preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
|
|
422
|
-
return this._register(t, h, undefined, true)?.[0];
|
|
466
|
+
return this._register(t, h, undefined, this._options?.syncEnabled ?? true)?.[0];
|
|
423
467
|
};
|
|
424
468
|
|
|
425
469
|
_unmarshalFind = (h: QuickJSHandle): unknown => {
|
|
@@ -432,6 +476,12 @@ export class Arena {
|
|
|
432
476
|
return registered;
|
|
433
477
|
}
|
|
434
478
|
|
|
479
|
+
// Resolve opaque HostRefs before wrapping, since a proxy would hide them.
|
|
480
|
+
if (this._options?.marshalByReference) {
|
|
481
|
+
const ref = unmarshalHostRef(this.context, handle);
|
|
482
|
+
if (ref) return ref.value;
|
|
483
|
+
}
|
|
484
|
+
|
|
435
485
|
const [wrappedHandle] = this._wrapHandle(handle);
|
|
436
486
|
return unmarshal(wrappedHandle ?? handle, {
|
|
437
487
|
ctx: this.context,
|
|
@@ -439,6 +489,7 @@ export class Arena {
|
|
|
439
489
|
find: this._unmarshalFind,
|
|
440
490
|
pre: this._preUnmarshal,
|
|
441
491
|
custom: this._options?.customUnmarshaller,
|
|
492
|
+
hostRef: !!this._options?.marshalByReference,
|
|
442
493
|
});
|
|
443
494
|
};
|
|
444
495
|
|
|
@@ -487,6 +538,7 @@ export class Arena {
|
|
|
487
538
|
this._marshal,
|
|
488
539
|
this._syncMode,
|
|
489
540
|
this._options?.isWrappable,
|
|
541
|
+
this._options?.syncEnabled ?? true,
|
|
490
542
|
);
|
|
491
543
|
}
|
|
492
544
|
|
|
@@ -508,6 +560,7 @@ export class Arena {
|
|
|
508
560
|
this._unmarshal,
|
|
509
561
|
this._syncMode,
|
|
510
562
|
this._options?.isHandleWrappable,
|
|
563
|
+
this._options?.syncEnabled ?? true,
|
|
511
564
|
);
|
|
512
565
|
}
|
|
513
566
|
|
|
@@ -515,3 +568,28 @@ export class Arena {
|
|
|
515
568
|
return unwrapHandle(this.context, target, this._symbolHandle);
|
|
516
569
|
}
|
|
517
570
|
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* An Arena backed by a {@link QuickJSAsyncContext}. In addition to everything
|
|
574
|
+
* `Arena` offers, it can evaluate code asynchronously with `evalCodeAsync`,
|
|
575
|
+
* which lets the VM await host promises (e.g. async module loaders or async
|
|
576
|
+
* functions exposed from the host) without manually pumping pending jobs.
|
|
577
|
+
*/
|
|
578
|
+
export class AsyncArena extends Arena {
|
|
579
|
+
asyncContext: QuickJSAsyncContext;
|
|
580
|
+
|
|
581
|
+
constructor(ctx: QuickJSAsyncContext, options?: Options) {
|
|
582
|
+
super(ctx, options);
|
|
583
|
+
this.asyncContext = ctx;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Evaluate JS code asynchronously in the VM and get the result on the host
|
|
588
|
+
* side. Like `evalCode`, it converts and re-throws errors thrown during
|
|
589
|
+
* evaluation. Use this when the code awaits host-provided promises.
|
|
590
|
+
*/
|
|
591
|
+
async evalCodeAsync<T = any>(code: string, filename?: string): Promise<T> {
|
|
592
|
+
const result = await this.asyncContext.evalCodeAsync(code, filename);
|
|
593
|
+
return this._unwrapResultAndUnmarshal(result);
|
|
594
|
+
}
|
|
595
|
+
}
|
package/src/marshal/custom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { call } from "../vmutil";
|
|
3
|
+
import { call, consumeAll } from "../vmutil";
|
|
4
4
|
|
|
5
5
|
export default function marshalCustom(
|
|
6
6
|
ctx: QuickJSContext,
|
|
@@ -33,4 +33,18 @@ export function date(target: unknown, ctx: QuickJSContext): QuickJSHandle | unde
|
|
|
33
33
|
return handle;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export
|
|
36
|
+
export function arrayBuffer(target: unknown, ctx: QuickJSContext): QuickJSHandle | undefined {
|
|
37
|
+
if (target instanceof ArrayBuffer) {
|
|
38
|
+
return ctx.newArrayBuffer(target.slice(0));
|
|
39
|
+
}
|
|
40
|
+
if (ArrayBuffer.isView(target)) {
|
|
41
|
+
// TypedArray or DataView: copy the viewed bytes and rebuild in the VM.
|
|
42
|
+
const bytes = new Uint8Array(target.buffer, target.byteOffset, target.byteLength).slice();
|
|
43
|
+
return consumeAll(
|
|
44
|
+
[ctx.newArrayBuffer(bytes.buffer), ctx.newString(target.constructor.name)],
|
|
45
|
+
([buf, name]) => call(ctx, `(buf, name) => new globalThis[name](buf)`, undefined, buf, name),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const defaultCustom = [symbol, date, arrayBuffer];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Marshal a host object as an opaque HostRef handle instead of deep-copying or
|
|
5
|
+
* proxying it. The guest cannot read the object, but can hold it and pass it
|
|
6
|
+
* back to the host, where it unmarshals to the original object by reference.
|
|
7
|
+
*/
|
|
8
|
+
export default function marshalHostRef(
|
|
9
|
+
ctx: QuickJSContext,
|
|
10
|
+
target: unknown,
|
|
11
|
+
// Registers handle <-> target without wrapping (a proxy would hide the opaque
|
|
12
|
+
// HostRef). Keeps identity and lets the arena dispose the handle on teardown.
|
|
13
|
+
register: (target: unknown, handle: QuickJSHandle) => QuickJSHandle,
|
|
14
|
+
): QuickJSHandle | undefined {
|
|
15
|
+
if (typeof target !== "object" && typeof target !== "function") return;
|
|
16
|
+
if (target === null) return;
|
|
17
|
+
return register(target, ctx.newHostRef(target as object).handle);
|
|
18
|
+
}
|
package/src/marshal/index.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { QuickJSDeferredPromise, QuickJSHandle, QuickJSContext } from "quic
|
|
|
2
2
|
|
|
3
3
|
import marshalCustom, { defaultCustom } from "./custom";
|
|
4
4
|
import marshalFunction from "./function";
|
|
5
|
+
import marshalHostRef from "./hostref";
|
|
5
6
|
import marshalJSON from "./json";
|
|
7
|
+
import marshalMapSet from "./mapset";
|
|
6
8
|
import marshalObject from "./object";
|
|
7
9
|
import marshalPrimitive from "./primitive";
|
|
8
10
|
import marshalPromise from "./promise";
|
|
@@ -11,6 +13,8 @@ export type Options = {
|
|
|
11
13
|
ctx: QuickJSContext;
|
|
12
14
|
unmarshal: (handle: QuickJSHandle) => unknown;
|
|
13
15
|
isMarshalable?: (target: unknown) => boolean | "json";
|
|
16
|
+
marshalByReference?: (target: unknown) => boolean;
|
|
17
|
+
registerHostRef?: (target: unknown, handle: QuickJSHandle) => QuickJSHandle;
|
|
14
18
|
find: (target: unknown) => QuickJSHandle | undefined;
|
|
15
19
|
pre: (
|
|
16
20
|
target: unknown,
|
|
@@ -36,6 +40,13 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {
|
|
|
36
40
|
if (handle) return handle;
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
// Opt-in pass-by-reference: hand the object to the VM as an opaque HostRef
|
|
44
|
+
// instead of marshalling its contents. Bypasses isMarshalable on purpose.
|
|
45
|
+
if (options.marshalByReference?.(target) && options.registerHostRef) {
|
|
46
|
+
const handle = marshalHostRef(ctx, target, options.registerHostRef);
|
|
47
|
+
if (handle) return handle;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
const marshalable = isMarshalable?.(target);
|
|
40
51
|
if (marshalable === false) {
|
|
41
52
|
return ctx.undefined;
|
|
@@ -52,6 +63,7 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {
|
|
|
52
63
|
marshalCustom(ctx, target, pre2, [...defaultCustom, ...(options.custom ?? [])]) ??
|
|
53
64
|
marshalPromise(ctx, target, marshal2, pre2) ??
|
|
54
65
|
marshalFunction(ctx, target, marshal2, unmarshal, pre2, options.preApply) ??
|
|
66
|
+
marshalMapSet(ctx, target, marshal2, pre2) ??
|
|
55
67
|
marshalObject(ctx, target, marshal2, pre2) ??
|
|
56
68
|
ctx.undefined
|
|
57
69
|
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
|
+
|
|
3
|
+
import { call } from "../vmutil";
|
|
4
|
+
|
|
5
|
+
export default function marshalMapSet(
|
|
6
|
+
ctx: QuickJSContext,
|
|
7
|
+
target: unknown,
|
|
8
|
+
marshal: (target: unknown) => QuickJSHandle,
|
|
9
|
+
preMarshal: (target: unknown, handle: QuickJSHandle) => QuickJSHandle | undefined,
|
|
10
|
+
): QuickJSHandle | undefined {
|
|
11
|
+
if (target instanceof Map) {
|
|
12
|
+
const raw = call(ctx, "() => new Map()");
|
|
13
|
+
const handle = preMarshal(target, raw) ?? raw;
|
|
14
|
+
for (const [k, v] of target) {
|
|
15
|
+
call(ctx, "(m, k, v) => m.set(k, v)", undefined, raw, marshal(k), marshal(v)).dispose();
|
|
16
|
+
}
|
|
17
|
+
return handle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (target instanceof Set) {
|
|
21
|
+
const raw = call(ctx, "() => new Set()");
|
|
22
|
+
const handle = preMarshal(target, raw) ?? raw;
|
|
23
|
+
for (const v of target) {
|
|
24
|
+
call(ctx, "(s, v) => s.add(v)", undefined, raw, marshal(v)).dispose();
|
|
25
|
+
}
|
|
26
|
+
return handle;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/marshal/primitive.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
// import { call } from "../vmutil";
|
|
4
|
-
|
|
5
3
|
export default function marshalPrimitive(
|
|
6
4
|
ctx: QuickJSContext,
|
|
7
5
|
target: unknown,
|
|
@@ -15,17 +13,10 @@ export default function marshalPrimitive(
|
|
|
15
13
|
return ctx.newString(target);
|
|
16
14
|
case "boolean":
|
|
17
15
|
return target ? ctx.true : ctx.false;
|
|
16
|
+
case "bigint":
|
|
17
|
+
return ctx.newBigInt(target);
|
|
18
18
|
case "object":
|
|
19
19
|
return target === null ? ctx.null : undefined;
|
|
20
|
-
|
|
21
|
-
// BigInt is not supported by quickjs-emscripten
|
|
22
|
-
// case "bigint":
|
|
23
|
-
// return call(
|
|
24
|
-
// ctx,
|
|
25
|
-
// `s => BigInt(s)`,
|
|
26
|
-
// undefined,
|
|
27
|
-
// ctx.newString(target.toString())
|
|
28
|
-
// );
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
return undefined;
|
|
@@ -35,11 +35,13 @@ export default function marshalProperties(
|
|
|
35
35
|
});
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
try {
|
|
39
|
+
const desc = Object.getOwnPropertyDescriptors(target);
|
|
40
|
+
Object.entries(desc).forEach(([k, v]) => cb(k, v));
|
|
41
|
+
Object.getOwnPropertySymbols(desc).forEach(k => cb(k, (desc as any)[k]));
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
call(ctx, `Object.defineProperties`, undefined, handle, descs).dispose();
|
|
44
|
+
} finally {
|
|
45
|
+
descs.dispose();
|
|
46
|
+
}
|
|
45
47
|
}
|
package/src/unmarshal/custom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { call } from "../vmutil";
|
|
3
|
+
import { call, consume } from "../vmutil";
|
|
4
4
|
|
|
5
5
|
export default function unmarshalCustom(
|
|
6
6
|
ctx: QuickJSContext,
|
|
@@ -23,9 +23,40 @@ export function symbol(handle: QuickJSHandle, ctx: QuickJSContext): symbol | und
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function date(handle: QuickJSHandle, ctx: QuickJSContext): Date | undefined {
|
|
26
|
-
if (!
|
|
27
|
-
const t =
|
|
26
|
+
if (!consume(call(ctx, "a => a instanceof Date", undefined, handle), h => ctx.dump(h))) return;
|
|
27
|
+
const t = consume(call(ctx, "a => a.getTime()", undefined, handle), h => ctx.getNumber(h));
|
|
28
28
|
return new Date(t);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export
|
|
31
|
+
export function arrayBuffer(
|
|
32
|
+
handle: QuickJSHandle,
|
|
33
|
+
ctx: QuickJSContext,
|
|
34
|
+
): ArrayBuffer | ArrayBufferView | undefined {
|
|
35
|
+
if (consume(call(ctx, "a => a instanceof ArrayBuffer", undefined, handle), h => ctx.dump(h))) {
|
|
36
|
+
const lifetime = ctx.getArrayBuffer(handle);
|
|
37
|
+
const copy = lifetime.value.slice();
|
|
38
|
+
lifetime.dispose();
|
|
39
|
+
return copy.buffer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (consume(call(ctx, "a => ArrayBuffer.isView(a)", undefined, handle), h => ctx.dump(h))) {
|
|
43
|
+
const name = consume(call(ctx, "a => a.constructor.name", undefined, handle), h =>
|
|
44
|
+
ctx.getString(h),
|
|
45
|
+
);
|
|
46
|
+
const Ctor = (globalThis as any)[name];
|
|
47
|
+
if (typeof Ctor !== "function") return;
|
|
48
|
+
const bufHandle = call(
|
|
49
|
+
ctx,
|
|
50
|
+
"a => a.buffer.slice(a.byteOffset, a.byteOffset + a.byteLength)",
|
|
51
|
+
undefined,
|
|
52
|
+
handle,
|
|
53
|
+
);
|
|
54
|
+
const lifetime = ctx.getArrayBuffer(bufHandle);
|
|
55
|
+
const bytes = lifetime.value.slice();
|
|
56
|
+
lifetime.dispose();
|
|
57
|
+
bufHandle.dispose();
|
|
58
|
+
return new Ctor(bytes.buffer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const defaultCustom = [symbol, date, arrayBuffer];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { call, mayConsumeAll } from "../vmutil";
|
|
3
|
+
import { call, mayConsumeAll, unwrapResult } from "../vmutil";
|
|
4
4
|
|
|
5
5
|
import unmarshalProperties from "./properties";
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ export default function unmarshalFunction(
|
|
|
26
26
|
return this;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const resultHandle =
|
|
29
|
+
const resultHandle = unwrapResult(ctx, ctx.callFunction(handle, thisHandle, ...argHandles));
|
|
30
30
|
|
|
31
31
|
const [result, alreadyExists] = unmarshal(resultHandle);
|
|
32
32
|
if (alreadyExists) resultHandle.dispose();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* If `handle` is an opaque HostRef, resolve it back to the original host value.
|
|
5
|
+
* Returns a wrapper so the caller can tell "resolved to a falsy value" apart
|
|
6
|
+
* from "not a HostRef". `toHostRef` is cheap for non-HostRef handles.
|
|
7
|
+
*/
|
|
8
|
+
export default function unmarshalHostRef(
|
|
9
|
+
ctx: QuickJSContext,
|
|
10
|
+
handle: QuickJSHandle,
|
|
11
|
+
): { value: unknown } | undefined {
|
|
12
|
+
const ref = ctx.toHostRef(handle);
|
|
13
|
+
if (!ref) return;
|
|
14
|
+
try {
|
|
15
|
+
return { value: ref.value };
|
|
16
|
+
} finally {
|
|
17
|
+
// toHostRef returns a HostRef wrapping a *dup* of the handle, so disposing
|
|
18
|
+
// it does not affect the caller's handle.
|
|
19
|
+
ref.dispose();
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/unmarshal/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
|
2
2
|
|
|
3
3
|
import unmarshalCustom, { defaultCustom } from "./custom";
|
|
4
4
|
import unmarshalFunction from "./function";
|
|
5
|
+
import unmarshalHostRef from "./hostref";
|
|
6
|
+
import unmarshalMapSet from "./mapset";
|
|
5
7
|
import unmarshalObject from "./object";
|
|
6
8
|
import unmarshalPrimitive from "./primitive";
|
|
7
9
|
import unmarshalPromise from "./promise";
|
|
@@ -13,6 +15,8 @@ export type Options = {
|
|
|
13
15
|
find: (handle: QuickJSHandle) => unknown | undefined;
|
|
14
16
|
pre: <T = unknown>(target: T, handle: QuickJSHandle) => T | undefined;
|
|
15
17
|
custom?: Iterable<(obj: QuickJSHandle, ctx: QuickJSContext) => any>;
|
|
18
|
+
/** When true, resolve opaque HostRef handles back to their host value. */
|
|
19
|
+
hostRef?: boolean;
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
export function unmarshal(handle: QuickJSHandle, options: Options): any {
|
|
@@ -37,8 +41,24 @@ function unmarshalInner(handle: QuickJSHandle, options: Options): [any, boolean]
|
|
|
37
41
|
|
|
38
42
|
const unmarshal2 = (h: QuickJSHandle) => unmarshalInner(h, options);
|
|
39
43
|
|
|
44
|
+
// Opaque HostRef handles resolve back to the original host value by reference.
|
|
45
|
+
if (options.hostRef) {
|
|
46
|
+
const ref = unmarshalHostRef(ctx, handle);
|
|
47
|
+
if (ref) return [ref.value, true];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Custom types (Symbol, Date, ArrayBuffer, TypedArray, ...) are unmarshalled
|
|
51
|
+
// by value and not tracked in the map, so their source handle is not owned by
|
|
52
|
+
// anyone else and the caller must dispose it.
|
|
53
|
+
const custom = unmarshalCustom(ctx, handle, pre, [...defaultCustom, ...(options.custom ?? [])]);
|
|
54
|
+
if (custom) return [custom, true];
|
|
55
|
+
|
|
56
|
+
// Map/Set are unmarshalled by value (snapshot copy), so the source handle is
|
|
57
|
+
// not tracked and the caller must dispose it.
|
|
58
|
+
const mapSet = unmarshalMapSet(ctx, handle, unmarshal2, pre);
|
|
59
|
+
if (mapSet) return [mapSet, true];
|
|
60
|
+
|
|
40
61
|
const result =
|
|
41
|
-
unmarshalCustom(ctx, handle, pre, [...defaultCustom, ...(options.custom ?? [])]) ??
|
|
42
62
|
unmarshalPromise(ctx, handle, marshal, pre) ??
|
|
43
63
|
unmarshalFunction(ctx, handle, marshal, unmarshal2, pre) ??
|
|
44
64
|
unmarshalObject(ctx, handle, unmarshal2, pre);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
|
+
|
|
3
|
+
import { call, consume } from "../vmutil";
|
|
4
|
+
|
|
5
|
+
export default function unmarshalMapSet(
|
|
6
|
+
ctx: QuickJSContext,
|
|
7
|
+
handle: QuickJSHandle,
|
|
8
|
+
unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
|
|
9
|
+
preUnmarshal: <T>(target: T, handle: QuickJSHandle) => T | undefined,
|
|
10
|
+
): Map<any, any> | Set<any> | undefined {
|
|
11
|
+
const isMap = consume(call(ctx, "a => a instanceof Map", undefined, handle), h => ctx.dump(h));
|
|
12
|
+
const isSet =
|
|
13
|
+
!isMap && consume(call(ctx, "a => a instanceof Set", undefined, handle), h => ctx.dump(h));
|
|
14
|
+
if (!isMap && !isSet) return;
|
|
15
|
+
|
|
16
|
+
const result: Map<any, any> | Set<any> = isMap ? new Map() : new Set();
|
|
17
|
+
preUnmarshal(result, handle);
|
|
18
|
+
|
|
19
|
+
const iterator = ctx.unwrapResult(ctx.getIterator(handle));
|
|
20
|
+
try {
|
|
21
|
+
for (const elResult of iterator) {
|
|
22
|
+
const el = ctx.unwrapResult(elResult);
|
|
23
|
+
if (isMap) {
|
|
24
|
+
const keyHandle = ctx.getProp(el, 0);
|
|
25
|
+
const valueHandle = ctx.getProp(el, 1);
|
|
26
|
+
const [key, disposeKey] = unmarshal(keyHandle);
|
|
27
|
+
const [value, disposeValue] = unmarshal(valueHandle);
|
|
28
|
+
if (disposeKey) keyHandle.dispose();
|
|
29
|
+
if (disposeValue) valueHandle.dispose();
|
|
30
|
+
(result as Map<any, any>).set(key, value);
|
|
31
|
+
el.dispose();
|
|
32
|
+
} else {
|
|
33
|
+
const [value, disposeValue] = unmarshal(el);
|
|
34
|
+
if (disposeValue) el.dispose();
|
|
35
|
+
(result as Set<any>).add(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
if (iterator.alive) iterator.dispose();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
package/src/unmarshal/object.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { call } from "../vmutil";
|
|
3
|
+
import { call, consume } from "../vmutil";
|
|
4
4
|
|
|
5
5
|
import unmarshalProperties from "./properties";
|
|
6
6
|
|
|
@@ -10,31 +10,27 @@ export default function unmarshalObject(
|
|
|
10
10
|
unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
|
|
11
11
|
preUnmarshal: <T>(target: T, handle: QuickJSHandle) => T | undefined,
|
|
12
12
|
): object | undefined {
|
|
13
|
-
if (
|
|
14
|
-
ctx.typeof(handle) !== "object" ||
|
|
15
|
-
// null check
|
|
16
|
-
ctx
|
|
17
|
-
.unwrapResult(ctx.evalCode("o => o === null"))
|
|
18
|
-
.consume(n => ctx.dump(ctx.unwrapResult(ctx.callFunction(n, ctx.undefined, handle))))
|
|
19
|
-
)
|
|
20
|
-
return;
|
|
13
|
+
if (ctx.typeof(handle) !== "object" || ctx.sameValue(handle, ctx.null)) return;
|
|
21
14
|
|
|
22
|
-
const raw = call(ctx, "Array.isArray", undefined, handle)
|
|
15
|
+
const raw = consume(call(ctx, "Array.isArray", undefined, handle), r => ctx.dump(r)) ? [] : {};
|
|
23
16
|
const obj = preUnmarshal(raw, handle) ?? raw;
|
|
24
17
|
|
|
25
|
-
const prototype =
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const prototype = consume(
|
|
19
|
+
call(
|
|
20
|
+
ctx,
|
|
21
|
+
`o => {
|
|
28
22
|
const p = Object.getPrototypeOf(o);
|
|
29
23
|
return !p || p === Object.prototype || p === Array.prototype ? undefined : p;
|
|
30
24
|
}`,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
undefined,
|
|
26
|
+
handle,
|
|
27
|
+
),
|
|
28
|
+
prototype => {
|
|
29
|
+
if (ctx.typeof(prototype) === "undefined") return;
|
|
30
|
+
const [proto] = unmarshal(prototype);
|
|
31
|
+
return proto;
|
|
32
|
+
},
|
|
33
|
+
);
|
|
38
34
|
if (typeof prototype === "object") {
|
|
39
35
|
Object.setPrototypeOf(obj, prototype);
|
|
40
36
|
}
|
|
@@ -7,24 +7,13 @@ export default function unmarshalPrimitive(
|
|
|
7
7
|
const ty = ctx.typeof(handle);
|
|
8
8
|
if (ty === "undefined" || ty === "number" || ty === "string" || ty === "boolean") {
|
|
9
9
|
return [ctx.dump(handle), true];
|
|
10
|
-
} else if (ty === "object") {
|
|
11
|
-
const isNull = ctx
|
|
12
|
-
.unwrapResult(ctx.evalCode("a => a === null"))
|
|
13
|
-
.consume(n => ctx.dump(ctx.unwrapResult(ctx.callFunction(n, ctx.undefined, handle))));
|
|
14
|
-
if (isNull) {
|
|
15
|
-
return [null, true];
|
|
16
|
-
}
|
|
17
10
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// .consume(str => ctx.getString(str));
|
|
25
|
-
// const bi = BigInt(str);
|
|
26
|
-
// return [bi, true];
|
|
27
|
-
// }
|
|
11
|
+
if (ty === "bigint") {
|
|
12
|
+
return [ctx.getBigInt(handle), true];
|
|
13
|
+
}
|
|
14
|
+
if (ty === "object" && ctx.sameValue(handle, ctx.null)) {
|
|
15
|
+
return [null, true];
|
|
16
|
+
}
|
|
28
17
|
|
|
29
18
|
return [undefined, false];
|
|
30
19
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { call } from "../vmutil";
|
|
3
|
+
import { call, consume } from "../vmutil";
|
|
4
4
|
|
|
5
5
|
export default function unmarshalProperties(
|
|
6
6
|
ctx: QuickJSContext,
|
|
@@ -8,8 +8,8 @@ export default function unmarshalProperties(
|
|
|
8
8
|
target: object | ((...args: any[]) => any),
|
|
9
9
|
unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
|
|
10
10
|
) {
|
|
11
|
-
|
|
12
|
-
.newFunction("", (key, value) => {
|
|
11
|
+
consume(
|
|
12
|
+
ctx.newFunction("", (key, value) => {
|
|
13
13
|
const [keyName] = unmarshal(key);
|
|
14
14
|
if (typeof keyName !== "string" && typeof keyName !== "number" && typeof keyName !== "symbol")
|
|
15
15
|
return;
|
|
@@ -43,8 +43,8 @@ export default function unmarshalProperties(
|
|
|
43
43
|
}, {});
|
|
44
44
|
|
|
45
45
|
Object.defineProperty(target, keyName, desc);
|
|
46
|
-
})
|
|
47
|
-
|
|
46
|
+
}),
|
|
47
|
+
fn => {
|
|
48
48
|
call(
|
|
49
49
|
ctx,
|
|
50
50
|
`(o, fn) => {
|
|
@@ -56,5 +56,6 @@ export default function unmarshalProperties(
|
|
|
56
56
|
handle,
|
|
57
57
|
fn,
|
|
58
58
|
).dispose();
|
|
59
|
-
}
|
|
59
|
+
},
|
|
60
|
+
);
|
|
60
61
|
}
|