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
|
@@ -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";
|
|
@@ -8,8 +9,11 @@ import unmarshalFunction from "./function";
|
|
|
8
9
|
test("arrow function", async () => {
|
|
9
10
|
const ctx = (await getQuickJS()).newContext();
|
|
10
11
|
const marshal = vi.fn((v): [QuickJSHandle, boolean] => [json(ctx, v), false]);
|
|
11
|
-
const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
|
|
12
|
-
|
|
12
|
+
const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
|
|
13
|
+
ctx.dump(v),
|
|
14
|
+
false,
|
|
15
|
+
]);
|
|
16
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
13
17
|
|
|
14
18
|
const handle = ctx.unwrapResult(ctx.evalCode(`(a, b) => a + b`));
|
|
15
19
|
const func = unmarshalFunction(ctx, handle, marshal, unmarshal, preUnmarshal);
|
|
@@ -49,9 +53,11 @@ test("function", async () => {
|
|
|
49
53
|
if (ty === "object" || ty === "function") disposables.push(v);
|
|
50
54
|
return [ctx.dump(v), false];
|
|
51
55
|
});
|
|
52
|
-
const preUnmarshal = vi.fn(a => a);
|
|
56
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
53
57
|
|
|
54
|
-
const handle = ctx.unwrapResult(
|
|
58
|
+
const handle = ctx.unwrapResult(
|
|
59
|
+
ctx.evalCode(`(function (a) { return this.a + a; })`),
|
|
60
|
+
);
|
|
55
61
|
|
|
56
62
|
const func = unmarshalFunction(ctx, handle, marshal, unmarshal, preUnmarshal);
|
|
57
63
|
if (!func) throw new Error("func is undefined");
|
|
@@ -71,7 +77,7 @@ test("function", async () => {
|
|
|
71
77
|
expect(preUnmarshal).toBeCalledTimes(1);
|
|
72
78
|
expect(preUnmarshal).toBeCalledWith(func, handle);
|
|
73
79
|
|
|
74
|
-
disposables.forEach(d => d.dispose());
|
|
80
|
+
disposables.forEach((d) => d.dispose());
|
|
75
81
|
thatHandle.dispose();
|
|
76
82
|
handle.dispose();
|
|
77
83
|
ctx.dispose();
|
|
@@ -89,11 +95,19 @@ test("constructor", async () => {
|
|
|
89
95
|
if (ty === "object" || ty === "function") disposables.push(v);
|
|
90
96
|
return [ctx.dump(v), false];
|
|
91
97
|
});
|
|
92
|
-
const preUnmarshal = vi.fn(a => a);
|
|
93
|
-
|
|
94
|
-
const handle = ctx.unwrapResult(
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
99
|
+
|
|
100
|
+
const handle = ctx.unwrapResult(
|
|
101
|
+
ctx.evalCode(`(function (b) { this.a = b + 2; })`),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const Cls = unmarshalFunction(
|
|
105
|
+
ctx,
|
|
106
|
+
handle,
|
|
107
|
+
marshal,
|
|
108
|
+
unmarshal,
|
|
109
|
+
preUnmarshal,
|
|
110
|
+
) as any;
|
|
97
111
|
if (!Cls) throw new Error("Cls is undefined");
|
|
98
112
|
|
|
99
113
|
const instance = new Cls(100);
|
|
@@ -113,7 +127,7 @@ test("constructor", async () => {
|
|
|
113
127
|
expect(preUnmarshal).toBeCalledTimes(1);
|
|
114
128
|
expect(preUnmarshal).toBeCalledWith(Cls, handle);
|
|
115
129
|
|
|
116
|
-
disposables.forEach(d => d.dispose());
|
|
130
|
+
disposables.forEach((d) => d.dispose());
|
|
117
131
|
handle.dispose();
|
|
118
132
|
ctx.dispose();
|
|
119
133
|
});
|
|
@@ -130,11 +144,19 @@ test("class", async () => {
|
|
|
130
144
|
if (ty === "object" || ty === "function") disposables.push(v);
|
|
131
145
|
return [ctx.dump(v), false];
|
|
132
146
|
});
|
|
133
|
-
const preUnmarshal = vi.fn(a => a);
|
|
134
|
-
|
|
135
|
-
const handle = ctx.unwrapResult(
|
|
136
|
-
|
|
137
|
-
|
|
147
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
148
|
+
|
|
149
|
+
const handle = ctx.unwrapResult(
|
|
150
|
+
ctx.evalCode(`(class A { constructor(a) { this.a = a + 1; } })`),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const Cls = unmarshalFunction(
|
|
154
|
+
ctx,
|
|
155
|
+
handle,
|
|
156
|
+
marshal,
|
|
157
|
+
unmarshal,
|
|
158
|
+
preUnmarshal,
|
|
159
|
+
) as any;
|
|
138
160
|
if (!Cls) throw new Error("Cls is undefined");
|
|
139
161
|
|
|
140
162
|
const instance = new Cls(2);
|
|
@@ -154,7 +176,7 @@ test("class", async () => {
|
|
|
154
176
|
expect(preUnmarshal).toBeCalledTimes(1);
|
|
155
177
|
expect(preUnmarshal).toBeCalledWith(Cls, handle);
|
|
156
178
|
|
|
157
|
-
disposables.forEach(d => d.dispose());
|
|
179
|
+
disposables.forEach((d) => d.dispose());
|
|
158
180
|
handle.dispose();
|
|
159
181
|
ctx.dispose();
|
|
160
182
|
});
|
|
@@ -167,8 +189,12 @@ test("undefined", async () => {
|
|
|
167
189
|
expect(unmarshalFunction(ctx, ctx.true, f, f, f)).toEqual(undefined);
|
|
168
190
|
expect(unmarshalFunction(ctx, ctx.false, f, f, f)).toEqual(undefined);
|
|
169
191
|
expect(unmarshalFunction(ctx, ctx.null, f, f, f)).toEqual(undefined);
|
|
170
|
-
expect(unmarshalFunction(ctx, ctx.newString("hoge"), f, f, f)).toEqual(
|
|
171
|
-
|
|
192
|
+
expect(unmarshalFunction(ctx, ctx.newString("hoge"), f, f, f)).toEqual(
|
|
193
|
+
undefined,
|
|
194
|
+
);
|
|
195
|
+
expect(unmarshalFunction(ctx, ctx.newNumber(-10), f, f, f)).toEqual(
|
|
196
|
+
undefined,
|
|
197
|
+
);
|
|
172
198
|
|
|
173
199
|
const obj = ctx.newObject();
|
|
174
200
|
expect(unmarshalFunction(ctx, obj, f, f, f)).toEqual(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 VMMap from "../vmmap";
|
|
@@ -29,13 +30,19 @@ test("primitive, array, object", async () => {
|
|
|
29
30
|
});
|
|
30
31
|
expect(map.size).toBe(5);
|
|
31
32
|
expect(map.getByHandle(handle)).toBe(target);
|
|
32
|
-
ctx.getProp(handle, "aaa").consume(h => expect(map.getByHandle(h)).toBe(target.aaa));
|
|
33
33
|
ctx
|
|
34
34
|
.getProp(handle, "aaa")
|
|
35
|
-
.consume(h =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
.consume((h) => expect(map.getByHandle(h)).toBe(target.aaa));
|
|
36
|
+
ctx
|
|
37
|
+
.getProp(handle, "aaa")
|
|
38
|
+
.consume((h) => ctx.getProp(h, 2))
|
|
39
|
+
.consume((h) => expect(map.getByHandle(h)).toBe(target.aaa[2]));
|
|
40
|
+
ctx
|
|
41
|
+
.getProp(handle, "nested")
|
|
42
|
+
.consume((h) => expect(map.getByHandle(h)).toBe(target.nested));
|
|
43
|
+
ctx
|
|
44
|
+
.getProp(handle, "bbb")
|
|
45
|
+
.consume((h) => expect(map.getByHandle(h)).toBe(target.bbb));
|
|
39
46
|
|
|
40
47
|
expect(marshal).toBeCalledTimes(0);
|
|
41
48
|
expect(target.bbb()).toBe("bar");
|
|
@@ -65,7 +72,9 @@ test("object with symbol key", async () => {
|
|
|
65
72
|
test("function", async () => {
|
|
66
73
|
const { ctx, unmarshal, marshal, map, dispose } = await setup();
|
|
67
74
|
|
|
68
|
-
const handle = ctx.unwrapResult(
|
|
75
|
+
const handle = ctx.unwrapResult(
|
|
76
|
+
ctx.evalCode(`(function(a) { return a.a + "!"; })`),
|
|
77
|
+
);
|
|
69
78
|
const func = unmarshal(handle);
|
|
70
79
|
const arg = { a: "hoge" };
|
|
71
80
|
expect(func(arg)).toBe("hoge!");
|
|
@@ -156,7 +165,7 @@ const setup = async () => {
|
|
|
156
165
|
const handle2 =
|
|
157
166
|
typeof target === "function"
|
|
158
167
|
? ctx.newFunction(target.name, (...handles) => {
|
|
159
|
-
target(...handles.map(h => ctx.dump(h)));
|
|
168
|
+
target(...handles.map((h) => ctx.dump(h)));
|
|
160
169
|
})
|
|
161
170
|
: json(ctx, target);
|
|
162
171
|
const ty = ctx.typeof(handle2);
|
|
@@ -169,7 +178,7 @@ const setup = async () => {
|
|
|
169
178
|
map,
|
|
170
179
|
unmarshal: (handle: QuickJSHandle) =>
|
|
171
180
|
unmarshal(handle, {
|
|
172
|
-
find: h => map.getByHandle(h),
|
|
181
|
+
find: (h) => map.getByHandle(h),
|
|
173
182
|
marshal,
|
|
174
183
|
pre: (t, h) => {
|
|
175
184
|
map.set(t, h);
|
|
@@ -179,7 +188,7 @@ const setup = async () => {
|
|
|
179
188
|
}),
|
|
180
189
|
marshal,
|
|
181
190
|
dispose: () => {
|
|
182
|
-
disposables.forEach(d => d.dispose());
|
|
191
|
+
disposables.forEach((d) => d.dispose());
|
|
183
192
|
map.dispose();
|
|
184
193
|
ctx.dispose();
|
|
185
194
|
},
|
package/src/unmarshal/mapset.ts
CHANGED
|
@@ -21,18 +21,33 @@ export default function unmarshalMapSet(
|
|
|
21
21
|
for (const elResult of iterator) {
|
|
22
22
|
const el = ctx.unwrapResult(elResult);
|
|
23
23
|
if (isMap) {
|
|
24
|
+
// `disposeKey`/`disposeValue` start true so a mid-flight throw (e.g. OOM
|
|
25
|
+
// inside `unmarshal`) disposes the property handles instead of orphaning
|
|
26
|
+
// them; on success they are disposed only when redundant (already owned).
|
|
24
27
|
const keyHandle = ctx.getProp(el, 0);
|
|
25
28
|
const valueHandle = ctx.getProp(el, 1);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
let disposeKey = true;
|
|
30
|
+
let disposeValue = true;
|
|
31
|
+
try {
|
|
32
|
+
const [key, dk] = unmarshal(keyHandle);
|
|
33
|
+
disposeKey = dk;
|
|
34
|
+
const [value, dv] = unmarshal(valueHandle);
|
|
35
|
+
disposeValue = dv;
|
|
36
|
+
(result as Map<any, any>).set(key, value);
|
|
37
|
+
} finally {
|
|
38
|
+
if (disposeKey && keyHandle.alive) keyHandle.dispose();
|
|
39
|
+
if (disposeValue && valueHandle.alive) valueHandle.dispose();
|
|
40
|
+
if (el.alive) el.dispose();
|
|
41
|
+
}
|
|
32
42
|
} else {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
let disposeValue = true;
|
|
44
|
+
try {
|
|
45
|
+
const [value, dv] = unmarshal(el);
|
|
46
|
+
disposeValue = dv;
|
|
47
|
+
(result as Set<any>).add(value);
|
|
48
|
+
} finally {
|
|
49
|
+
if (disposeValue && el.alive) el.dispose();
|
|
50
|
+
}
|
|
36
51
|
}
|
|
37
52
|
}
|
|
38
53
|
} finally {
|
|
@@ -1,12 +1,16 @@
|
|
|
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 unmarshalObject from "./object";
|
|
5
6
|
|
|
6
7
|
test("normal object", async () => {
|
|
7
8
|
const ctx = (await getQuickJS()).newContext();
|
|
8
|
-
const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
|
|
9
|
-
|
|
9
|
+
const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
|
|
10
|
+
ctx.dump(v),
|
|
11
|
+
false,
|
|
12
|
+
]);
|
|
13
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
10
14
|
|
|
11
15
|
const handle = ctx.unwrapResult(ctx.evalCode(`({ a: 1, b: true })`));
|
|
12
16
|
const obj = unmarshalObject(ctx, handle, unmarshal, preUnmarshal);
|
|
@@ -31,7 +35,7 @@ test("properties", async () => {
|
|
|
31
35
|
disposables.push(v);
|
|
32
36
|
return [ctx.typeof(v) === "function" ? () => {} : ctx.dump(v), false];
|
|
33
37
|
});
|
|
34
|
-
const preUnmarshal = vi.fn(a => a);
|
|
38
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
35
39
|
|
|
36
40
|
const handle = ctx.unwrapResult(
|
|
37
41
|
ctx.evalCode(`{
|
|
@@ -69,15 +73,18 @@ test("properties", async () => {
|
|
|
69
73
|
expect(preUnmarshal).toBeCalledTimes(1);
|
|
70
74
|
expect(preUnmarshal).toBeCalledWith(obj, handle);
|
|
71
75
|
|
|
72
|
-
disposables.forEach(d => d.dispose());
|
|
76
|
+
disposables.forEach((d) => d.dispose());
|
|
73
77
|
handle.dispose();
|
|
74
78
|
ctx.dispose();
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
test("array", async () => {
|
|
78
82
|
const ctx = (await getQuickJS()).newContext();
|
|
79
|
-
const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
|
|
80
|
-
|
|
83
|
+
const unmarshal = vi.fn((v: QuickJSHandle): [unknown, boolean] => [
|
|
84
|
+
ctx.dump(v),
|
|
85
|
+
false,
|
|
86
|
+
]);
|
|
87
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
81
88
|
|
|
82
89
|
const handle = ctx.unwrapResult(ctx.evalCode(`[1, true, "a"]`));
|
|
83
90
|
const array = unmarshalObject(ctx, handle, unmarshal, preUnmarshal);
|
|
@@ -103,9 +110,11 @@ test("prototype", async () => {
|
|
|
103
110
|
ctx.typeof(v) === "object" ? { a: () => 1 } : ctx.dump(v),
|
|
104
111
|
false,
|
|
105
112
|
]);
|
|
106
|
-
const preUnmarshal = vi.fn(a => a);
|
|
113
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
107
114
|
|
|
108
|
-
const handle = ctx.unwrapResult(
|
|
115
|
+
const handle = ctx.unwrapResult(
|
|
116
|
+
ctx.evalCode(`Object.create({ a: () => 1 })`),
|
|
117
|
+
);
|
|
109
118
|
const obj = unmarshalObject(ctx, handle, unmarshal, preUnmarshal) as any;
|
|
110
119
|
if (!obj) throw new Error("obj is undefined");
|
|
111
120
|
expect(Object.getPrototypeOf(obj)).toEqual({ a: expect.any(Function) });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
2
|
+
import type { Disposable, QuickJSHandle } from "quickjs-emscripten";
|
|
2
3
|
import { expect, test, vi } from "vitest";
|
|
3
4
|
|
|
4
5
|
import unmarshalPromise from "./promise";
|
|
@@ -7,13 +8,13 @@ const testPromise = (reject: boolean) => async () => {
|
|
|
7
8
|
const ctx = (await getQuickJS()).newContext();
|
|
8
9
|
const disposables: Disposable[] = [];
|
|
9
10
|
const marshal = vi.fn((v): [QuickJSHandle, boolean] => {
|
|
10
|
-
const f = ctx.newFunction(v.name, h => {
|
|
11
|
+
const f = ctx.newFunction(v.name, (h) => {
|
|
11
12
|
v(ctx.dump(h));
|
|
12
13
|
});
|
|
13
14
|
disposables.push(f);
|
|
14
15
|
return [f, false];
|
|
15
16
|
});
|
|
16
|
-
const preUnmarshal = vi.fn(a => a);
|
|
17
|
+
const preUnmarshal = vi.fn((a) => a);
|
|
17
18
|
|
|
18
19
|
const deferred = ctx.newPromise();
|
|
19
20
|
disposables.push(deferred);
|
|
@@ -36,7 +37,7 @@ const testPromise = (reject: boolean) => async () => {
|
|
|
36
37
|
await expect(promise).resolves.toBe("hoge");
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
disposables.forEach(d => d.dispose());
|
|
40
|
+
disposables.forEach((d) => d.dispose());
|
|
40
41
|
ctx.dispose();
|
|
41
42
|
};
|
|
42
43
|
|
package/src/unmarshal/promise.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
3
|
import { newDeferred } from "../util";
|
|
4
|
-
import { call, instanceOf } from "../vmutil";
|
|
4
|
+
import { call, consume, instanceOf } from "../vmutil";
|
|
5
5
|
|
|
6
6
|
export default function unmarshalPromise<T = unknown>(
|
|
7
7
|
ctx: QuickJSContext,
|
|
@@ -24,7 +24,11 @@ export default function unmarshalPromise<T = unknown>(
|
|
|
24
24
|
|
|
25
25
|
function isPromiseHandle(ctx: QuickJSContext, handle: QuickJSHandle): boolean {
|
|
26
26
|
if (!handle.owner) return false;
|
|
27
|
-
|
|
27
|
+
// `consume` disposes the Promise constructor handle even if `instanceOf`
|
|
28
|
+
// throws mid-flight (e.g. an interrupt or OOM lands in the `a instanceof b`
|
|
29
|
+
// VM call); the raw `Lifetime.consume` would skip disposal on throw, orphaning
|
|
30
|
+
// the constructor handle.
|
|
31
|
+
return consume(ctx.unwrapResult(ctx.evalCode("Promise")), promise => {
|
|
28
32
|
if (!handle.owner) return false;
|
|
29
33
|
return instanceOf(ctx, handle, promise);
|
|
30
34
|
});
|
|
@@ -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 unmarshalProperties from "./properties";
|
|
@@ -47,7 +48,7 @@ test("works", async () => {
|
|
|
47
48
|
expect(unmarshal).toReturnWith(["c", false]);
|
|
48
49
|
expect(unmarshal).toReturnWith([expect.any(Function), false]); // get, set
|
|
49
50
|
|
|
50
|
-
disposables.forEach(d => d.dispose());
|
|
51
|
+
disposables.forEach((d) => d.dispose());
|
|
51
52
|
handle.dispose();
|
|
52
53
|
ctx.dispose();
|
|
53
54
|
});
|
|
@@ -2,6 +2,46 @@ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
|
2
2
|
|
|
3
3
|
import { call, consume } from "../vmutil";
|
|
4
4
|
|
|
5
|
+
// The VM-side iterator computes a numeric bitmask per property so the host can
|
|
6
|
+
// rebuild the PropertyDescriptor with at most one `ctx.getProp` (for the single
|
|
7
|
+
// value/get/set entry that is actually present) plus one `ctx.getNumber`,
|
|
8
|
+
// instead of the previous six `getProp` + six `typeof` calls per property.
|
|
9
|
+
//
|
|
10
|
+
// Bit layout (a "present" bit for every field, plus a "value" bit for the three
|
|
11
|
+
// boolean fields whose value we cannot recover from a handle):
|
|
12
|
+
// bit 0 (1) value present (typeof d.value !== "undefined")
|
|
13
|
+
// bit 1 (2) get present (typeof d.get !== "undefined")
|
|
14
|
+
// bit 2 (4) set present (typeof d.set !== "undefined")
|
|
15
|
+
// bit 3 (8) configurable present (typeof d.configurable === "boolean")
|
|
16
|
+
// bit 4 (16) configurable value (d.configurable === true)
|
|
17
|
+
// bit 5 (32) enumerable present (typeof d.enumerable === "boolean")
|
|
18
|
+
// bit 6 (64) enumerable value (d.enumerable === true)
|
|
19
|
+
// bit 7 (128) writable present (typeof d.writable === "boolean")
|
|
20
|
+
// bit 8 (256) writable value (d.writable === true)
|
|
21
|
+
//
|
|
22
|
+
// This mirrors the previous logic exactly: value/get/set are copied only when
|
|
23
|
+
// defined (an explicit `value: undefined` is treated as absent, so defaults
|
|
24
|
+
// apply, just as `typeof === "undefined"` skipped it before); a boolean field
|
|
25
|
+
// is copied only when actually a boolean, and absent boolean fields stay absent
|
|
26
|
+
// so `Object.defineProperty` applies its defaults. Data vs accessor descriptors
|
|
27
|
+
// are distinguished naturally: data descriptors carry a `writable` present bit
|
|
28
|
+
// and no get/set, accessors carry get/set and no `writable`.
|
|
29
|
+
const flagsFn = `(o, fn) => {
|
|
30
|
+
const descs = Object.getOwnPropertyDescriptors(o);
|
|
31
|
+
const emit = (k, d) => {
|
|
32
|
+
let f = 0;
|
|
33
|
+
if (typeof d.value !== "undefined") f |= 1;
|
|
34
|
+
if (typeof d.get !== "undefined") f |= 2;
|
|
35
|
+
if (typeof d.set !== "undefined") f |= 4;
|
|
36
|
+
if (typeof d.configurable === "boolean") { f |= 8; if (d.configurable) f |= 16; }
|
|
37
|
+
if (typeof d.enumerable === "boolean") { f |= 32; if (d.enumerable) f |= 64; }
|
|
38
|
+
if (typeof d.writable === "boolean") { f |= 128; if (d.writable) f |= 256; }
|
|
39
|
+
fn(k, d, f);
|
|
40
|
+
};
|
|
41
|
+
Object.entries(descs).forEach(([k, v]) => emit(k, v));
|
|
42
|
+
Object.getOwnPropertySymbols(descs).forEach(k => emit(k, descs[k]));
|
|
43
|
+
}`;
|
|
44
|
+
|
|
5
45
|
export default function unmarshalProperties(
|
|
6
46
|
ctx: QuickJSContext,
|
|
7
47
|
handle: QuickJSHandle,
|
|
@@ -9,53 +49,47 @@ export default function unmarshalProperties(
|
|
|
9
49
|
unmarshal: (handle: QuickJSHandle) => [unknown, boolean],
|
|
10
50
|
) {
|
|
11
51
|
consume(
|
|
12
|
-
ctx.newFunction("", (key,
|
|
52
|
+
ctx.newFunction("", (key, descHandle, flagsHandle) => {
|
|
13
53
|
const [keyName] = unmarshal(key);
|
|
14
54
|
if (typeof keyName !== "string" && typeof keyName !== "number" && typeof keyName !== "symbol")
|
|
15
55
|
return;
|
|
16
56
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
["value", true],
|
|
20
|
-
["get", true],
|
|
21
|
-
["set", true],
|
|
22
|
-
["configurable", false],
|
|
23
|
-
["enumerable", false],
|
|
24
|
-
["writable", false],
|
|
25
|
-
] as const
|
|
26
|
-
).reduce<PropertyDescriptor>((desc, [key, unmarshable]) => {
|
|
27
|
-
const h = ctx.getProp(value, key);
|
|
28
|
-
const ty = ctx.typeof(h);
|
|
29
|
-
|
|
30
|
-
if (ty === "undefined") return desc;
|
|
31
|
-
if (!unmarshable && ty === "boolean") {
|
|
32
|
-
desc[key] = ctx.dump(ctx.getProp(value, key));
|
|
33
|
-
return desc;
|
|
34
|
-
}
|
|
57
|
+
const flags = ctx.getNumber(flagsHandle);
|
|
58
|
+
const desc: PropertyDescriptor = {};
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
// Obtain value/get/set as host-owned handles via `ctx.getProp` exactly as
|
|
61
|
+
// before: these must NOT come from the (scope-borrowed, auto-disposed)
|
|
62
|
+
// callback arguments, because `unmarshal` may retain the handle in the
|
|
63
|
+
// Arena's VMMap for identity tracking. Dispose only when `unmarshal`
|
|
64
|
+
// reports the value already existed (so the fresh handle is redundant);
|
|
65
|
+
// otherwise ownership was transferred to the map and we must keep it.
|
|
66
|
+
const readHandle = (fieldKey: string): unknown => {
|
|
67
|
+
const h = ctx.getProp(descHandle, fieldKey);
|
|
68
|
+
let retained = false;
|
|
69
|
+
try {
|
|
70
|
+
const [v, alreadyExists] = unmarshal(h);
|
|
71
|
+
// When the value already existed, ownership stays with the map and the
|
|
72
|
+
// fresh handle is redundant; otherwise `unmarshal` transferred it to the
|
|
73
|
+
// map and we must keep it. A mid-flight throw in `unmarshal` also lands
|
|
74
|
+
// here with `retained` false, so the orphaned handle is disposed.
|
|
75
|
+
retained = !alreadyExists;
|
|
76
|
+
return v;
|
|
77
|
+
} finally {
|
|
78
|
+
if (!retained && h.alive) h.dispose();
|
|
39
79
|
}
|
|
40
|
-
|
|
80
|
+
};
|
|
41
81
|
|
|
42
|
-
|
|
43
|
-
|
|
82
|
+
if (flags & 1) desc.value = readHandle("value");
|
|
83
|
+
if (flags & 2) desc.get = readHandle("get") as any;
|
|
84
|
+
if (flags & 4) desc.set = readHandle("set") as any;
|
|
85
|
+
if (flags & 8) desc.configurable = !!(flags & 16);
|
|
86
|
+
if (flags & 32) desc.enumerable = !!(flags & 64);
|
|
87
|
+
if (flags & 128) desc.writable = !!(flags & 256);
|
|
44
88
|
|
|
45
89
|
Object.defineProperty(target, keyName, desc);
|
|
46
90
|
}),
|
|
47
91
|
fn => {
|
|
48
|
-
call(
|
|
49
|
-
ctx,
|
|
50
|
-
`(o, fn) => {
|
|
51
|
-
const descs = Object.getOwnPropertyDescriptors(o);
|
|
52
|
-
Object.entries(descs).forEach(([k, v]) => fn(k, v));
|
|
53
|
-
Object.getOwnPropertySymbols(descs).forEach(k => fn(k, descs[k]));
|
|
54
|
-
}`,
|
|
55
|
-
undefined,
|
|
56
|
-
handle,
|
|
57
|
-
fn,
|
|
58
|
-
).dispose();
|
|
92
|
+
call(ctx, flagsFn, undefined, handle, fn).dispose();
|
|
59
93
|
},
|
|
60
94
|
);
|
|
61
95
|
}
|
package/src/vmmap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
-
import { unwrapResult } from "./vmutil";
|
|
3
|
+
import { consume, unwrapResult } from "./vmutil";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Bidirectional map between host values and QuickJS handles.
|
|
@@ -79,7 +79,19 @@ export default class VMMap {
|
|
|
79
79
|
return v === handle || v === handle2;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const id = this._nextId
|
|
82
|
+
const id = this._nextId;
|
|
83
|
+
|
|
84
|
+
// Register on the VM side FIRST, before mutating any host-side bookkeeping.
|
|
85
|
+
// If this throws (e.g. an OOM lands inside `_mapSet`), no host map has been
|
|
86
|
+
// touched and `handle`/`handle2` are NOT retained here, so the caller still
|
|
87
|
+
// owns them and can dispose them: `set` stays atomic (all-or-nothing).
|
|
88
|
+
// `consume` disposes the id number handle even when the call throws (the raw
|
|
89
|
+
// `Lifetime.consume` skips disposal on throw, leaking it).
|
|
90
|
+
consume(this.ctx.newNumber(id), c => {
|
|
91
|
+
this._call(this._mapSet, undefined, handle, c, handle2 ?? this.ctx.undefined);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this._nextId++;
|
|
83
95
|
this._keyToId.set(key, id);
|
|
84
96
|
this._idToHandle.set(id, handle);
|
|
85
97
|
this._idToKey.set(id, key);
|
|
@@ -91,10 +103,6 @@ export default class VMMap {
|
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
this.ctx.newNumber(id).consume(c => {
|
|
95
|
-
this._call(this._mapSet, undefined, handle, c, handle2 ?? this.ctx.undefined);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
106
|
return true;
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -121,7 +129,11 @@ export default class VMMap {
|
|
|
121
129
|
|
|
122
130
|
if (!handle) return;
|
|
123
131
|
if (!handle.alive) {
|
|
124
|
-
this.
|
|
132
|
+
// The primary handle was freed by the VM, so this entry is stale. Dispose
|
|
133
|
+
// its sibling handle while evicting it: otherwise the sibling (e.g. the
|
|
134
|
+
// unwrapped handle registered alongside a proxy) is orphaned and leaks
|
|
135
|
+
// when the same value is marshalled again.
|
|
136
|
+
this.delete(key, true);
|
|
125
137
|
return;
|
|
126
138
|
}
|
|
127
139
|
|
package/src/vmutil.test.ts
CHANGED
package/src/wrapper.test.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { QuickJSHandle
|
|
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 { call, json } from "./vmutil";
|
|
6
|
+
import type {
|
|
7
|
+
SyncMode} from "./wrapper";
|
|
5
8
|
import {
|
|
6
9
|
wrap,
|
|
7
10
|
unwrap,
|
|
8
11
|
isWrapped,
|
|
9
12
|
wrapHandle,
|
|
10
13
|
unwrapHandle,
|
|
11
|
-
isHandleWrapped
|
|
12
|
-
SyncMode,
|
|
14
|
+
isHandleWrapped
|
|
13
15
|
} from "./wrapper";
|
|
14
16
|
|
|
15
17
|
test("wrap, unwrap, isWrapped", async () => {
|