这个项目提供一个 Rust 宿主 + QuickJS 运行时,核心能力有三块:
- Web API 兼容层(
fetch) - 异步文件 API(
fs/fs.promises,无同步接口) - Native 二进制计算管道 + WASI 模块执行
下面重点说明你现在最关心的 wasi 用法,以及高性能二进制处理方式。
这个仓库现在是 library-first:默认提供可引入的 Rust 库。
在你的项目里引入:
[dependencies]
rquickjs_playground = { path = "../rquickjs_playground" }最小示例:
use rquickjs_playground::AsyncHostRuntime;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let host = AsyncHostRuntime::new("demo-runtime")?;
let output = host
.spawn("(async () => JSON.stringify({ ok: true }))()")?
.wait()?;
println!("{output}");
Ok(())
}说明:AsyncHostRuntime::new(cache_scope_id) 默认不注入 wasi,而且默认构建也不会编译进 WASI 依赖。如果需要 wasi,请在 Cargo 里开启 wasi 特性,并改用 AsyncHostRuntime::new_with_options(cache_scope_id, WebRuntimeOptions { wasi: true })。宿主会在 Rust 侧自动把 cache key 前缀化,不需要在 JS 里手动拼接 runtime id。
如果你想运行仓库里的演示:
cargo run --example demo设计目标:尽量减少 JS <-> Rust 间的大对象开销。
native.put(Uint8Array) -> Promise<number>- 把字节放进 Rust 缓冲池,返回
id。
- 把字节放进 Rust 缓冲池,返回
native.exec(op, inputId, args?, extraInputId?) -> Promise<number>- 对
inputId执行一个操作,返回新id。
- 对
native.execChain(inputId, steps) -> Promise<number>- 一次提交多个步骤执行,减少 host 往返。
native.take(id) -> Promise<Uint8Array>- 取回结果(会消费该 id)。
native.takeInto(id, existing, offset?) -> Promise<{...}>- 把结果拷贝进已有
Uint8Array,减少 JS 侧重新分配。
- 把结果拷贝进已有
native.free(id) -> Promise<void>- 释放不再需要的缓冲。
native.run(op, input, args?, extraInput?) -> Promise<Uint8Array>- 单步便捷接口。
native.chain(steps, inputOrId) -> Promise<Uint8Array>- 多步便捷接口。
invertgrayscale_rgbaxor(需要第二输入)noop
const inputId = await native.put(new Uint8Array([1, 2, 3, 4]));
const outId = await native.execChain(inputId, [
{ op: "invert" },
{ op: "invert" },
{ op: "noop" }
]);
const target = new Uint8Array(1024);
const info = await native.takeInto(outId, target, 0);
// info.bytesWritten / info.sourceLength / info.truncatedwasi 是“在宿主里执行 WASI 模块”,不是把宿主本身跑在 WASI 里。
wasi.run(moduleBytes, options?)wasi.runById(moduleId, options?)wasi.takeStdout(result)wasi.takeStderr(result)
run/runById 返回:
{
exitCode: number,
stdoutId: number,
stderrId: number
}要使用这一组 API,需要同时满足两件事:
- Cargo 开启
wasi特性 - 创建运行时时显式开启
wasi
[dependencies]
rquickjs_playground = { path = "../rquickjs_playground", features = ["wasi"] }然后:
use rquickjs_playground::{AsyncHostRuntime, WebRuntimeOptions};
let host = AsyncHostRuntime::new_with_options(
"demo-runtime",
WebRuntimeOptions { wasi: true },
)?;args?: string[]- 传给 WASI 模块 argv(宿主会自动补
module.wasm为 argv[0])。
- 传给 WASI 模块 argv(宿主会自动补
stdinId?: number- 从
native.put(...)得到的输入缓冲 id。
- 从
reuseModule?: booleantrue:不消费moduleId,可重复运行。false/未传:默认消费moduleId(更省内存)。
// wasmBytes: Uint8Array
const moduleId = await native.put(wasmBytes);
const result = await wasi.runById(moduleId, {
reuseModule: true,
args: ["--mode", "fast"]
});
const stdout = await wasi.takeStdout(result); // Uint8Array
const stderr = await wasi.takeStderr(result); // Uint8Array
// 不再复用时释放
await native.free(moduleId);当前 wasi 执行上下文默认是“计算优先、权限最小”:
- 没有给 guest 预打开目录(不提供宿主文件系统权限)
- 没有额外网络权限配置
- stdin/stdout/stderr 走内存管道
这适合你现在的目标:CPU 密集计算(如图片处理)而不是系统 IO。
如果你要用 Rust 写一个 guest 程序,可以编译为 wasm32-wasip1:
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release生成的 .wasm 读成字节后传给 wasi.run(...) 或 native.put(...) + wasi.runById(...)。
cargo test如果你要跑包含 WASI 的测试:
cargo test --features wasi相关测试重点在:
src/tests/native.rssrc/tests/fs.rssrc/tests/fetch.rs
为了让 multipart 边界、编码细节完全由 reqwest 处理,当前实现采用“JS 结构化描述 -> Rust 组装 multipart”的协议。
- 避免在 JS 端手写 multipart 文本与 boundary。
- 把 multipart 规范细节交给
reqwest::multipart。 - 后续可通过
kind版本化扩展而不破坏已有行为。
- JS 端检测到
body instanceof FormData。 - JS 将
FormData编码为 JSON plan,作为body传给 host。 - 同时追加请求头:
x-rquickjs-host-body-formdata-v1: 1。 - Rust 端识别该头后:
- 解析 JSON plan;
- 用
reqwest::multipart::Form/Part构造请求; - 忽略 JS 侧
content-type,由 reqwest 自动生成multipart/form-data; boundary=...。
顶层结构:
{
"kind": "rquickjs-formdata-v1",
"entries": [
{
"name": "field1",
"kind": "text",
"value": "hello"
},
{
"name": "file1",
"kind": "binary",
"dataB64": "aGVsbG8=",
"filename": "a.txt",
"contentType": "text/plain"
}
]
}字段说明:
kind(顶层)- 当前固定
rquickjs-formdata-v1。
- 当前固定
entries[]name: string字段名。kind: "text" | "binary"。value?: string(text必填)。dataB64?: string(binary必填,base64 字节)。filename?: string | null(binary可选)。contentType?: string | null(binary可选)。
- Rust 端如果收到未知顶层
kind,会直接报错。 - 该协议只用于
FormData;其他 body(JSON、URLSearchParams、Blob等)走原有分支。 - 建议后续新增字段时保持向后兼容;如有不兼容变更,升级
kind(例如rquickjs-formdata-v2)。
下面给一个“可直接粘贴到 QuickJS 里执行”的最小示例。
场景:
- 输入一段 RGBA 原始像素(2x1)
- 先用
native.chain做灰度化 - 再用
takeInto复用预分配缓冲
(async () => {
// 2x1 RGBA: 红像素 + 绿像素
const rgba = new Uint8Array([
255, 0, 0, 255,
0, 255, 0, 255,
]);
// 多步链式(这里只有一步,也可继续加更多步骤)
const out = await native.chain([
"grayscale_rgba"
], rgba);
// out 是新的 Uint8Array
// 如果你想减少分配,建议用 takeInto
const id = await native.put(rgba);
const outId = await native.execChain(id, [
{ op: "grayscale_rgba" }
]);
const reused = new Uint8Array(8);
const info = await native.takeInto(outId, reused, 0);
return JSON.stringify({
out: Array.from(out),
reused: Array.from(reused),
bytesWritten: info.bytesWritten,
truncated: info.truncated
});
})()如果你要把“下载图片 -> Rust/WASI 计算 -> 回 JS”串起来,推荐流程:
- JS 先拿到图片字节(
Uint8Array) native.put(bytes)拿到输入 idnative.execChain(...)或wasi.runById(...)做计算native.take(...)或native.takeInto(...)取结果- 中间不再用的 id 及时
native.free(...)
这个示例展示如何把输入字节通过 stdinId 传给 WASI 模块,再从 stdout 取回结果。
约定:
- WASI 模块从 stdin 读取输入字节
- WASI 模块把处理后的字节写到 stdout
(async () => {
// 这里只是示意:wasmBytes 通常来自文件读取或网络下载
// 例如:const wasmBytes = await fs.promises.readFile("./image_worker.wasm");
const wasmBytes = new Uint8Array([/* ... wasm 二进制 ... */]);
// 输入图片字节(示意,真实场景可以是 PNG/JPEG/RGBA 等)
const imageBytes = new Uint8Array([1, 2, 3, 4, 5]);
const moduleId = await native.put(wasmBytes);
const stdinId = await native.put(imageBytes);
const result = await wasi.runById(moduleId, {
// true 表示模块可复用,多次执行同一个模块时建议开启
reuseModule: true,
stdinId,
args: ["--op", "grayscale"]
});
const processed = await wasi.takeStdout(result); // Uint8Array
const logs = await wasi.takeStderr(result); // Uint8Array
// 用完后释放模块 id(如果后续还要复用就先不释放)
await native.free(moduleId);
return JSON.stringify({
exitCode: result.exitCode,
outputSize: processed.length,
stderrSize: logs.length
});
})()建议:
- 多次调用同一个 WASI 模块时,用
reuseModule: true,减少模块重复加载成本。 - 如果输出大小可预估,拿到
stdoutId后也可以结合native.takeInto(...)做复用缓冲。
新增了一个“宿主收到多个 HTTP 请求 -> 分发到多个 QJS worker -> 按完成顺序逐条返回”的示例:
examples/http_plugin_pool.rs
运行:
cargo run --example http_plugin_pool这个示例演示了:
- 固定数量 QJS worker(常驻,不是每请求销毁)
- 本地 HTTP 接口(
POST /invoke) - JS 侧用
fetch并发请求并按完成顺序收结果
现在支持“插件 bundle 导出对象 + Rust 按函数名调用”的模式,不再要求插件把函数挂到 globalThis。
推荐插件产物(CJS bundle)导出一个对象:
module.exports = {
async getInfo() {
return {
name: "image-tools",
version: "1.2.3",
apiVersion: 1,
description: "示例插件"
};
},
async run(input) {
return { ok: true, input };
}
};Rust 侧用法:
use rquickjs::{Context, Runtime};
use rquickjs_playground::web_runtime::{
WEB_POLYFILL, WebRuntimeOptions, plugin_call, plugin_get_info, plugin_load_bundle,
polyfill_script,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = Runtime::new()?;
let context = Context::full(&runtime)?;
let script = polyfill_script(WebRuntimeOptions::default());
context.with(|ctx| {
ctx.eval::<(), _>(script.as_str())?;
plugin_load_bundle(
&ctx,
"image-tools".to_string(),
r#"
module.exports = {
getInfo() {
return { name: "image-tools", version: "1.2.3", apiVersion: 1 };
},
echo(name, version) {
return { name, version };
}
};
"#
.to_string(),
)?;
let info = plugin_get_info(&ctx, "image-tools".to_string())?;
let echoed = plugin_call(
&ctx,
"image-tools".to_string(),
"echo".to_string(),
Some("[\"demo\",\"0.0.1\"]".to_string()),
)?;
println!("info={info} echoed={echoed}");
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}备注:当前加载器按 CJS 方式执行 bundle(module.exports / exports.default),如果你在 TS 中使用 import/export,请先打包成单文件 CJS 再交给宿主加载。
新增了 pnpm_demo/src/runtime-api.ts,提供统一类型化入口,插件代码可以直接 import 使用。
示例:
import { requireApi, requireCryptoLike, runtime } from "../src/runtime-api";
const crypto = requireCryptoLike();
const sign = crypto.createHmac("sha256", "key").update("text").digest("hex");
const native = requireApi("native");
const out = await native.chain(["invert"], new Uint8Array([1, 2, 3]));
const id = runtime.uuidv4();可用能力包括(按需读取):
- Web API:
fetch、Request、Response、Headers、FormData、Blob、URL等 - Host API:
fs、native、wasi、cache、bridge、plugin - Runtime API:
crypto/nodeCryptoCompat、uuidv4、Buffer、TextEncoder/TextDecoder
并且提供了 getApi(name)(可选)与 requireApi(name)(缺失直接抛错)两套调用方式。
补充:当前 cache API 不再提供 cache.scoped(...)。如果需要业务内分组,请直接在 key 上自行加前缀(例如 "jm_http::jwt")。实例级隔离由 AsyncHostRuntime::new(cache_scope_id) 在 Rust 侧统一处理。