JavaScript 在 WebAssembly 容器中运行:Javy 与 QuickJS 的编译实践

JavaScript 在 WebAssembly 容器中运行:Javy 与 QuickJS 的编译实践

各位开发者朋友,大家好!今天我们要深入探讨一个近年来备受关注的技术方向——如何在 WebAssembly(WASM)环境中运行 JavaScript。这听起来可能有些抽象,但其实它正在改变我们对“浏览器外运行 JS”的认知。

我们将聚焦两个主流项目:JavyQuickJS,它们分别代表了两种不同的实现思路。通过本讲座,你将掌握:

  • 为什么要在 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。

步骤如下:

  1. 安装 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
  2. 下载 QuickJS 源码:

    git clone https://bellard.org/quickjs/
    cd quickjs
  3. 编译为 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>

⚠️ 注意:这里简化了内存管理细节(实际应使用 mallocfree),但在演示层面足够清晰。

✅ 优点:极致轻量,适合资源受限环境
❗ 缺点:部分高级特性缺失(如 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 或浏览器里才能活得好!


📌 建议下一步行动:

  1. 克隆 Javy GitHub,跑通第一个例子;
  2. 下载 QuickJS,动手编译成 WASM;
  3. 在自己的项目中加入 WASM JS 引擎,体验“真正的跨平台 JS”。

祝你在 WebAssembly 的世界里玩得开心!谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注