quickjs-emscripten-sync 1.8.3 → 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 +9 -7
- 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/README.md
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
# quickjs-emscripten-sync
|
|
2
2
|
|
|
3
|
-
[](https://github.com/reearth/quickjs-emscripten-sync/actions/workflows/
|
|
3
|
+
[](https://github.com/reearth/quickjs-emscripten-sync/actions/workflows/ci.yml) [](https://codecov.io/gh/reearth/quickjs-emscripten-sync)
|
|
4
4
|
|
|
5
|
-
Build a secure plugin system
|
|
5
|
+
**Build a secure plugin system for web browsers.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
quickjs-emscripten-sync wraps [quickjs-emscripten](https://github.com/justjake/quickjs-emscripten) and keeps object state in sync between the host (browser or Node.js) and a sandboxed QuickJS VM, so you can exchange values across the boundary as if they were plain JavaScript objects.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Exchange and synchronize values between the host and QuickJS seamlessly:
|
|
12
|
+
- Primitives (number, boolean, string, symbol, bigint)
|
|
13
|
+
- Arrays, and objects with prototypes and any property descriptors
|
|
14
|
+
- Functions, classes, and instances
|
|
15
15
|
- Promises
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
16
|
+
- `Date`
|
|
17
|
+
- `Map` and `Set` (by value)
|
|
18
|
+
- `ArrayBuffer`, typed arrays, and `DataView` (by value)
|
|
19
|
+
- Expose host objects as globals inside the VM.
|
|
20
|
+
- Fine-grained control over which objects may be marshalled (for security).
|
|
21
|
+
- Pass objects opaquely by reference, or register host/VM object pairs to be treated as identical.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
19
24
|
|
|
20
25
|
```
|
|
21
26
|
npm install quickjs-emscripten quickjs-emscripten-sync
|
|
22
27
|
```
|
|
23
28
|
|
|
29
|
+
`quickjs-emscripten` is a peer dependency.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
24
33
|
```js
|
|
25
34
|
import { getQuickJS } from "quickjs-emscripten";
|
|
26
35
|
import { Arena } from "quickjs-emscripten-sync";
|
|
@@ -36,7 +45,7 @@ class Cls {
|
|
|
36
45
|
const ctx = (await getQuickJS()).newContext();
|
|
37
46
|
const arena = new Arena(ctx, { isMarshalable: true });
|
|
38
47
|
|
|
39
|
-
//
|
|
48
|
+
// Pass host objects to the VM and run code against them safely.
|
|
40
49
|
const exposed = {
|
|
41
50
|
Cls,
|
|
42
51
|
cls: new Cls(),
|
|
@@ -44,100 +53,74 @@ const exposed = {
|
|
|
44
53
|
};
|
|
45
54
|
arena.expose(exposed);
|
|
46
55
|
|
|
47
|
-
arena.evalCode(`cls instanceof Cls`); //
|
|
48
|
-
arena.evalCode(`cls.field`);
|
|
49
|
-
arena.evalCode(`cls.method()`);
|
|
50
|
-
arena.evalCode(`cls.field`);
|
|
56
|
+
arena.evalCode(`cls instanceof Cls`); // true
|
|
57
|
+
arena.evalCode(`cls.field`); // 0
|
|
58
|
+
arena.evalCode(`cls.method()`); // 1
|
|
59
|
+
arena.evalCode(`cls.field`); // 1
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
// Changes to a synced object are reflected on both sides.
|
|
62
|
+
arena.evalCode(`syncedCls.field`); // 0
|
|
63
|
+
exposed.syncedCls.method(); // 1
|
|
64
|
+
arena.evalCode(`syncedCls.field`); // 1
|
|
55
65
|
|
|
66
|
+
// Always dispose the arena before disposing the context.
|
|
56
67
|
arena.dispose();
|
|
57
68
|
ctx.dispose();
|
|
58
69
|
```
|
|
59
70
|
|
|
60
|
-
[
|
|
71
|
+
More runnable examples can be found in the [unit tests](src/index.test.ts).
|
|
61
72
|
|
|
62
|
-
## Operating
|
|
73
|
+
## Operating environment
|
|
63
74
|
|
|
64
75
|
- Web browsers that support WebAssembly
|
|
65
76
|
- Node.js
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
To run in a web browser, bundle your code with a tool such as webpack, Vite, or Rollup, since the WebAssembly module cannot be loaded directly via a `<script>` tag.
|
|
68
79
|
|
|
69
|
-
##
|
|
80
|
+
## How it works
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
import { getQuickJS } from "quickjs-emscripten";
|
|
73
|
-
import { Arena } from "quickjs-emscripten-sync";
|
|
82
|
+
Running untrusted JS in quickjs-emscripten is safe, but it requires you to manage a large number of handles and their lifetimes by hand. Any handle that is not freed before the context is destroyed causes an error.
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
const ctx = (await getQuickJS()).newContext();
|
|
77
|
-
|
|
78
|
-
// init Arena
|
|
79
|
-
// ⚠️ Marshaling is opt-in for security reasons.
|
|
80
|
-
// ⚠️ Be careful when activating marshalling.
|
|
81
|
-
const arena = new Arena(ctx, { isMarshalable: true });
|
|
82
|
-
|
|
83
|
-
// expose objects as global objects in QuickJS context
|
|
84
|
-
arena.expose({
|
|
85
|
-
console: {
|
|
86
|
-
log: console.log
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
arena.evalCode(`console.log("hello, world");`); // run console.log
|
|
90
|
-
arena.evalCode(`1 + 1`); // 2
|
|
91
|
-
|
|
92
|
-
// expose objects but also enable sync
|
|
93
|
-
const data = arena.sync({ hoge: "foo" });
|
|
94
|
-
arena.expose({ data });
|
|
95
|
-
|
|
96
|
-
arena.evalCode(`data.hoge = "bar"`);
|
|
97
|
-
// eval code and operations to exposed objects are automatically synced
|
|
98
|
-
console.log(data.hoge); // "bar"
|
|
99
|
-
data.hoge = "changed!";
|
|
100
|
-
console.log(arena.evalCode(`data.hoge`)); // "changed!"
|
|
101
|
-
|
|
102
|
-
// Don't forget calling arena.dispose() before disposing QuickJS context!
|
|
103
|
-
arena.dispose();
|
|
104
|
-
ctx.dispose();
|
|
105
|
-
})();
|
|
106
|
-
```
|
|
84
|
+
quickjs-emscripten-sync hides this complexity behind the `Arena` class:
|
|
107
85
|
|
|
108
|
-
|
|
86
|
+
- It tracks every handle generated through the context and frees them for you when the arena is disposed.
|
|
87
|
+
- It **marshals** host objects into VM handles and **unmarshals** VM handles back into host objects, recursing through properties and the prototype chain so the conversion is transparent. When a function is called, its arguments and `this` are converted for the side where the function is defined, and the return value is converted back for the caller.
|
|
88
|
+
- Most objects are wrapped in proxies during conversion, so that `set`, `delete`, and `defineProperty` operations are synchronized between the host and the VM.
|
|
109
89
|
|
|
110
|
-
|
|
90
|
+
> **Marshal** = converting a host object into a VM handle.
|
|
91
|
+
> **Unmarshal** = converting a VM handle back into a host object.
|
|
111
92
|
|
|
112
|
-
|
|
93
|
+
## Controlling what gets marshalled
|
|
113
94
|
|
|
114
|
-
|
|
95
|
+
You can control whether (and how) host objects are marshalled into the VM. This matters for security: exposing the host's global object to the VM, for example, is both heavy and dangerous.
|
|
96
|
+
|
|
97
|
+
Use the `isMarshalable` option to limit it. When the callback returns `false`, `undefined` is passed to the VM instead of the object.
|
|
115
98
|
|
|
116
99
|
```js
|
|
117
100
|
import { Arena, complexity } from "quickjs-emscripten-sync";
|
|
118
101
|
|
|
119
102
|
const arena = new Arena(ctx, {
|
|
120
103
|
isMarshalable: (target: any) => {
|
|
121
|
-
//
|
|
104
|
+
// Never pass globalThis to the VM.
|
|
122
105
|
if (target === window) return false;
|
|
123
|
-
// complexity
|
|
106
|
+
// complexity() helps detect objects that are too heavy to pass.
|
|
124
107
|
if (complexity(target, 30) >= 30) return false;
|
|
125
|
-
return true; //
|
|
126
|
-
}
|
|
108
|
+
return true; // anything else is fine
|
|
109
|
+
},
|
|
127
110
|
});
|
|
128
111
|
|
|
129
|
-
arena.evalCode(`a => a === undefined`)({});
|
|
130
|
-
arena.evalCode(`a => a === undefined`)(window); //
|
|
112
|
+
arena.evalCode(`a => a === undefined`)({}); // false
|
|
113
|
+
arena.evalCode(`a => a === undefined`)(window); // true
|
|
131
114
|
arena.evalCode(`a => a === undefined`)(document); // true
|
|
132
115
|
```
|
|
133
116
|
|
|
134
|
-
|
|
117
|
+
See [`isMarshalable`](#options) for all accepted values.
|
|
135
118
|
|
|
136
|
-
## Security
|
|
119
|
+
## Security
|
|
137
120
|
|
|
138
|
-
QuickJS
|
|
121
|
+
⚠️ QuickJS runs in an environment isolated from the browser, so untrusted code can generally be executed safely. However, there are edge cases where objects you expose through quickjs-emscripten-sync can break that isolation.
|
|
139
122
|
|
|
140
|
-
quickjs-emscripten-sync cannot
|
|
123
|
+
quickjs-emscripten-sync cannot detect every such case, so **be very careful and deliberate about what you expose to the VM.**
|
|
141
124
|
|
|
142
125
|
### Case 1: Prototype pollution
|
|
143
126
|
|
|
@@ -146,165 +129,259 @@ import { set } from "lodash-es";
|
|
|
146
129
|
|
|
147
130
|
arena.expose({
|
|
148
131
|
danger: (keys, value) => {
|
|
149
|
-
//
|
|
150
|
-
set({}, keys, value)
|
|
151
|
-
}
|
|
132
|
+
// Calling this from the VM can pollute prototypes in the host.
|
|
133
|
+
set({}, keys, value);
|
|
134
|
+
},
|
|
152
135
|
});
|
|
153
136
|
|
|
154
137
|
arena.evalCode(`danger("__proto__.a", () => { /* injected */ })`);
|
|
155
138
|
```
|
|
156
139
|
|
|
157
|
-
### Case 2: Unintended HTTP
|
|
158
|
-
|
|
159
|
-
It is very dangerous to expose or use directly or indirectly the `window` object, `localStorage`, `fetch`, `XMLHttpRequest` ...
|
|
140
|
+
### Case 2: Unintended HTTP requests and DOM access
|
|
160
141
|
|
|
161
|
-
|
|
142
|
+
Exposing `window`, `localStorage`, `fetch`, `XMLHttpRequest`, and similar APIs — directly or indirectly — is very dangerous. It lets sandboxed code read local storage, send arbitrary HTTP requests, manipulate the DOM, and mount XSS-style attacks.
|
|
162
143
|
|
|
163
144
|
```js
|
|
164
145
|
arena.expose({
|
|
165
|
-
//
|
|
146
|
+
// Calling this from the VM can trigger unintended HTTP requests.
|
|
166
147
|
danger: (url, body) => {
|
|
167
148
|
fetch(url, {
|
|
168
149
|
method: "POST",
|
|
169
|
-
headers: {
|
|
170
|
-
|
|
171
|
-
},
|
|
172
|
-
body: JSON.stringify(body)
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
body: JSON.stringify(body),
|
|
173
152
|
});
|
|
174
|
-
}
|
|
153
|
+
},
|
|
175
154
|
});
|
|
176
155
|
|
|
177
156
|
arena.evalCode(`danger("/api", { dangerous: true })`);
|
|
178
157
|
```
|
|
179
158
|
|
|
180
|
-
By default, quickjs-emscripten-sync
|
|
159
|
+
By default, quickjs-emscripten-sync does not block any marshalling. Because the host has many built-in objects, the `isMarshalable` option alone cannot prevent every dangerous case — design your exposed surface carefully.
|
|
181
160
|
|
|
182
161
|
## API
|
|
183
162
|
|
|
184
163
|
### `Arena`
|
|
185
164
|
|
|
186
|
-
|
|
165
|
+
`Arena` manages all handles generated by quickjs-emscripten and automatically converts objects between the host and the VM.
|
|
187
166
|
|
|
188
167
|
#### `new Arena(ctx: QuickJSContext, options?: Options)`
|
|
189
168
|
|
|
190
|
-
|
|
169
|
+
Creates a new arena. `ctx` must be a context created with `quickjs.newContext()`.
|
|
191
170
|
|
|
192
|
-
|
|
171
|
+
> ⚠️ Marshalling is opt-in for security reasons. Enable it deliberately.
|
|
172
|
+
|
|
173
|
+
##### Options
|
|
193
174
|
|
|
194
175
|
```ts
|
|
195
176
|
type Options = {
|
|
196
|
-
/**
|
|
177
|
+
/**
|
|
178
|
+
* Controls whether and how an object is marshalled. By default, objects are
|
|
179
|
+
* marshalled via JSON. See the table below for accepted values.
|
|
180
|
+
*/
|
|
197
181
|
isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
|
|
198
|
-
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Pre-registered pairs of objects that are treated as identical between the
|
|
185
|
+
* host and the VM, and reused automatically during conversion. Defaults to
|
|
186
|
+
* `defaultRegisteredObjects`.
|
|
199
187
|
*
|
|
200
|
-
* Instead of a string
|
|
188
|
+
* Instead of a code string you may pass a QuickJSHandle directly; in that
|
|
189
|
+
* case you must dispose of it yourself when destroying the VM.
|
|
201
190
|
*/
|
|
202
191
|
registeredObjects?: Iterable<[any, QuickJSHandle | string]>;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
customUnmarshaller?: Iterable<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
192
|
+
|
|
193
|
+
/** Custom functions that convert a host object into a QuickJS handle. */
|
|
194
|
+
customMarshaller?: Iterable<(target: unknown, ctx: QuickJSContext) => QuickJSHandle | undefined>;
|
|
195
|
+
|
|
196
|
+
/** Custom functions that convert a QuickJS handle into a host object. */
|
|
197
|
+
customUnmarshaller?: Iterable<(target: QuickJSHandle, ctx: QuickJSContext) => any>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Returns whether an object may be wrapped with a proxy. If it returns
|
|
201
|
+
* `false`, the object cannot be synchronized even when `arena.sync` is used.
|
|
202
|
+
*/
|
|
212
203
|
isWrappable?: (target: any) => boolean;
|
|
213
|
-
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns whether a QuickJS handle may be wrapped with a proxy. If it returns
|
|
207
|
+
* `false`, the handle cannot be synchronized even when `arena.sync` is used.
|
|
208
|
+
*/
|
|
214
209
|
isHandleWrappable?: (handle: QuickJSHandle, ctx: QuickJSContext) => boolean;
|
|
215
|
-
|
|
210
|
+
|
|
211
|
+
/** Compatibility shim for quickjs-emscripten prior to v0.15. */
|
|
216
212
|
compat?: boolean;
|
|
217
|
-
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Enables sync mode globally (default `true`). When `false`, objects are not
|
|
216
|
+
* wrapped with proxies and marshalled handles are disposed right after use:
|
|
217
|
+
* `arena.sync` has no effect, but objects are not retained for the arena's
|
|
218
|
+
* whole lifetime. Useful to avoid memory growth when frequently exchanging
|
|
219
|
+
* short-lived objects.
|
|
220
|
+
*/
|
|
221
|
+
syncEnabled?: boolean;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns whether an object should be passed to the VM by reference (as an
|
|
225
|
+
* opaque HostRef) instead of being marshalled by value or proxy. See
|
|
226
|
+
* "Passing objects by reference" below.
|
|
227
|
+
*/
|
|
228
|
+
marshalByReference?: (target: any) => boolean;
|
|
229
|
+
};
|
|
218
230
|
```
|
|
219
231
|
|
|
220
|
-
|
|
232
|
+
###### `isMarshalable`
|
|
233
|
+
|
|
234
|
+
Determines how objects are marshalled from the host into the VM. **Keep this as restrictive as possible — loosening it can reduce your application's security.** See [Security](#security).
|
|
221
235
|
|
|
222
|
-
|
|
236
|
+
| Value | Behaviour |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `"json"` | **Default.** The object is serialized to JSON on the host and parsed in the VM. Functions and classes are lost. Safe. |
|
|
239
|
+
| `false` | The object is never marshalled; `undefined` is passed instead. Safe. |
|
|
240
|
+
| `(target) => boolean \| "json"` | **Recommended.** Decide per object. Return `true` to fully marshal, `"json"` for JSON, or `false` to skip. |
|
|
241
|
+
| `true` | The object is always fully marshalled. **Risky — not recommended.** |
|
|
223
242
|
|
|
224
|
-
|
|
225
|
-
- `false` (safety): Target object will not be always marshalled as `undefined`.
|
|
226
|
-
- `(target: any) => boolean | "json"` (recoomended): You can control marshalling mode for each objects. If you want to do marshalling, usually use this method. Allow partial marshalling by returning `true` only for some objects.
|
|
227
|
-
- `true` (**risky and not recommended**): Target object will be always marshaled. This setting may reduce security.
|
|
243
|
+
###### `registeredObjects`
|
|
228
244
|
|
|
229
|
-
|
|
245
|
+
Pre-register host/VM object pairs that should be treated as identical during conversion. Defaults to [`defaultRegisteredObjects`](src/default.ts). To extend it:
|
|
230
246
|
|
|
231
247
|
```js
|
|
232
248
|
import { defaultRegisteredObjects } from "quickjs-emscripten-sync";
|
|
233
249
|
|
|
234
250
|
const arena = new Arena(ctx, {
|
|
235
|
-
registeredObjects: [
|
|
236
|
-
...defaultRegisteredObjects,
|
|
237
|
-
[Math, "Math"]
|
|
238
|
-
]
|
|
251
|
+
registeredObjects: [...defaultRegisteredObjects, [Math, "Math"]],
|
|
239
252
|
});
|
|
240
253
|
```
|
|
241
254
|
|
|
242
|
-
Instead of a string
|
|
255
|
+
Instead of a code string you may pass a QuickJSHandle directly; in that case you must dispose of it yourself when destroying the context.
|
|
243
256
|
|
|
244
|
-
|
|
257
|
+
###### `marshalByReference`
|
|
245
258
|
|
|
246
|
-
|
|
259
|
+
Return `true` for objects you want to pass to the VM as an opaque reference (a [HostRef](https://github.com/justjake/quickjs-emscripten)) instead of marshalling their contents. The VM cannot read or mutate such objects, but it can hold them and pass them back to the host, where they resolve to the **original** object (identity is preserved). This is useful for handing the sandbox a host resource — a class instance, a DOM node, and so on — that it should carry around opaquely rather than copy.
|
|
247
260
|
|
|
248
|
-
|
|
261
|
+
```js
|
|
262
|
+
const secret = { token: "..." };
|
|
263
|
+
const arena = new Arena(ctx, {
|
|
264
|
+
isMarshalable: true,
|
|
265
|
+
marshalByReference: target => target === secret,
|
|
266
|
+
});
|
|
249
267
|
|
|
250
|
-
|
|
268
|
+
arena.expose({
|
|
269
|
+
getSecret: () => secret,
|
|
270
|
+
useSecret: s => s.token, // the host receives the original `secret`
|
|
271
|
+
});
|
|
251
272
|
|
|
252
|
-
|
|
273
|
+
arena.evalCode(`useSecret(getSecret())`); // "..." (the VM never sees the contents)
|
|
274
|
+
```
|
|
253
275
|
|
|
254
|
-
|
|
276
|
+
#### `evalCode<T = any>(code: string): T`
|
|
277
|
+
|
|
278
|
+
Evaluate JS code in the VM and return the result as a host object. Errors thrown during evaluation are converted and re-thrown on the host.
|
|
279
|
+
|
|
280
|
+
#### `evalModule<T = any>(code: string, filename?: string): T | Promise<T>`
|
|
281
|
+
|
|
282
|
+
Evaluate ES module code and return the module's exports. Requires quickjs-emscripten >= 0.29.0. Returns a promise if the module uses top-level `await`.
|
|
255
283
|
|
|
256
284
|
#### `expose(obj: { [k: string]: any })`
|
|
257
285
|
|
|
258
|
-
Expose objects as
|
|
286
|
+
Expose host objects as globals in the VM. Exposed objects are not synchronized by default; to sync one, wrap it with `sync` first and expose the wrapped object.
|
|
259
287
|
|
|
260
|
-
|
|
261
|
-
|
|
288
|
+
```js
|
|
289
|
+
arena.expose({ console: { log: console.log } });
|
|
290
|
+
arena.evalCode(`console.log("hello, world")`);
|
|
291
|
+
```
|
|
262
292
|
|
|
263
293
|
#### `sync<T>(target: T): T`
|
|
264
294
|
|
|
265
|
-
|
|
295
|
+
Enable synchronization for an object and return a proxy-wrapped version of it. **Use the returned value** — mutating the original object does not propagate changes. Conversely, `set` and `delete` on the wrapped object (from either side) are synchronized.
|
|
266
296
|
|
|
267
|
-
|
|
297
|
+
```js
|
|
298
|
+
const data = arena.sync({ hoge: "foo" });
|
|
299
|
+
arena.expose({ data });
|
|
300
|
+
|
|
301
|
+
arena.evalCode(`data.hoge = "bar"`);
|
|
302
|
+
console.log(data.hoge); // "bar"
|
|
303
|
+
|
|
304
|
+
data.hoge = "changed!";
|
|
305
|
+
console.log(arena.evalCode(`data.hoge`)); // "changed!"
|
|
306
|
+
```
|
|
268
307
|
|
|
269
308
|
#### `register(target: any, code: string | QuickJSHandle)`
|
|
270
309
|
|
|
271
|
-
Register a
|
|
310
|
+
Register a single host/VM object pair to be treated as identical.
|
|
272
311
|
|
|
273
|
-
#### `
|
|
312
|
+
#### `registerAll(map: Iterable<[any, string | QuickJSHandle]>)`
|
|
274
313
|
|
|
275
|
-
|
|
314
|
+
Call `register` for each pair.
|
|
276
315
|
|
|
277
|
-
#### `unregister(target: any)`
|
|
316
|
+
#### `unregister(target: any, dispose?: boolean)`
|
|
278
317
|
|
|
279
|
-
|
|
318
|
+
Remove a pair registered via the `registeredObjects` option or `register`.
|
|
280
319
|
|
|
281
|
-
#### `unregisterAll(targets: Iterable<any
|
|
320
|
+
#### `unregisterAll(targets: Iterable<any>, dispose?: boolean)`
|
|
282
321
|
|
|
283
|
-
|
|
322
|
+
Call `unregister` for each target.
|
|
284
323
|
|
|
285
|
-
|
|
324
|
+
#### `dispose()`
|
|
286
325
|
|
|
287
|
-
|
|
326
|
+
Dispose of the arena and the handles it manages. This does **not** dispose the context itself — dispose that manually, and always after the arena.
|
|
288
327
|
|
|
289
|
-
|
|
328
|
+
`Arena` also implements `Symbol.dispose`, so a `using` declaration disposes it automatically:
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
{
|
|
332
|
+
using arena = new Arena(ctx, { isMarshalable: true });
|
|
333
|
+
arena.evalCode(`1 + 1`);
|
|
334
|
+
} // arena.dispose() runs here
|
|
335
|
+
ctx.dispose();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### `executePendingJobs(maxJobsToExecute?: number): number`
|
|
339
|
+
|
|
340
|
+
Like `ctx.runtime.executePendingJobs()`, but converts and re-throws errors thrown during evaluation.
|
|
290
341
|
|
|
291
|
-
|
|
342
|
+
#### Runtime limits and stats
|
|
292
343
|
|
|
293
|
-
|
|
344
|
+
These forward to the underlying runtime and are useful for sandboxing untrusted code:
|
|
294
345
|
|
|
295
|
-
|
|
346
|
+
- `setMemoryLimit(limitBytes: number): void` — cap runtime memory (`-1` to remove the limit).
|
|
347
|
+
- `setMaxStackSize(stackSize: number): void` — cap stack size in bytes (`0` to remove the limit).
|
|
348
|
+
- `getMemoryUsage(): object` — detailed memory statistics.
|
|
349
|
+
- `dumpMemoryUsage(): string` — a human-readable memory report.
|
|
296
350
|
|
|
297
|
-
|
|
351
|
+
### `AsyncArena`
|
|
298
352
|
|
|
299
|
-
quickjs-emscripten
|
|
300
|
-
And it will automatically "marshal" objects as handles and "unmarshal" handles as objects to enable seamless data exchange between the browser and QuickJS. It recursively traverses the object properties and prototype chain to transform objects. A function is called after its arguments and this arg are automatically converted for the environment in which the function is defined. The return value will be automatically converted to match the environment of the caller.
|
|
301
|
-
Most objects are wrapped by proxies during conversion, allowing "set" and "delete" operations on objects to be synchronized between the browser and QuickJS.
|
|
353
|
+
`AsyncArena` extends `Arena` for use with a [`QuickJSAsyncContext`](https://github.com/justjake/quickjs-emscripten). It adds `evalCodeAsync`, the async counterpart to `evalCode`, so code that relies on asynchronous module loading can be evaluated.
|
|
302
354
|
|
|
303
|
-
|
|
355
|
+
```js
|
|
356
|
+
import { newAsyncContext } from "quickjs-emscripten";
|
|
357
|
+
import { AsyncArena } from "quickjs-emscripten-sync";
|
|
358
|
+
|
|
359
|
+
const ctx = await newAsyncContext();
|
|
360
|
+
const arena = new AsyncArena(ctx, { isMarshalable: true });
|
|
361
|
+
|
|
362
|
+
await arena.evalCodeAsync(`1 + 2`); // 3
|
|
363
|
+
|
|
364
|
+
arena.dispose();
|
|
365
|
+
ctx.dispose();
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `evalCodeAsync<T = any>(code: string, filename?: string): Promise<T>`
|
|
369
|
+
|
|
370
|
+
Evaluate JS code asynchronously and return the result on the host. Like `evalCode`, it converts and re-throws errors thrown during evaluation.
|
|
304
371
|
|
|
305
|
-
|
|
372
|
+
### `defaultRegisteredObjects: [any, string][]`
|
|
373
|
+
|
|
374
|
+
The default value of the `registeredObjects` option.
|
|
375
|
+
|
|
376
|
+
### `complexity(target: any, max?: number): number`
|
|
377
|
+
|
|
378
|
+
Measure the complexity of an object by traversing its fields and prototype chain. Each object and function counts as 1; primitives are not counted. If `max` is given, traversal stops once the count reaches `max` and returns `max` — handy for cheaply detecting objects that are too heavy to marshal.
|
|
306
379
|
|
|
307
|
-
|
|
380
|
+
## Limitations
|
|
381
|
+
|
|
382
|
+
### Class constructors
|
|
383
|
+
|
|
384
|
+
When a class is instantiated inside the VM, `this` (and `new.target`) cannot be fully proxied during the constructor call. quickjs-emscripten-sync runs the host constructor and then copies the resulting fields onto the VM-side `this`, so constructors that rely on the live `this` during construction may behave unexpectedly in edge cases.
|
|
308
385
|
|
|
309
386
|
```js
|
|
310
387
|
class Cls {
|
|
@@ -317,9 +394,13 @@ arena.expose({ Cls });
|
|
|
317
394
|
arena.evalCode(`new Cls()`); // Cls { hoge: "foo" }
|
|
318
395
|
```
|
|
319
396
|
|
|
320
|
-
|
|
397
|
+
### Operation synchronization
|
|
398
|
+
|
|
399
|
+
Only the `set`, `deleteProperty`, and `defineProperty` operations on objects are synchronized. Other operations (for example `Object.setPrototypeOf`) are not propagated to the other side.
|
|
400
|
+
|
|
401
|
+
### Marshalling by value
|
|
321
402
|
|
|
322
|
-
|
|
403
|
+
`Date`, `Map`, `Set`, `ArrayBuffer`, and typed arrays are marshalled by value (a snapshot copy is created on the other side). They are not proxied, so later mutations are not synchronized, and self-referential `Map`/`Set` are not supported.
|
|
323
404
|
|
|
324
405
|
## License
|
|
325
406
|
|