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报告查看器, 代码隔离, 动态代码执行, 后端开发, 快照与恢复, 沙箱环境, 浏览器, 状态恢复, 状态管理, 虚拟机状态快照, 边缘计算