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/src/marshal/function.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
1
|
+
import type { QuickJSAsyncContext, QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
3
|
import { isES2015Class, isObject } from "../util";
|
|
4
|
-
import { call } from "../vmutil";
|
|
4
|
+
import { call, consume } from "../vmutil";
|
|
5
5
|
|
|
6
6
|
import marshalProperties from "./properties";
|
|
7
7
|
|
|
@@ -13,45 +13,111 @@ export default function marshalFunction(
|
|
|
13
13
|
preMarshal: (target: unknown, handle: QuickJSHandle) => QuickJSHandle | undefined,
|
|
14
14
|
preApply?: (target: (...args: any[]) => any, thisArg: unknown, args: unknown[]) => any,
|
|
15
15
|
disposeTransient: (handle: QuickJSHandle) => void = () => {},
|
|
16
|
+
prepareReturn: (handle: QuickJSHandle) => QuickJSHandle = h => h,
|
|
17
|
+
unwrap: (target: unknown) => unknown = t => t,
|
|
18
|
+
asyncify?: (target: unknown) => boolean,
|
|
16
19
|
): QuickJSHandle | undefined {
|
|
17
20
|
if (typeof target !== "function") return;
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
22
|
+
// `target` may be a host-side proxy wrapper; unwrap it before the class check,
|
|
23
|
+
// because Function.prototype.toString on a callable proxy never matches /^class/.
|
|
24
|
+
// Computed once here rather than per call to avoid the toString + regex on every
|
|
25
|
+
// VM→host invocation.
|
|
26
|
+
const unwrapped = unwrap(target);
|
|
27
|
+
const isClass = isES2015Class(unwrapped);
|
|
28
|
+
|
|
29
|
+
// Marshal as an Asyncified function only when the caller opts in for this
|
|
30
|
+
// target AND the context actually supports it (a plain sync context has no
|
|
31
|
+
// `newAsyncifiedFunction`, so fall back to the normal function marshalling,
|
|
32
|
+
// which hands the guest a marshalled Promise as before).
|
|
33
|
+
const useAsyncify =
|
|
34
|
+
!!asyncify && asyncify(unwrapped) && "newAsyncifiedFunction" in ctx;
|
|
35
|
+
|
|
36
|
+
const inner = (
|
|
37
|
+
useAsyncify
|
|
38
|
+
? // Asyncify: the VM stack is suspended until the host promise settles, so
|
|
39
|
+
// the guest receives the resolved value synchronously. Async functions
|
|
40
|
+
// can never be class constructors, so the class-constructor path is
|
|
41
|
+
// skipped here.
|
|
42
|
+
(ctx as QuickJSAsyncContext).newAsyncifiedFunction(target.name, async function (
|
|
43
|
+
...argHandles
|
|
44
|
+
) {
|
|
45
|
+
const that = ctx.sameValue(this, ctx.global) ? undefined : unmarshal(this);
|
|
46
|
+
const args = argHandles.map(a => unmarshal(a));
|
|
47
|
+
|
|
48
|
+
// `preApply` wraps the (synchronous) invocation to toggle temporal sync
|
|
49
|
+
// around it; it returns the host promise, which we await here. Note that
|
|
50
|
+
// its finally runs as soon as `apply` returns the promise, so temporal
|
|
51
|
+
// sync is only active for the synchronous portion of the async function,
|
|
52
|
+
// not across its awaits.
|
|
53
|
+
const result = await (preApply
|
|
54
|
+
? preApply(target as (...args: any[]) => any, that, args)
|
|
55
|
+
: (target as (...args: any[]) => any).apply(that, args));
|
|
56
|
+
|
|
57
|
+
return prepareReturn(marshal(result));
|
|
58
|
+
})
|
|
59
|
+
: ctx.newFunction(target.name, function (...argHandles) {
|
|
60
|
+
// A plain call (`fn()`) passes the VM global object as `this`. Unmarshalling
|
|
61
|
+
// it would eagerly deep-copy the entire global graph (hundreds of handles)
|
|
62
|
+
// on the first call, for a `this` host functions almost never use — and
|
|
63
|
+
// leaking globalThis to the host is undesirable. So global `this` is passed
|
|
64
|
+
// to the host function as `undefined`, which differs from plain JS where a
|
|
65
|
+
// non-strict function sees `this === globalThis` (see README Limitations).
|
|
66
|
+
// Real method calls still get their receiver unmarshalled.
|
|
67
|
+
const that = ctx.sameValue(this, ctx.global) ? undefined : unmarshal(this);
|
|
68
|
+
const args = argHandles.map(a => unmarshal(a));
|
|
69
|
+
|
|
70
|
+
if (isClass && isObject(that)) {
|
|
71
|
+
// Class constructors cannot be invoked without new expression, and new.target is not changed
|
|
72
|
+
const result = new (target as new (...args: any[]) => any)(...args);
|
|
73
|
+
Object.entries(result).forEach(([key, value]) => {
|
|
74
|
+
const valueHandle = marshal(value);
|
|
75
|
+
ctx.setProp(this, key, valueHandle);
|
|
76
|
+
// setProp dup'd the value into `this`; drop ours if it was transient.
|
|
77
|
+
disposeTransient(valueHandle);
|
|
78
|
+
});
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The VM disposes whatever we return here. `prepareReturn` dups the
|
|
83
|
+
// handle when the VMMap retains it, so the map keeps a live copy and
|
|
84
|
+
// identity (`x === fn()` across calls) survives instead of going stale.
|
|
85
|
+
return prepareReturn(
|
|
86
|
+
marshal(
|
|
87
|
+
preApply
|
|
88
|
+
? preApply(target as (...args: any[]) => any, that, args)
|
|
89
|
+
: (target as (...args: any[]) => any).apply(that, args),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// `consume` disposes the raw newFunction handle even if `call` throws (the raw
|
|
96
|
+
// `Lifetime.consume` would skip disposal on throw, leaking the function handle).
|
|
97
|
+
const raw = consume(inner, handle2 =>
|
|
98
|
+
// functions created by vm.newFunction are not callable as a class constructor
|
|
99
|
+
call(
|
|
100
|
+
ctx,
|
|
101
|
+
`Cls => {
|
|
102
|
+
const fn = function(...args) { return Cls.apply(this, args); };
|
|
103
|
+
fn.name = Cls.name;
|
|
104
|
+
fn.length = Cls.length;
|
|
105
|
+
return fn;
|
|
106
|
+
}`,
|
|
107
|
+
undefined,
|
|
108
|
+
handle2,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Own `raw` until `preMarshal` registers it; dispose it if `preMarshal` throws
|
|
113
|
+
// mid-flight so the wrapped function handle is not orphaned.
|
|
114
|
+
let ownRaw = true;
|
|
115
|
+
try {
|
|
116
|
+
const handle = preMarshal(target, raw) ?? raw;
|
|
117
|
+
ownRaw = false;
|
|
118
|
+
marshalProperties(ctx, target, raw, marshal, disposeTransient);
|
|
119
|
+
return handle;
|
|
120
|
+
} finally {
|
|
121
|
+
if (ownRaw && raw.alive) raw.dispose();
|
|
122
|
+
}
|
|
57
123
|
}
|
package/src/marshal/index.ts
CHANGED
|
@@ -28,6 +28,19 @@ export type Options = {
|
|
|
28
28
|
mode: true | "json" | undefined,
|
|
29
29
|
) => QuickJSHandle | undefined;
|
|
30
30
|
preApply?: (target: (...args: any[]) => any, thisArg: unknown, args: unknown[]) => any;
|
|
31
|
+
// Adjust ownership of a handle that is about to be handed to the VM as a host
|
|
32
|
+
// function's return value. The VM disposes whatever it receives, so a handle
|
|
33
|
+
// the VMMap retains (for identity, while sync is on) must be dup'd here or its
|
|
34
|
+
// map entry goes stale. Defaults to identity (no-op).
|
|
35
|
+
prepareReturn?: (handle: QuickJSHandle) => QuickJSHandle;
|
|
36
|
+
// Strip a host-side proxy wrapper from a target. Used to detect a wrapped class
|
|
37
|
+
// constructor, which would otherwise be hidden behind the proxy. Defaults to identity.
|
|
38
|
+
unwrap?: (target: unknown) => unknown;
|
|
39
|
+
// Decide whether a host function should be marshalled as an Asyncified function
|
|
40
|
+
// (the VM suspends until the host promise settles, so the guest gets the
|
|
41
|
+
// resolved value synchronously). Called with the unwrapped target. Only takes
|
|
42
|
+
// effect when the context supports `newAsyncifiedFunction`.
|
|
43
|
+
asyncify?: (target: unknown) => boolean;
|
|
31
44
|
custom?: Iterable<(obj: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined>;
|
|
32
45
|
};
|
|
33
46
|
|
|
@@ -73,7 +86,18 @@ export function marshal(target: unknown, options: Options): QuickJSHandle {
|
|
|
73
86
|
return (
|
|
74
87
|
marshalCustom(ctx, target, pre2, [...defaultCustom, ...(options.custom ?? [])]) ??
|
|
75
88
|
marshalPromise(ctx, target, marshal2, pre2) ??
|
|
76
|
-
marshalFunction(
|
|
89
|
+
marshalFunction(
|
|
90
|
+
ctx,
|
|
91
|
+
target,
|
|
92
|
+
marshal2,
|
|
93
|
+
unmarshal,
|
|
94
|
+
pre2,
|
|
95
|
+
options.preApply,
|
|
96
|
+
disposeTransient,
|
|
97
|
+
options.prepareReturn,
|
|
98
|
+
options.unwrap,
|
|
99
|
+
options.asyncify,
|
|
100
|
+
) ??
|
|
77
101
|
marshalMapSet(ctx, target, marshal2, pre2, disposeTransient) ??
|
|
78
102
|
marshalObject(ctx, target, marshal2, pre2, disposeTransient) ??
|
|
79
103
|
ctx.undefined
|
package/src/marshal/mapset.ts
CHANGED
|
@@ -11,27 +11,41 @@ export default function marshalMapSet(
|
|
|
11
11
|
): QuickJSHandle | undefined {
|
|
12
12
|
if (target instanceof Map) {
|
|
13
13
|
const raw = call(ctx, "() => new Map()");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
// Own `raw` until `preMarshal` registers it; dispose it if `preMarshal`
|
|
15
|
+
// throws mid-flight so the fresh Map handle is not orphaned.
|
|
16
|
+
let ownRaw = true;
|
|
17
|
+
try {
|
|
18
|
+
const handle = preMarshal(target, raw) ?? raw;
|
|
19
|
+
ownRaw = false;
|
|
20
|
+
for (const [k, v] of target) {
|
|
21
|
+
const kh = marshal(k);
|
|
22
|
+
const vh = marshal(v);
|
|
23
|
+
call(ctx, "(m, k, v) => m.set(k, v)", undefined, raw, kh, vh).dispose();
|
|
24
|
+
// set() has taken its own references; drop ours if they were transient.
|
|
25
|
+
disposeTransient(kh);
|
|
26
|
+
disposeTransient(vh);
|
|
27
|
+
}
|
|
28
|
+
return handle;
|
|
29
|
+
} finally {
|
|
30
|
+
if (ownRaw && raw.alive) raw.dispose();
|
|
22
31
|
}
|
|
23
|
-
return handle;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
if (target instanceof Set) {
|
|
27
35
|
const raw = call(ctx, "() => new Set()");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
let ownRaw = true;
|
|
37
|
+
try {
|
|
38
|
+
const handle = preMarshal(target, raw) ?? raw;
|
|
39
|
+
ownRaw = false;
|
|
40
|
+
for (const v of target) {
|
|
41
|
+
const vh = marshal(v);
|
|
42
|
+
call(ctx, "(s, v) => s.add(v)", undefined, raw, vh).dispose();
|
|
43
|
+
// add() has taken its own reference; drop ours if it was transient.
|
|
44
|
+
disposeTransient(vh);
|
|
45
|
+
}
|
|
46
|
+
return handle;
|
|
47
|
+
} finally {
|
|
48
|
+
if (ownRaw && raw.alive) raw.dispose();
|
|
34
49
|
}
|
|
35
|
-
return handle;
|
|
36
50
|
}
|
|
37
51
|
}
|
package/src/marshal/object.ts
CHANGED
|
@@ -14,21 +14,30 @@ export default function marshalObject(
|
|
|
14
14
|
if (typeof target !== "object" || target === null) return;
|
|
15
15
|
|
|
16
16
|
const raw = Array.isArray(target) ? ctx.newArray() : ctx.newObject();
|
|
17
|
-
|
|
17
|
+
// We own `raw` until `preMarshal` registers it (in the map or the transient
|
|
18
|
+
// set) or returns it as `handle`. If `preMarshal` throws mid-flight (e.g. an
|
|
19
|
+
// OOM while creating the proxy), dispose `raw` so it is not orphaned.
|
|
20
|
+
let ownRaw = true;
|
|
21
|
+
try {
|
|
22
|
+
const handle = preMarshal(target, raw) ?? raw;
|
|
23
|
+
ownRaw = false;
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
// prototype
|
|
26
|
+
const prototype = Object.getPrototypeOf(target);
|
|
27
|
+
const prototypeHandle =
|
|
28
|
+
prototype && prototype !== Object.prototype && prototype !== Array.prototype
|
|
29
|
+
? marshal(prototype)
|
|
30
|
+
: undefined;
|
|
31
|
+
if (prototypeHandle) {
|
|
32
|
+
call(ctx, "Object.setPrototypeOf", undefined, handle, prototypeHandle).dispose();
|
|
33
|
+
// setPrototypeOf has taken its own reference; drop ours if it was transient.
|
|
34
|
+
disposeTransient(prototypeHandle);
|
|
35
|
+
}
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
marshalProperties(ctx, target, raw, marshal, disposeTransient);
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
return handle;
|
|
40
|
+
} finally {
|
|
41
|
+
if (ownRaw && raw.alive) raw.dispose();
|
|
42
|
+
}
|
|
34
43
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
2
|
+
import type {
|
|
3
|
+
Disposable,
|
|
4
|
+
QuickJSDeferredPromise,
|
|
5
|
+
QuickJSHandle,
|
|
6
6
|
} from "quickjs-emscripten";
|
|
7
7
|
import { expect, test, vi } from "vitest";
|
|
8
8
|
|
|
@@ -15,15 +15,17 @@ const testPromise = (reject: boolean) => async () => {
|
|
|
15
15
|
const ctx = (await getQuickJS()).newContext();
|
|
16
16
|
|
|
17
17
|
const disposables: Disposable[] = [];
|
|
18
|
-
const marshal = vi.fn(v => {
|
|
18
|
+
const marshal = vi.fn((v) => {
|
|
19
19
|
const handle = json(ctx, v);
|
|
20
20
|
disposables.push(handle);
|
|
21
21
|
return handle;
|
|
22
22
|
});
|
|
23
|
-
const preMarshal = vi.fn(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const preMarshal = vi.fn(
|
|
24
|
+
(_: any, a: QuickJSDeferredPromise): QuickJSHandle => {
|
|
25
|
+
disposables.push(a);
|
|
26
|
+
return a.handle;
|
|
27
|
+
},
|
|
28
|
+
);
|
|
27
29
|
|
|
28
30
|
const mockNotify = vi.fn();
|
|
29
31
|
const notify = ctx.newFunction("notify", (handle1, handle2) => {
|
|
@@ -77,7 +79,7 @@ const testPromise = (reject: boolean) => async () => {
|
|
|
77
79
|
expect(marshal).toBeCalledTimes(1);
|
|
78
80
|
expect(marshal.mock.calls).toEqual([["hoge"]]);
|
|
79
81
|
|
|
80
|
-
disposables.forEach(h => h.dispose());
|
|
82
|
+
disposables.forEach((h) => h.dispose());
|
|
81
83
|
ctx.dispose();
|
|
82
84
|
};
|
|
83
85
|
|
package/src/marshal/promise.ts
CHANGED
|
@@ -9,11 +9,19 @@ export default function marshalPromise(
|
|
|
9
9
|
if (!(target instanceof Promise)) return;
|
|
10
10
|
|
|
11
11
|
const promise = ctx.newPromise();
|
|
12
|
+
// Own the deferred promise until `preMarshal` registers it; dispose it (handle
|
|
13
|
+
// plus resolve/reject callbacks) if `preMarshal` throws mid-flight.
|
|
14
|
+
let owned = true;
|
|
15
|
+
try {
|
|
16
|
+
target.then(
|
|
17
|
+
d => promise.resolve(marshal(d)),
|
|
18
|
+
d => promise.reject(marshal(d)),
|
|
19
|
+
);
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const result = preMarshal(target, promise) ?? promise.handle;
|
|
22
|
+
owned = false;
|
|
23
|
+
return result;
|
|
24
|
+
} finally {
|
|
25
|
+
if (owned && promise.alive) promise.dispose();
|
|
26
|
+
}
|
|
19
27
|
}
|
|
@@ -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 } from "../vmutil";
|
|
@@ -23,7 +24,7 @@ test("works", async () => {
|
|
|
23
24
|
);
|
|
24
25
|
|
|
25
26
|
const disposables: QuickJSHandle[] = [];
|
|
26
|
-
const marshal = vi.fn(t => {
|
|
27
|
+
const marshal = vi.fn((t) => {
|
|
27
28
|
if (typeof t !== "function") return json(ctx, t);
|
|
28
29
|
const fn = ctx.newFunction("", () => {});
|
|
29
30
|
disposables.push(fn);
|
|
@@ -51,7 +52,13 @@ test("works", async () => {
|
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
marshalProperties(ctx, obj, handle, marshal);
|
|
54
|
-
expect(marshal.mock.calls).toEqual([
|
|
55
|
+
expect(marshal.mock.calls).toEqual([
|
|
56
|
+
["bar"],
|
|
57
|
+
[bar],
|
|
58
|
+
["foo"],
|
|
59
|
+
[fooGet],
|
|
60
|
+
[fooSet],
|
|
61
|
+
]);
|
|
55
62
|
|
|
56
63
|
const expected = ctx.unwrapResult(
|
|
57
64
|
ctx.evalCode(`({
|
|
@@ -59,10 +66,12 @@ test("works", async () => {
|
|
|
59
66
|
foo: { valueType: "undefined", getType: "function", setType: "function", enumerable: false, configurable: true }
|
|
60
67
|
})`),
|
|
61
68
|
);
|
|
62
|
-
ctx.unwrapResult(
|
|
69
|
+
ctx.unwrapResult(
|
|
70
|
+
ctx.callFunction(descTester, ctx.undefined, handle, expected),
|
|
71
|
+
);
|
|
63
72
|
|
|
64
73
|
expected.dispose();
|
|
65
|
-
disposables.forEach(d => d.dispose());
|
|
74
|
+
disposables.forEach((d) => d.dispose());
|
|
66
75
|
handle.dispose();
|
|
67
76
|
descTester.dispose();
|
|
68
77
|
ctx.dispose();
|
|
@@ -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 marshalProperties(
|
|
6
6
|
ctx: QuickJSContext,
|
|
@@ -9,12 +9,55 @@ export default function marshalProperties(
|
|
|
9
9
|
marshal: (target: unknown) => QuickJSHandle,
|
|
10
10
|
disposeTransient: (handle: QuickJSHandle) => void = () => {},
|
|
11
11
|
): void {
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// `descs` aggregates only the properties that need the full descriptor path
|
|
13
|
+
// (accessors, non-default flags, symbol keys). It is created lazily so plain
|
|
14
|
+
// objects/arrays skip both the `ctx.newObject()` and the final
|
|
15
|
+
// `Object.defineProperties` roundtrip entirely.
|
|
16
|
+
let descs: QuickJSHandle | undefined;
|
|
17
|
+
// Descriptor-path values may be transient (json copies / BigInt) with no
|
|
18
|
+
// owner; once `Object.defineProperties` has copied them into `handle` the
|
|
19
|
+
// standalone handles are redundant and disposed below. Owned handles are left
|
|
20
|
+
// untouched. Fast-path values are disposed inline (see below).
|
|
16
21
|
const transient: QuickJSHandle[] = [];
|
|
22
|
+
|
|
23
|
+
// `ctx.setProp` uses `[[Set]]` semantics, which walks the prototype chain and
|
|
24
|
+
// would invoke an inherited accessor/setter (or the `__proto__` setter)
|
|
25
|
+
// instead of creating an own property. That only matters when `marshalObject`
|
|
26
|
+
// installed a custom prototype (see marshal/object.ts): a default-prototype VM
|
|
27
|
+
// object (plain object / array) has no shadowing accessors, so `setProp` on a
|
|
28
|
+
// normal string key is identical to `defineProperty` with all flags true. For
|
|
29
|
+
// custom-prototype objects (e.g. class instances) we must keep the descriptor
|
|
30
|
+
// path, which uses `[[DefineOwnProperty]]` semantics.
|
|
31
|
+
const proto = Object.getPrototypeOf(target);
|
|
32
|
+
const defaultProto = proto === Object.prototype || proto === Array.prototype;
|
|
33
|
+
|
|
17
34
|
const cb = (key: string | number | symbol, desc: PropertyDescriptor) => {
|
|
35
|
+
// Fast path: a plain data property with a string key and all flags at their
|
|
36
|
+
// defaults, on a default-prototype object, is semantically identical to
|
|
37
|
+
// `ctx.setProp` on a fresh object/array, so we skip building a descriptor
|
|
38
|
+
// object. `writable === true` implies a data descriptor (accessors have no
|
|
39
|
+
// `writable`), so there is no get/set. Symbol keys keep the descriptor path
|
|
40
|
+
// (setProp string-key fast path does not apply). `__proto__` is excluded
|
|
41
|
+
// because it resolves to the prototype setter under `[[Set]]`. The value is
|
|
42
|
+
// copied into `handle` by setProp immediately, so its transient handle can
|
|
43
|
+
// be disposed right away.
|
|
44
|
+
if (
|
|
45
|
+
defaultProto &&
|
|
46
|
+
typeof key === "string" &&
|
|
47
|
+
key !== "__proto__" &&
|
|
48
|
+
desc.writable === true &&
|
|
49
|
+
desc.enumerable === true &&
|
|
50
|
+
desc.configurable === true &&
|
|
51
|
+
desc.get === undefined &&
|
|
52
|
+
desc.set === undefined
|
|
53
|
+
) {
|
|
54
|
+
const keyHandle = marshal(key);
|
|
55
|
+
const valueHandle = marshal(desc.value);
|
|
56
|
+
ctx.setProp(handle, keyHandle, valueHandle);
|
|
57
|
+
disposeTransient(valueHandle);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
const keyHandle = marshal(key);
|
|
19
62
|
const valueHandle = typeof desc.value === "undefined" ? undefined : marshal(desc.value);
|
|
20
63
|
const getHandle = typeof desc.get === "undefined" ? undefined : marshal(desc.get);
|
|
@@ -23,7 +66,10 @@ export default function marshalProperties(
|
|
|
23
66
|
if (getHandle) transient.push(getHandle);
|
|
24
67
|
if (setHandle) transient.push(setHandle);
|
|
25
68
|
|
|
26
|
-
ctx.newObject()
|
|
69
|
+
const descsHandle = (descs ??= ctx.newObject());
|
|
70
|
+
// `consume` disposes the descriptor object even if a `setProp` throws
|
|
71
|
+
// mid-flight, so the intermediate handle is not orphaned.
|
|
72
|
+
consume(ctx.newObject(), descObj => {
|
|
27
73
|
Object.entries(desc).forEach(([k, v]) => {
|
|
28
74
|
const v2 =
|
|
29
75
|
k === "value"
|
|
@@ -39,7 +85,7 @@ export default function marshalProperties(
|
|
|
39
85
|
ctx.setProp(descObj, k, v2);
|
|
40
86
|
}
|
|
41
87
|
});
|
|
42
|
-
ctx.setProp(
|
|
88
|
+
ctx.setProp(descsHandle, keyHandle, descObj);
|
|
43
89
|
});
|
|
44
90
|
};
|
|
45
91
|
|
|
@@ -48,11 +94,13 @@ export default function marshalProperties(
|
|
|
48
94
|
Object.entries(desc).forEach(([k, v]) => cb(k, v));
|
|
49
95
|
Object.getOwnPropertySymbols(desc).forEach(k => cb(k, (desc as any)[k]));
|
|
50
96
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
if (descs) {
|
|
98
|
+
call(ctx, `Object.defineProperties`, undefined, handle, descs).dispose();
|
|
99
|
+
// Safe only after defineProperties has dup'd the values into `handle`;
|
|
100
|
+
// `descs` still holds its own references until its own dispose() below.
|
|
101
|
+
for (const h of transient) disposeTransient(h);
|
|
102
|
+
}
|
|
55
103
|
} finally {
|
|
56
|
-
descs
|
|
104
|
+
descs?.dispose();
|
|
57
105
|
}
|
|
58
106
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import variant from "@jitl/quickjs-wasmfile-debug-sync";
|
|
2
|
+
import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { Arena } from ".";
|
|
6
|
+
|
|
7
|
+
// Marshalling the same host object into the VM more than once used to leak a
|
|
8
|
+
// handle: the first registration stores both a wrapped (proxy) handle and its
|
|
9
|
+
// unwrapped sibling. Once the VM frees the proxy, the map entry goes stale, and
|
|
10
|
+
// the lazy eviction in VMMap.get dropped the entry without disposing the
|
|
11
|
+
// still-alive sibling. The debug-sync runtime aborts on dispose if any GC
|
|
12
|
+
// object handle leaked, so reaching ctx.dispose() without an abort proves the
|
|
13
|
+
// sibling is now released.
|
|
14
|
+
async function withArena(fn: (arena: Arena) => void) {
|
|
15
|
+
const mod = await newQuickJSWASMModuleFromVariant(variant as any);
|
|
16
|
+
const ctx = mod.newContext();
|
|
17
|
+
const arena = new Arena(ctx, { isMarshalable: true });
|
|
18
|
+
fn(arena);
|
|
19
|
+
arena.dispose();
|
|
20
|
+
expect(() => ctx.dispose()).not.toThrow();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("re-marshal handle leak", () => {
|
|
24
|
+
// Calling an exposed function from inside the VM marshals the whole global
|
|
25
|
+
// graph on each call, which is slow under the debug-sync runtime + coverage,
|
|
26
|
+
// so these get a generous timeout (cf. the "many newFunction" edge test).
|
|
27
|
+
it(
|
|
28
|
+
"does not leak when a host function returns the same object twice",
|
|
29
|
+
async () => {
|
|
30
|
+
await withArena(arena => {
|
|
31
|
+
const shared = { k: 1 };
|
|
32
|
+
arena.expose({ get: () => shared });
|
|
33
|
+
arena.evalCode(`get(); get();`);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
90000,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
it(
|
|
40
|
+
"does not leak when the same object is compared across two calls",
|
|
41
|
+
async () => {
|
|
42
|
+
await withArena(arena => {
|
|
43
|
+
const shared = { k: 1 };
|
|
44
|
+
arena.expose({ get: () => shared });
|
|
45
|
+
arena.evalCode(`get() === get();`);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
90000,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
@@ -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 unmarshalCustom, { defaultCustom } from "./custom";
|
|
@@ -9,7 +10,8 @@ test("symbol", async () => {
|
|
|
9
10
|
const obj = ctx.newObject();
|
|
10
11
|
const handle = ctx.unwrapResult(ctx.evalCode(`Symbol("foobar")`));
|
|
11
12
|
|
|
12
|
-
const unmarshal = (h: QuickJSHandle): any =>
|
|
13
|
+
const unmarshal = (h: QuickJSHandle): any =>
|
|
14
|
+
unmarshalCustom(ctx, h, pre, defaultCustom);
|
|
13
15
|
|
|
14
16
|
expect(unmarshal(obj)).toBe(undefined);
|
|
15
17
|
expect(pre).toBeCalledTimes(0);
|
|
@@ -32,7 +34,8 @@ test("date", async () => {
|
|
|
32
34
|
const obj = ctx.newObject();
|
|
33
35
|
const handle = ctx.unwrapResult(ctx.evalCode(`new Date(2022, 7, 26)`));
|
|
34
36
|
|
|
35
|
-
const unmarshal = (h: QuickJSHandle): any =>
|
|
37
|
+
const unmarshal = (h: QuickJSHandle): any =>
|
|
38
|
+
unmarshalCustom(ctx, h, pre, defaultCustom);
|
|
36
39
|
|
|
37
40
|
expect(unmarshal(obj)).toBe(undefined);
|
|
38
41
|
expect(pre).toBeCalledTimes(0);
|