vercel-labs/quickjs-wasi
GitHub: vercel-labs/quickjs-wasi
一个基于 WebAssembly 的可快照化 QuickJS 运行时,支持将整个虚拟机状态(含 pending Promise)序列化保存并在新实例中恢复,适用于沙箱执行、确定性重放和边缘计算冷启动优化等场景。
Stars: 50 | Forks: 3
# quickjs-wasi
一个通过 WebAssembly 实现的可快照化 JavaScript 运行时。运行编译为 WASM 的 [QuickJS](https://github.com/quickjs-ng/quickjs),具备**快照整个 VM 状态**(包括 pending 状态的 promise)并**在全新的 WASM 实例中恢复**的能力。
## 安装
```
npm install quickjs-wasi
```
## 用法
### 加载 WASM 二进制文件
调用者负责提供 WASM 字节(或预编译的 `WebAssembly.Module`)—— quickjs-wasi 不执行任何隐式的文件系统或网络 I/O。本包在 `quickjs-wasi/quickjs.wasm` 子路径下提供了该二进制文件,可通过您的环境首选的机制进行解析。
Node.js(从磁盘读取):
```
import { readFile } from 'node:fs/promises';
const wasmBytes = await readFile(new URL(import.meta.resolve('quickjs-wasi/quickjs.wasm')));
// or with require.resolve in CJS:
// readFileSync(require.resolve('quickjs-wasi/quickjs.wasm'))
```
浏览器(`fetch` + 流式编译):
```
const wasmModule = await WebAssembly.compileStreaming(fetch('/quickjs.wasm'));
```
Vite / 使用 `?url` 导入后缀的打包工具:
```
import wasmUrl from 'quickjs-wasi/quickjs.wasm?url';
const wasmModule = await WebAssembly.compileStreaming(fetch(wasmUrl));
```
强烈建议编译一次 `WebAssembly.Module` 并在多个 VM 中复用——从已编译模块实例化的速度远快于在每次调用 `QuickJS.create()` 时重新编译字节。
### 基础执行
`QuickJS` 和 `JSValueHandle` 都实现了 `Symbol.dispose`,因此您可以使用 `using` 声明进行自动清理:
```
import { QuickJS } from 'quickjs-wasi';
{
using vm = await QuickJS.create({ wasm: wasmBytes });
// Evaluate code — handles are auto-disposed with `using`
using result = vm.evalCode('1 + 2');
console.log(result.toNumber()); // 3
} // vm and result are automatically disposed here
```
### 操作值
```
using vm = await QuickJS.create(wasmBytes);
// Create values — `using` ensures they're disposed at end of scope
{
using str = vm.newString('hello');
using num = vm.newNumber(42);
using big = vm.newBigInt(9007199254740993n);
vm.setProp(vm.global, 'message', str);
}
// Read back the value
using msg = vm.evalCode('message');
console.log(msg.toString()); // "hello"
// Convert host values to QuickJS handles (and back)
using handle = vm.hostToHandle({ x: 1, y: [2, 3] });
const dumped = vm.dump(handle); // { x: 1, y: [2, 3] }
// `consume()` is useful for inline one-liners
const value = vm.evalCode('1 + 2').consume(h => h.toNumber()); // 3
```
### 宿主函数
注册由宿主(Node.js)回调支持的 JavaScript 函数:
```
using vm = await QuickJS.create(wasmBytes);
// The first argument to the callback is always `this`
{
using add = vm.newFunction('add', (...args) => {
return vm.newNumber(args[0].toNumber() + args[1].toNumber());
});
vm.setProp(vm.global, 'add', add);
}
using result = vm.evalCode('add(3, 4)');
console.log(result.toNumber()); // 7
```
### Promise 和异步宿主函数
将异步宿主操作桥接到 QuickJS 沙箱中:
```
using vm = await QuickJS.create(wasmBytes);
// Create an async host function that returns a promise to QuickJS
{
using dnsResolve = vm.newFunction('dnsResolve', (...args) => {
const hostname = args[0].toString();
const deferred = vm.newPromise();
// Do real async work on the host side
dns.resolve4(hostname).then(
(addresses) => {
deferred.resolve(vm.newString(addresses[0]));
vm.executePendingJobs(); // drain the QuickJS job queue
},
(err) => {
deferred.reject(vm.newError(err));
vm.executePendingJobs();
}
);
return deferred.handle; // return the QuickJS promise
});
vm.setProp(vm.global, 'dnsResolve', dnsResolve);
}
```
### 错误处理
```
using vm = await QuickJS.create(wasmBytes);
// evalCode() throws a JSException if the evaluated code throws
try {
vm.evalCode('throw new TypeError("bad")');
} catch (err) {
console.log(err.name); // "TypeError"
console.log(err.message); // "bad"
console.log(err.stack); // QuickJS stack trace
}
// Create errors from host Error objects (preserves name, message, stack)
{
using errHandle = vm.newError(new RangeError('out of bounds'));
vm.setProp(vm.global, 'hostError', errHandle);
}
```
### WASI 覆盖
`wasi` 选项允许您覆盖任何 `wasi_snapshot_preview1` 宿主函数。它是一个工厂函数,接收 WASM 线性内存并返回一个包含覆盖函数的对象。覆盖项会同时应用于主模块和所有已加载的扩展。
这对于确定性执行非常有用——QuickJS 使用 [xorshift64*](https://en.wikipedia.org/wiki/Xorshift) PRNG,在上下文创建期间通过时钟值进行一次种子设定。覆盖 `clock_time_get` 以同时控制 `Date.now()` 和 `Math.random()` 的种子:
```
const fixedClock = (memory: WebAssembly.Memory) => ({
clock_time_get(_clockId: number, _precision: bigint, resultPtr: number) {
new DataView(memory.buffer).setBigUint64(resultPtr, 1700000000000n * 1_000_000n, true);
return 0;
},
});
using vm1 = await QuickJS.create({ wasm: wasmBytes, wasi: fixedClock });
using vm2 = await QuickJS.create({ wasm: wasmBytes, wasi: fixedClock });
vm1.evalCode('Math.random()').consume(h => h.toNumber());
// => 0.8130834347906803
vm2.evalCode('Math.random()').consume(h => h.toNumber());
// => 0.8130834347906803 (identical)
```
覆盖 `random_get` 以控制 crypto 扩展的 RNG:
```
using vm = await QuickJS.create({
wasm: wasmBytes,
wasi: (memory) => ({
random_get(bufPtr: number, bufLen: number) {
new Uint8Array(memory.buffer, bufPtr, bufLen).fill(0x42); // deterministic
return 0;
},
}),
extensions: [cryptoExtension],
});
```
在调用之间也可以推进时间以模拟真实行为:
```
let currentTime = 1700000000000n;
using vm = await QuickJS.create({
wasm: wasmBytes,
wasi: (memory) => ({
clock_time_get(_clockId: number, _precision: bigint, resultPtr: number) {
new DataView(memory.buffer).setBigUint64(resultPtr, currentTime * 1_000_000n, true);
return 0;
},
}),
});
vm.evalCode('Date.now()').consume(h => h.toNumber()); // 1700000000000
currentTime += 1000n; // advance 1 second
vm.evalCode('Date.now()').consume(h => h.toNumber()); // 1700000001000
```
### 内存限制
限制 QuickJS 运行时可以分配的内存量。超出限制时,分配将失败并作为 JS 异常抛出:
```
using vm = await QuickJS.create({
wasm: wasmBytes,
memoryLimit: 4 * 1024 * 1024, // 4 MB
});
vm.evalCode(`
try {
const huge = new Array(10000000).fill("x".repeat(1000));
} catch (e) {
console.log(e.message); // allocation failure
}
`);
```
该限制会在 `QuickJS.restore()` 之后重新应用,因此您可以为恢复的 VM 使用与原始 VM 不同的限制。
### 中断处理程序
防止无限循环并强制执行超时:
```
const start = Date.now();
using vm = await QuickJS.create({
wasm: wasmBytes,
interruptHandler: () => {
// Return true to interrupt — called periodically during JS execution
return Date.now() - start > 5000; // 5 second timeout
},
});
try {
vm.evalCode('while (true) {}');
} catch (err) {
// JSException — interrupted
err.dispose();
}
// VM is still usable after an interrupt
vm.evalCode('1 + 2').consume(h => h.toNumber()); // 3
```
该处理程序大约每条 JS 字节码指令调用一次,因此它应该保持快速。当它返回 `true` 时,当前执行将被中断并抛出 `JSException`。VM 在中断后仍可继续使用。
### 时区偏移
默认情况下,沙箱内的 `Date` 会映射宿主环境的时区。您可以使用固定偏移量或动态回调来覆盖它:
```
// Fixed offset: UTC-8 (480 minutes west of UTC)
using vm = await QuickJS.create({
wasm: wasmBytes,
timezoneOffset: 480,
});
vm.evalCode('new Date().getTimezoneOffset()').consume(h => h.toNumber()); // 480
```
```
// Force UTC (offset 0)
using vm = await QuickJS.create({
wasm: wasmBytes,
timezoneOffset: 0,
});
```
```
// Dynamic callback for custom DST-aware logic
using vm = await QuickJS.create({
wasm: wasmBytes,
timezoneOffset: (timeSecs) => {
// Return offset in minutes (getTimezoneOffset convention: positive = west of UTC)
return new Date(timeSecs * 1000).getTimezoneOffset();
},
});
```
`timezoneOffset` 选项接受:
- **`'host'`**(默认)——映射宿主的时区,包括 DST 转换。
- **一个数字**——使用 `getTimezoneOffset()` 符号约定的固定 UTC 偏移分钟数(正值表示 UTC 以西,例如 `480` 表示 UTC-8)。
- **回调 `(timeSecs: number) => number`**——以自纪元以来的秒数调用,必须返回以分钟为单位的偏移量。适用于自定义时区逻辑。每当 QuickJS 需要在 UTC 和本地时间之间进行转换时(例如 `getHours()`、`toString()`、`new Date(year, month, ...)`、`getTimezoneOffset()`),都会调用此回调,因此单次 Date 操作可能会多次调用它。
### 快照与恢复
核心亮点——快照整个 VM 状态并在稍后恢复:
```
let snapshot: Snapshot;
{
using vm = await QuickJS.create(wasmBytes);
// Build up some state, including a pending promise
vm.evalCode(`
globalThis.counter = 0;
let __resolve;
globalThis.pendingWork = new Promise(r => { __resolve = r; });
globalThis.__resolve = __resolve;
globalThis.pendingWork.then(value => {
globalThis.counter = value;
});
`).dispose();
vm.executePendingJobs();
// Take a snapshot
snapshot = vm.snapshot();
}
// Serialize to a binary buffer for storage (apply gzip on top for best compression)
const bytes = QuickJS.serializeSnapshot(snapshot);
await storage.put('snapshots/run-123', bytes);
// ... time passes, maybe a different process entirely ...
// Deserialize and restore
const loaded = await storage.get('snapshots/run-123');
const restored = QuickJS.deserializeSnapshot(loaded);
{
using vm = await QuickJS.restore(restored, wasmBytes);
// The pending promise still exists — resolve it
using resolve = vm.global.getProp('__resolve');
using arg = vm.newNumber(42);
vm.callFunction(resolve, vm.undefined, arg).dispose();
vm.executePendingJobs();
// The .then handler ran in the restored VM
using counter = vm.global.getProp('counter');
console.log(counter.toNumber()); // 42
}
```
### 恢复后的宿主回调
使用 `newFunction()` 注册的宿主函数以它们的名称作为键,该名称会被固化到快照中。恢复之后,按名称重新注册回调:
```
let snapshot: Snapshot;
{
using vm = await QuickJS.create(wasmBytes);
using fn = vm.newFunction('hostAdd', (...args) => {
return vm.newNumber(args[0].toNumber() + args[1].toNumber());
});
vm.setProp(vm.global, 'hostAdd', fn);
snapshot = vm.snapshot();
}
{
// After restore — re-register by name
using vm = await QuickJS.restore(snapshot, wasmBytes);
vm.registerHostCallback('hostAdd', (...args) => {
return vm.newNumber(args[0].toNumber() + args[1].toNumber());
});
// hostAdd() works again
using result = vm.evalCode('hostAdd(100, 200)');
console.log(result.toNumber()); // 300
}
```
注意:每次调用 `newFunction()` 必须使用唯一的名称。尝试注册两个同名的宿主函数将抛出错误。
### 原生 WASM 扩展
加载编译为 WASM 共享库的基于 C 的扩展。扩展直接链接到 QuickJS C API,零编组开销——它们共享相同的线性内存,并且可以注册自定义类、原型和全局对象。
本包附带六个预构建扩展,每个都可以作为子路径导出使用。与主 `quickjs.wasm` 二进制文件一样,调用者负责加载字节:
| 扩展 | 子路径 | 添加内容 |
|-----------|---------|------|
| URL | `quickjs-wasi/url.so` | `URL`,`URLSearchParams` (ada-url) |
| Encoding | `quickjs-wasi/encoding.so` | `TextEncoder`,`TextDecoder` |
| Base64 | `quickjs-wasi/base64.so` | `atob`,`btoa`,`Uint8Array` base64/hex |
| Headers | `quickjs-wasi/headers.so` | `Headers` |
| Crypto | `quickjs-wasi/crypto.so` | `crypto.subtle`,`crypto.getRandomValues` |
| Structured Clone | `quickjs-wasi/structured-clone.so` | `structuredClone` |
Node.js:
```
import { readFile } from 'node:fs/promises';
import { QuickJS } from 'quickjs-wasi';
const urlExtBytes = await readFile(
new URL(import.meta.resolve('quickjs-wasi/url.so')),
);
using vm = await QuickJS.create({
wasm: wasmBytes,
extensions: [{ name: 'url', wasm: urlExtBytes }],
});
using result = vm.evalCode(`
const url = new URL('https://example.com:8080/api?key=value#section');
url.hostname // 'example.com'
`);
```
Vite / 打包工具:
```
import urlSoUrl from 'quickjs-wasi/url.so?url';
const urlExtBytes = await fetch(urlSoUrl).then((r) => r.arrayBuffer());
```
扩展在快照/恢复后依然有效——恢复时提供相同的扩展:
```
const snapshot = vm.snapshot();
using vm2 = await QuickJS.restore(snapshot, {
wasm: wasmBytes,
extensions: [{ name: 'url', wasm: urlExtBytes }],
});
// URL objects created before the snapshot still work
```
有关如何构建自己的扩展、动态链接的工作原理以及已知的限制,请参阅 [EXTENSIONS.md](./EXTENSIONS.md)。
## API 参考
### `QuickJS` (VM 实例)
| 方法 | 描述 |
|--------|-------------|
| `QuickJS.create(options?)` | 创建一个全新的 VM 实例 |
| `QuickJS.restore(snapshot, options?)` | 从快照恢复 VM |
| `QuickJS.serializeSnapshot(snapshot)` | 将快照序列化为带版本的二进制 `Uint8Array` |
| `QuickJS.deserializeSnapshot(data)` | 从二进制 `Uint8Array` 反序列化快照 |
| `vm.evalCode(code, filename?)` | 执行 JS 代码,返回 `JSValueHandle`(出错时抛出 `JSException`) |
| `vm.callFunction(fn, this, ...args)` | 调用 QuickJS 函数(出错时抛出 `JSException`) |
| `vm.executePendingJobs()` | 排空 promise 微任务队列 |
| `vm.newString(str)` | 创建一个字符串值 |
| `vm.newNumber(num)` | 创建一个数值 |
| `vm.newBigInt(val)` | 创建一个 BigInt 值 |
| `vm.newObject()` | 创建一个空对象 |
| `vm.newArray()` | 创建一个空数组 |
| `vm.newSymbolFor(description)` | 创建一个全局 symbol(`Symbol.for(description)`) |
| `vm.newArrayBuffer(data)` | 从宿主 `ArrayBuffer` 或 `Uint8Array` 创建一个 ArrayBuffer |
| `vm.newUint8Array(data)` | 从宿主 `Uint8Array` 创建一个 Uint8Array |
| `vm.newFunction(name, callback)` | 创建一个由宿主回调支持的函数 |
| `vm.newPromise()` | 创建一个 `Deferred`(promise + resolve/reject) |
| `vm.newError(messageOrError)` | 从字符串或原生 `Error` 创建一个 Error |
| `vm.resolvePromise(handle)` | 从宿主端等待一个 QuickJS promise |
| `vm.setProp(obj, key, value)` | 设置一个属性(key:字符串或句柄,包括 symbol) |
| `vm.getProp(obj, key)` | 使用句柄 key 获取一个属性(包括 symbol) |
| `vm.typeof(handle)` | 以字符串形式获取 `typeof` |
| `vm.dump(handle)` | 将 QuickJS 值转换为宿主值 |
| `vm.hostToHandle(value)` | 将宿主值转换为 QuickJS 句柄 |
| `vm.snapshot()` | 捕获整个 VM 状态(包括扩展元数据) |
| `vm.registerHostCallback(name, fn)` | 恢复后按名称重新注册宿主回调 |
| `vm.dispose()` | 释放 VM |
| `vm[Symbol.dispose]()` | 与 `dispose()` 相同——启用 `using vm = ...` |
### `QuickJSOptions`
| 选项 | 描述 |
|--------|-------------|
| `wasm` | WASM 模块字节或预编译的 `WebAssembly.Module` |
| `wasi` | WASI 覆盖工厂:`(memory) => ({ random_get, clock_time_get, ... })`。应用于主模块和所有扩展 |
| `memoryLimit` | QuickJS 运行时可分配的最大内存(字节) |
| `interruptHandler` | 用于中断执行的回调(返回 `true` 停止) |
| `extensions` | `ExtensionDescriptor` 对象数组——要加载的原生 WASM 扩展 |
| `timezoneOffset` | VM 内部 `Date` 的时区:`'host'`(默认),以分钟为单位的固定偏移量,或 `(timeSecs) => minutes` 回调 |
### `ExtensionDescriptor`
| 属性 | 描述 |
|----------|-------------|
| `name` | 标识符字符串(用于快照元数据) |
| `wasm` | WASM 字节(`BufferSource`)或预编译的 `WebAssembly.Module` |
| `initFn?` | 初始化函数名称(默认:`qjs_ext_${name}_init`) |
| `wasi?` | 扩展提供的 WASI 覆盖:`(memory) => ({...})`。分层介于内置默认值和用户覆盖之间 |
### 缓存属性
这些是单例句柄——**不要** dispose 它们:
| 属性 | 值 |
|----------|-------|
| `vm.global` | 全局对象 |
| `vm.undefined` | `undefined` |
| `vm.null` | `null` |
| `vm.true` | `true` |
| `vm.false` | `false` |
### `JSValueHandle`
| 方法 / 属性 | 描述 |
|-------------------|-------------|
| `handle.isUndefined` | 如果是 `undefined` 则为 `true` |
| `handle.isNull` | 如果是 `null` 则为 `true` |
| `handle.promiseState` | `0` pending,`1` fulfilled,`2` rejected |
| `handle.toNumber()` | 提取为 `number` |
| `handle.toBigInt()` | 提取为 `bigint` |
| `handle.toString()` | 提取为 `string` |
| `handle.toArrayBuffer()` | 提取为 `ArrayBuffer`(从 WASM 内存复制) |
| `handle.toUint8Array()` | 提取为 `Uint8Array`(从 WASM 内存复制) |
| `handle.getProp(name)` | 按名称获取属性 |
| `handle.setProp(name, value)` | 按名称设置属性 |
| `handle.consume(fn)` | 调用 `fn(handle)`,然后 dispose,返回结果 |
| `handle.dup()` | 复制句柄(增加引用计数) |
| `handle.dispose()` | 释放句柄 |
| `handle[Symbol.dispose]()` | 与 `dispose()` 相同——启用 `using handle = ...` |
### `Deferred` (来自 `vm.newPromise()`)
| 属性 / 方法 | 描述 |
|--------------------|-------------|
| `deferred.handle` | QuickJS promise 对象 |
| `deferred.settled` | 在敲定(settled)时解析的宿主 `Promise` |
| `deferred.resolve(handle)` | 使用 QuickJS 值来 resolve promise |
| `deferred.reject(handle)` | 使用 QuickJS 值来 reject promise |
### 数据编组
`dump()` 和 `hostToHandle()` 自动在宿主和 QuickJS VM 之间转换值。支持以下类型:
| 宿主类型 | QuickJS 类型 | `dump()` 返回 | `hostToHandle()` 接受 |
|-----------|-------------|-----------------|------------------------|
| `undefined` | undefined | `undefined` | `undefined` |
| `null` | null | `null` | `null` |
| `boolean` | boolean | `boolean` | `boolean` |
| `number` | number | `number` | `number` |
| `string` | string | `string` | `string` |
| `bigint` | BigInt | `bigint` | `bigint` |
| `Symbol.for()` | global Symbol | `Symbol.for(description)` | `Symbol.for(description)` |
| `Error` | Error | `Error`(包含 name、message、stack) | `Error` |
| `Array` | Array | `Array`(递归) | `Array`(递归) |
| `ArrayBuffer | ArrayBuffer | `ArrayBuffer`(复制) | `ArrayBuffer` |
| `Uint8Array` | Uint8Array | `Uint8Array`(复制) | `Uint8Array` |
| 其他类型化数组 | typed array | 对应的 typed array(复制) | `ArrayBuffer`(通过视图) |
| `Promise` | Promise | — | QuickJS Promise(通过 `Deferred` 桥接) |
| 普通对象 | Object | `Record`(递归,自身可枚举键) | Object(递归) |
**注意:**
- 全局 symbol(`Symbol.for()`)通过 `Symbol.for(description)` 作为真实的宿主 `Symbol` 值进行往返转换
- 本地(匿名)symbol 在 dump 时为 `undefined`,并在传递给 `hostToHandle()` 时抛出异常
- 函数在 dump 时为 `undefined`(无法进行有意义的序列化)
- 循环引用和共享引用将被保留——对于相同的 QuickJS 对象指针,`dump()` 返回相同的宿主对象
- 在 dump 对象时,仅包含自身的可枚举字符串属性
- 二进制数据在宿主和 WASM 内存之间总是进行**复制**——没有零拷贝视图 API
- 类型化数组的 `dump()` 根据每个元素的 bytes 大小决定宿主构造函数(1 → `Uint8Array`,2 → `Uint16Array`,4 → `Uint32Array`,8 → `Float64Array`)
## 工作原理
### 核心洞察
WebAssembly 线性内存是一个扁平的字节数组。QuickJS 分配的所有内容——运行时结构体、所有上下文、所有 JS 对象、GC 堆、atom 表、promise 作业队列、pending promise——都存在于这个线性内存中。没有外部指针、文件句柄或操作系统资源。当您将整块内存整体复制到新的 WASM 实例时,所有内部指针关系都将被保留,因为它们引用的是相同的线性地址空间。
### 一个 VM = 一个 WASM 实例
与 quickjs-emscripten 具有双层模型(`QuickJSWASMModule` → `QuickJSContext`)不同,quickjs-wasm 使用更简单的单层模型:每次 `QuickJS.create()` 调用都会实例化自己独立的 WASM 模块,拥有独立的线性内存、运行时和上下文。这提供了更强的隔离性(VM 之间没有共享内存)并使快照更加干净——一个实例,一个上下文,一个快照。
### 架构
```
Host (Node.js / Deno / Bun / Browser)
|
+-- QuickJS class (ts/index.ts)
| |-- evalCode(), callFunction(), newFunction(), ...
| |-- snapshot() -> Snapshot { memory, stackPointer, runtimePtr, contextPtr }
| +-- restore(snapshot) -> QuickJS
|
+-- WASI Shim (ts/wasi-shim.ts)
| |-- clock_time_get, fd_write, random_get
| +-- fd_close, fd_fdstat_get, fd_seek (stubs)
|
+-- quickjs.wasm (1.4 MB)
|-- QuickJS-NG engine
+-- C interface layer (c/interface.c)
|-- Lifecycle, eval, value creation/extraction
|-- Host callback trampoline (imported host_call)
+-- Snapshot support (get/set runtime and context pointers)
```
### 宿主回调机制
当调用 `vm.newFunction(name, fn)` 时,会通过 `JS_NewCFunctionData2` 创建一个 QuickJS C 函数,函数名作为 JS 字符串存储在 `func_data[0]` 中。当 QuickJS 代码调用该函数时,C 跳板代码会提取名称并调用导入的 `host_call(name_ptr, name_len, this_ptr, argc, argv_ptr)` 函数,该函数按名称分派到已注册的宿主回调。
这种设计在快照/恢复后依然有效:名称字符串存储在 QuickJS 的堆中(是快照的一部分),恢复后,`registerHostCallback(name, fn)` 将名称重新映射到新的宿主函数。因为回调是以名称而非连续整数 ID 作为键,所以注册顺序无关紧要,并且添加或删除宿主函数不会导致恢复静默失败。
## 开发
### 前置条件
- [wasi-sdk](https://github.com/WebAssembly/wasi-sdk)(已在 v30 上测试)——设置 `WASI_SDK` 环境变量或默认为 `/tmp/wasi-sdk`
- Node.js >= 22
- pnpm
### 本地构建
```
# Clone with submodules
git clone --recursive https://github.com/vercel-labs/quickjs-wasm.git
cd quickjs-wasm
# 安装 wasi-sdk (macOS arm64 — 根据你的平台调整 URL)
curl -sL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-30/wasi-sdk-30.0-arm64-macos.tar.gz" \
| tar xz -C /tmp --strip-components=1 --one-top-level=wasi-sdk
# 安装依赖
pnpm install
# 构建 WASM binary + TypeScript
pnpm run build
# 运行测试
pnpm test
```
## 技术细节
### WASM 二进制
- 构建自 [quickjs-ng](https://github.com/quickjs-ng/quickjs)(MIT 许可证)
- 使用 wasi-sdk 在 reactor 模式下编译,目标为 `wasm32-wasip1`
- 未压缩大小为 1.4 MB
- 7 个 WASM 导入:6 个 WASI 函数 + 1 个用于宿主回调的 `env.host_call`
- 导出 `memory` 和 `__stack_pointer` 以支持快照
### 快照内容
快照捕获整个 WASM 线性内存,其中包含:
- `JSRuntime` 结构体(GC 状态、作业队列、模块加载器状态)
- `JSContext` 结构体(全局对象、内建对象、atom 表)
- 所有 JS 对象(通过 QuickJS 的 GC 堆)
- Promise 作业队列(pending 状态的 `.then` 回调)
- 字符串暂存表(atoms)
- `dlmalloc` 堆元数据
- C 接口的 `static JSRuntime *rt` 和 `static JSContext *ctx` 全局变量
- 存储在函数数据中的宿主回调 ID
加上 `__stack_pointer` WASM 全局变量(单个 i32)。
### 限制与未来工作
- **快照大小**:快照捕获整个 WASM 线性内存(约 256 KB 基线,随堆增长)。使用 `serializeSnapshot()` 获取二进制缓冲区,然后应用您自己的压缩(gzip/zstd)——由于存在大量零填充区域,内存压缩效果非常好。
- **栈大小限制**:QuickJS-ng 在 WASI 上禁用了 `JS_SetMaxStackSize`,因此深度递归会导致 WASM 陷阱(而不是可捕获的异常)。
- **ES Modules**:目前仅支持脚本模式的 eval。`import`/`export` 和模块加载器尚未连接。
- **扩展 ABI**:原生 WASM 扩展使用实验性的动态链接 ABI,该 ABI [尚未稳定](https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md)。所有扩展必须使用与主模块相同的 wasi-sdk 版本进行编译。详情请参阅 [EXTENSIONS.md](./EXTENSIONS.md)。
### 浏览器使用
quickjs-wasi 可在浏览器中工作——TypeScript API 仅使用标准的 `WebAssembly` API,并且 WASI shim 与环境无关。本包不执行隐式 I/O,因此加载 WASM 二进制文件取决于您。有关 `fetch()` 和基于打包工具的示例,请参见上面的[加载 WASM 二进制文件](#loading-the-wasm-binary)。
```
import { QuickJS } from 'quickjs-wasi';
import wasmUrl from 'quickjs-wasi/quickjs.wasm?url'; // Vite
// Fetch the .wasm file and compile it once
const wasmModule = await WebAssembly.compileStreaming(fetch(wasmUrl));
// Create VMs from the pre-compiled module (fast — no re-compilation)
using vm = await QuickJS.create({ wasm: wasmModule });
```
有关完整的 Vite 演示应用,请参见 [`examples/browser/`](./examples/browser/)。
标签:AI工具, CMS安全, DNS 反向解析, GNU通用公共许可证, JavaScript, JavaScript引擎, JavaScript运行时, JS运行时, MITM代理, Node.js, NPM包, OSV-Scalibr, Promise, QuickJS, QuickJS-NG, Rust/WASM, Serverless, Vite, VM快照, WASI, WASM, WebAssembly, WebAssembly编译, Web报告查看器, 代码隔离, 动态代码执行, 后端开发, 快照与恢复, 沙箱环境, 浏览器, 状态恢复, 状态管理, 虚拟机状态快照, 边缘计算