JavaScript 在 WebAssembly 容器中运行:Javy 与 QuickJS 的编译实践
各位开发者朋友,大家好!今天我们要深入探讨一个近年来备受关注的技术方向——如何在 WebAssembly(WASM)环境中运行 JavaScript。这听起来可能有些抽象,但其实它正在改变我们对“浏览器外运行 JS”的认知。
我们将聚焦两个主流项目:Javy 和 QuickJS,它们分别代表了两种不同的实现思路。通过本讲座,你将掌握:
- 为什么要在 WASM 中运行 JS?
- Javy 是什么?它是如何工作的?
- QuickJS 又是什么?它的优势在哪里?
- 如何从源码编译这两个项目?
- 实战案例:用 Javy 或 QuickJS 编写一个简单的 JS 脚本并运行在 WASM 中。
让我们开始吧!
一、为什么要让 JavaScript 运行在 WebAssembly 中?
传统上,JavaScript 主要运行在浏览器或 Node.js 环境中。然而,随着边缘计算、嵌入式系统和跨平台应用的发展,人们越来越希望:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 浏览器外执行 JS | Node.js 不适合所有环境(如 IoT 设备) | 使用轻量级 JS 引擎 + WASM |
| 安全沙箱隔离 | 直接运行 JS 可能带来安全风险 | WASM 提供强隔离机制 |
| 性能优化 | JS 引擎解释执行效率有限 | WASM 可以编译为原生指令,提升性能 |
| 多语言集成 | 想在 C/C++/Rust 中调用 JS | WASM 是天然的多语言互操作接口 |
WebAssembly 正是为此而生——它是一个可移植、高性能、低开销的二进制格式,允许你在任何支持 WASM 的平台上运行任意语言编译后的代码。
所以,“让 JavaScript 在 WASM 中运行”不是噱头,而是未来趋势。
二、Javy:用 Rust 构建的 WebAssembly 上 JS 引擎
什么是 Javy?
Javy 是一个用 Rust 编写的、基于 V8 引擎的 JavaScript 运行时,目标是在 WebAssembly 中提供完整的 JS 支持。它的亮点在于:
- 使用 wasm-bindgen 与 JS 交互;
- 支持 ES2022 标准;
- 提供类似 Node.js 的模块系统;
- 可直接嵌入到 Rust 应用中。
核心原理
Javy 的核心思想是:把 V8 引擎本身编译成 WASM,然后通过 Rust 层封装 API 接口。这样就能在不依赖外部进程的情况下,在 WASM 容器中执行 JS。
示例:使用 Javy 执行一段 JS 代码
首先安装依赖(需要 Rust 工具链):
cargo install wasm-pack
接着创建一个简单的 Rust 项目:
# Cargo.toml
[package]
name = "javy-example"
version = "0.1.0"
edition = "2021"
[dependencies]
javy = { git = "https://github.com/javyw/javy" }
编写 src/lib.rs:
use javy::{Context, JsValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ctx = Context::new();
// 执行一段 JS 代码
let result: JsValue = ctx.eval(r#"
function add(a, b) {
return a + b;
}
add(5, 3);
"#)?;
println!("Result: {}", result.as_f64().unwrap());
Ok(())
}
编译为 WASM:
wasm-pack build --target web
你会得到 pkg/javy_example_bg.wasm 文件,这就是可以嵌入网页或其它 WASM 容器中的模块。
✅ 优点:功能完整,接近原生 JS 行为
❗ 缺点:体积较大(V8 本身庞大),启动慢
三、QuickJS:更轻量的 JS 引擎,专为嵌入设计
什么是 QuickJS?
QuickJS 是由 Fabrice Bellard 开发的一款小巧、快速且内存友好的 JavaScript 引擎。它被设计用于嵌入式系统、IoT 设备、甚至游戏引擎中。
QuickJS 的特点包括:
- 单文件架构(只有一个
.c文件); - 支持 ES2020+;
- 内存占用极低(< 1MB);
- 无 GC 延迟,适合实时场景;
- 易于编译为 WASM。
为什么选择 QuickJS?
相比 Javy,QuickJS 更适合以下场景:
| 特性 | Javy | QuickJS |
|---|---|---|
| 启动速度 | 较慢(依赖 V8) | 快(轻量级) |
| 内存占用 | 高(~10MB+) | 极低(< 1MB) |
| 功能完整性 | 完整 ES2022 | ES2020+ |
| 编译复杂度 | 高(需链接 V8) | 极低(单文件) |
| 社区活跃度 | 中等 | 低(但稳定) |
编译 QuickJS 到 WASM
我们需要使用 Emscripten 将其编译为 WASM。
步骤如下:
-
安装 Emscripten(推荐版本 >= 3.1.17):
git clone https://github.com/emscripten-core/emsdk.git cd emsdk ./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh -
下载 QuickJS 源码:
git clone https://bellard.org/quickjs/ cd quickjs -
编译为 WASM:
emcc -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_main', '_qjs_eval']" -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap']" -o quickjs.wasm quickjs.c
这个命令会生成 quickjs.wasm,其中包含两个导出函数:
_main:主入口(可用于调试)_qjs_eval:执行 JS 字符串表达式的函数
使用示例(JavaScript 端)
在浏览器中加载并调用:
<script>
async function runJSInWASM() {
const response = await fetch('quickjs.wasm');
const bytes = await response.arrayBuffer();
const module = await WebAssembly.instantiate(bytes);
// 获取 qjs_eval 函数
const qjs_eval = module.instance.exports._qjs_eval;
// 执行 JS 表达式
const resultPtr = qjs_eval("5 + 3");
const result = new TextDecoder().decode(new Uint8Array(module.instance.exports.memory.buffer, resultPtr, 100));
console.log("Result:", result); // 输出: 8
}
</script>
⚠️ 注意:这里简化了内存管理细节(实际应使用 malloc 和 free),但在演示层面足够清晰。
✅ 优点:极致轻量,适合资源受限环境
❗ 缺点:部分高级特性缺失(如 import/export 模块系统较弱)
四、对比总结:Javy vs QuickJS
| 维度 | Javy | QuickJS |
|---|---|---|
| 引擎来源 | V8(Chrome 内核) | 自研(Bellard) |
| 编译方式 | Rust + wasm-bindgen | Emscripten |
| 支持标准 | ES2022 | ES2020 |
| 启动时间 | 几百毫秒 | < 50ms |
| 内存占用 | ~10MB+ | < 1MB |
| 是否支持模块 | ✅ 是(Node-style) | ❌ 有限(需手动处理) |
| 适用场景 | 通用脚本、服务器端 | 嵌入式、边缘计算 |
| 开发难度 | 中等(需懂 Rust) | 低(纯 C + Emscripten) |
📌 结论:如果你追求“像 Node.js 一样运行 JS”,选 Javy;如果你追求极致轻量和快速启动,选 QuickJS。
五、实战演练:用 QuickJS 编写一个简单计算器 WASM 模块
我们来做一个小项目:将一个计算器逻辑打包成 WASM,并在前端页面调用。
Step 1: 编写 JS 计算逻辑(作为字符串传入)
function calc(expr) {
try {
return eval(expr);
} catch (e) {
return "Error: " + e.message;
}
}
Step 2: 修改 QuickJS 源码添加 eval 接口
在 quickjs.c 中添加:
// 添加到 quickjs.c 的最后
static JSValue js_calc(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc != 1 || !JS_IsString(argv[0])) {
JS_ThrowTypeError(ctx, "Expected a string expression");
return JS_EXCEPTION;
}
const char *expr = JS_ToCString(ctx, argv[0]);
JSValue result = JS_Eval(ctx, expr, strlen(expr), "<eval>", JS_EVAL_TYPE_GLOBAL);
JS_FreeCString(ctx, expr);
return result;
}
// 注册函数
JS_SetPropertyStr(ctx, global_obj, "calc", js_calc);
Step 3: 重新编译为 WASM
emcc -O3
-s WASM=1
-s EXPORTED_FUNCTIONS="['_main', '_qjs_eval', '_calc']"
-s EXPORTED_RUNTIME_METHODS="['ccall','cwrap']"
-o calculator.wasm
quickjs.c
Step 4: 在 HTML 页面中使用
<!DOCTYPE html>
<html>
<head>
<title>QuickJS Calculator</title>
</head>
<body>
<input type="text" id="expr" placeholder="Enter JS expression">
<button onclick="runCalc()">Run</button>
<pre id="output"></pre>
<script>
async function loadWasm() {
const resp = await fetch('calculator.wasm');
const bytes = await resp.arrayBuffer();
return await WebAssembly.instantiate(bytes);
}
async function runCalc() {
const input = document.getElementById('expr').value;
const wasm = await loadWasm();
const calcFunc = wasm.instance.exports._calc;
try {
const resultPtr = calcFunc(input);
const result = new TextDecoder().decode(
new Uint8Array(wasm.instance.exports.memory.buffer, resultPtr, 100)
);
document.getElementById('output').textContent = result;
} catch (e) {
document.getElementById('output').textContent = "Error: " + e.message;
}
}
</script>
</body>
</html>
✅ 这个例子展示了如何将业务逻辑封装进 WASM,再在前端调用,真正做到“JS 在 WASM 中运行”。
六、常见问题与建议
Q1: 我能在浏览器中直接运行这些 WASM 吗?
✅ 可以!只要你的浏览器支持 WASM(现代 Chrome/Firefox/Safari 都支持),就可以直接加载 .wasm 文件并调用导出函数。
Q2: 性能怎么样?
- Javy:由于 V8 引擎复杂,首次加载较慢,但后续执行快;
- QuickJS:启动快,适合高频调用的场景(如游戏脚本、配置解析)。
Q3: 能否与其他语言交互?
✅ 当然!WASM 是多语言互操作的标准接口。例如:
- Rust → WASM → JS(Javy)
- C → WASM → Python(通过 Pyodide)
- Go → WASM → JS(通过 TinyGo)
Q4: 如何调试 WASM 中的 JS?
- 使用
console.log()+wasm-bindgen的日志钩子; - 在开发阶段用
wasm-pack dev自动重建; - 对于 QuickJS,可以用
-g参数开启调试符号。
七、结语:未来的可能性
今天我们看到,JavaScript 并不只是浏览器的专属语言。借助 WebAssembly,我们可以把它带入更多领域:
- IoT 设备:用 QuickJS 在 ESP32 上跑动态脚本;
- 云原生:用 Javy 在 serverless 函数中运行用户自定义逻辑;
- 游戏引擎:将 JS 作为脚本层嵌入 C++ 游戏逻辑;
- 区块链:智能合约中运行轻量 JS(虽然目前主流还是 Solidity)。
这不是技术炫技,而是实实在在的工程进步。
如果你现在就开始尝试 Javy 或 QuickJS,你会发现——原来 JS 不一定要在 Node.js 或浏览器里才能活得好!
📌 建议下一步行动:
- 克隆 Javy GitHub,跑通第一个例子;
- 下载 QuickJS,动手编译成 WASM;
- 在自己的项目中加入 WASM JS 引擎,体验“真正的跨平台 JS”。
祝你在 WebAssembly 的世界里玩得开心!谢谢大家!