WASM 语言运行时:在浏览器跑 Python、Ruby 与其他语言

详解浏览器语言运行时生态:Pyodide 的 Python 全栈、Ruby WASI、QuickJS 微型 JS 引擎、Wasmtime 嵌入自建运行时、跨语言调用与对象桥接、解释器进 WASM 的性能与体积代价,以及教学/沙箱/客户端计算三大场景和运行时选型矩阵。

导语:浏览器成为多语言运行时的第二现场

浏览器只有一个 JavaScript 运行时,但用户想把 Python、Ruby、Lua、SQL 都搬进网页——用于教学、数据可视化、沙箱执行甚至离线计算。WASM 让这一切成为现实:把解释器整个编译成 .wasm,浏览器就成了任意语言的运行时宿主。本文系统讲这套生态:Pyodide、Ruby WASI、QuickJS、Wasmtime 嵌入,跨语言调用与对象桥接,性能与体积的权衡,以及教学/沙箱/客户端计算三大场景与选型矩阵。

前置:/wasm-javascript-interop/(JS 互操作)、/wasm-embedding-hosting-apis/(嵌入 API)、/wasm-wasmtime-runtime/(Wasmtime 运行时)。


目录


1. 为什么在浏览器跑一门语言:场景与动机

在线教学/IDE 要免安装跑 Python、沙箱要执行不可信代码、客户端计算要让数据不出设备、还要复用已有 Python/R 生态。四条路线只有一条走得通:

□ 转译(Python→JS):运行时行为有偏差、调试困难、生态断裂
□ 编译官方解释器进 WASM:原生态(CPython/CRuby 原实现)、零重写
□ 后端代跑:多一次网络往返、数据出域、有冷启动
□ 本地安装:要求用户装环境,体验割裂
→ WASM = 浏览器里「真跑这门语言」,其余方案都是妥协

一句话总结:WASM 语言运行时 = 把官方解释器整个搬进浏览器,教学、沙箱、客户端计算、生态复用四类场景共享同一套机制。


2. Pyodide:浏览器里的 Python 全栈

Pyodide 把 CPython 3 编译为 WASM,内置 numpy/pandas/scipy/matplotlib 科学计算栈,还有 micropip 从 PyPI 安装纯 Python 包。

import { loadPyodide } from 'pyodide';
const pyodide = await loadPyodide({
  indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.26.2/full/',
});
const result = await pyodide.runPythonAsync(`
  import numpy as np
  a = np.array([1, 2, 3])
  (a * 2).tolist()
`);
console.log(result);   // [2, 4, 6]

await pyodide.runPythonAsync(`
  import micropip
  await micropip.install('requests')
  # scikit-learn / pandas 等已预编译包亦可安装
`);

Pyodide 的关键机制:Python 堆内存运行在 WASM 线性内存里,Python 对象通过 PyProxy 暴露给 JS;numpy 数组可零拷贝共享为 JS TypedArray。

一句话总结:Pyodide = 浏览器里的 CPython + 科学计算栈 + PyPI,Python 代码原样运行,数据通过 PyProxy 与 JS 互通。


3. Ruby WASI 与 QuickJS:脚本语言的 WASM 化

3.1 Ruby WASI(ruby.wasm)

ruby.wasm 项目把 CRuby 编译为 WASI 目标,支持解释执行与预编译字节码:

<script type="module">
import { DefaultRubyVM } from 'https://cdn.jsdelivr.net/npm/ruby-wasm-wasi@2.3.0';
const vm = await DefaultRubyVM();
vm.eval('puts "Hello from Ruby in WASM"');
</script>

命令行构建:ruby.wasm build --ruby-version 3.2 src.rb -o ruby-app.wasm,产物可直接 wasmtime run 或在浏览器加载。

3.2 QuickJS:微型 JS 引擎

QuickJS 是 Bellard 写的迷你 JS 引擎,编译成 WASM 后常用于「页面内嵌 JS 沙箱」:

import { getQuickJS } from 'quickjs-emscripten';
const { QuickJS, shouldInterruptAfterDeadline } = await getQuickJS();
const vm = QuickJS.createVm();
vm.setInterruptHandler(shouldInterruptAfterDeadline(1000));  // 1s 超时
const res = vm.evalCode('1 + 2');
console.log(res.consume(vm.dump));   // 3

QuickJS WASM 约 1~2MiB(gzip 后几百 KB),远小于完整引擎,适合「可信边界内再跑一个小 JS 世界」。

一句话总结:Ruby WASI 搬 CRuby、QuickJS 搬微型 JS 引擎——解释器进 WASM 不是 Python 专利,任何语言编译到 wasm32 都能在浏览器跑。


4. Wasmtime 嵌入与自建运行时

不想用现成方案时,把 Wasmtime 作为库嵌入,自己控制 WASI 能力与模块生命周期:

use wasmtime::{Engine, Module, Store, WasiCtx, WasiCtxBuilder};

let engine = Engine::default();
let wasi = WasiCtxBuilder::new()
    .inherit_stdio()
    .env("ALLOW", "1")          // 显式注入环境变量
    .build();
let mut store = Store::new(&engine, wasi);
let module = Module::from_file(&engine, "app.wasm")?;
// 绑定 wasi 到 linker 后实例化,调用 _start 入口

自建的价值:按需裁剪能力(无文件系统、仅白名单网络)、批量实例化做多租户、把 WASM 模块嵌进 Electron/桌面/服务端同一套代码。

一句话总结:Wasmtime 嵌入 = 自己掌握 WASI 能力注入与模块生命周期,适合需要精细裁剪权限、批量实例化或跨端复用的场景。


5. 跨语言调用与对象桥接

5.1 JS ⇄ Python 双向调用

// JS 调用 Python 函数
const pyFunc = pyodide.globals.get('add');
console.log(pyFunc(1, 2));                    // 3

// Python 调用 JS 对象
await pyodide.runPythonAsync(`
  from js import document
  document.title = "被 Python 改了"
`);

5.2 三种桥接形态与性能要点

□ PyProxy(pyodide):Python 对象 ↔ JS 引用,.get/.set/.call
□ JSProxy:JS 对象在 Python 侧变为可调用代理
□ TypedArray 零拷贝:numpy 数组 ↔ Float32Array 共享底层 buffer
→ 批量数据走 TypedArray 共享内存,避免逐元素过桥
→ 对象句柄用完 .destroy(),防止内存泄漏

一句话总结:跨语言桥接 = 代理对象 + 共享内存——小数据走 Proxy 调用、大数据走 TypedArray 零拷贝,边界转换是性能分水岭。


6. 性能与体积:解释器进 WASM 的代价

运行时体积(gzip 后)速度启动
Pyodide(含科学栈)10~12MiBCPython 原速 ~1x200ms~1s
Ruby WASI5~8MiBCRuby 原速 ~1x100~500ms
QuickJS WASM0.5~1MiB原生 ~1x<50ms

关键认知:WASM 不拖慢解释器——CPython/CRuby 本身是解释执行,编译成 WASM 后速度与原生基本持平(纯计算场景 SIMD 甚至略胜)。真正的代价是体积与启动:解释器 + 标准库 + 依赖包都要下载加载。

优化手段:
□ 按需裁剪标准库 / 用 esbuild 摇树 JS 胶水
□ 预编译字节码(Ruby)跳过 parse 阶段
□ 模块实例复用 + 预加载到 Cache API
□ 懒加载:用户真正触发执行时再 fetch 解释器

一句话总结:解释器进 WASM 性能不降、体积和启动才要命——运行时体积是选型第一考量,懒加载与 Cache API 是标配优化。


7. 场景一:教学与在线 IDE

典型架构是「编辑器 + WASM 解释器 + 输出面板」,全部页面内完成、零后端:

<script type="module">
import { loadPyodide } from 'pyodide';
const pyodide = await loadPyodide();
pyodide.setStdout({ batched: (s) => outputPanel.write(s) }); // 捕获 stdout
editor.on('run', async () => {
  await pyodide.runPythonAsync(editor.getValue());
});
</script>

教学场景三个注意点:stdout/stderr 捕获(setStdout/setStderr 重定向到输出面板)、执行超时(用 setInterruptBuffer 配合 worker 中断死循环)、状态隔离(每次运行是否保留全局变量,教学通常不保留)。

一句话总结:在线 IDE = 编辑器 + WASM 解释器 + stdout 重定向,教学场景要格外处理超时中断与全局状态隔离。


8. 场景二:沙箱与客户端计算

代码沙箱:不可信代码在 WASM 沙箱里执行,物理上无法访问文件系统、网络(除非显式注入)与 DOM;WASM 无 syscall、无进程、内存越界 trap,天然比 eval 安全——仍需自设超时与内存水位防 DoS:

const result = vm.evalCode(userCode, { type: 'global' }); // QuickJS,已设 1s 中断
// Pyodide 跑不可信 Python:不注入任何 WASI 能力,默认零权限

客户端计算:数据不出设备,隐私 + 离线双收,典型如 CSV 解析 + pandas 聚合 + 图表:

const df = await pyodide.runPythonAsync(`
  import pandas as pd, io
  df = pd.read_csv(io.StringIO(csvText))
  df.groupby('region')['amount'].sum().to_dict()
`);

一句话总结:沙箱用 WASM 隔离边界 + 自设超时,客户端计算让数据不离开设备——两类场景都吃「本地执行」的红利。


9. 运行时选型矩阵

需求选择
浏览器跑 Python + 科学栈Pyodide
浏览器跑 Rubyruby.wasm(DefaultRubyVM)
页面内嵌小 JS 沙箱QuickJS(quickjs-emscripten)
自建/裁剪运行时Wasmtime 嵌入 + 自定义 WASI
多语言统一平台Wasmer(多语言绑定)
只执行轻量脚本QuickJS(避免重解释器体积)
选型决策树:
□ 语言有官方 WASM 构建(pyodide/ruby.wasm)?→ 直接用
□ 无官方构建且体积敏感?→ QuickJS 或换语言
□ 需要精细权限控制?→ Wasmtime 自建
□ 科学计算?→ Pyodide(numpy/pandas 已内置)

一句话总结:选型 = 语言生态(有无官方 WASM)+ 体积预算 + 权限需求——有官方构建直接用、体积敏感降级小引擎、权限敏感自建 Wasmtime。


10. 速查表与一句话记忆

问题一句话答案
Python 浏览器运行时Pyodide(CPython + 科学栈 + micropip)
Ruby 浏览器运行时ruby.wasm / DefaultRubyVM
小体积 JS 沙箱QuickJS WASM(~1MiB)
自建运行时Wasmtime 嵌入 + 自定义 WASI
跨语言调用PyProxy/JSProxy + TypedArray 零拷贝
性能WASM 不拖慢解释器,代价在体积与启动
体积优化裁剪标准库 + 懒加载 + Cache API
教学场景stdout 重定向 + 超时中断 + 状态隔离
沙箱场景WASM 边界 + 自设超时与内存水位
客户端计算数据不出设备,隐私 + 离线

一句话记忆:WASM 语言运行时 = 把官方解释器编译成 .wasm 塞进浏览器——Python 用 Pyodide(科学栈 + micropip)、Ruby 用 ruby.wasm、小沙箱用 QuickJS、要裁剪权限就 Wasmtime 自建;跨语言靠 Proxy 代理 + TypedArray 零拷贝;WASM 不拖慢解释器、真正代价是体积与启动(懒加载 + Cache API);教学盯 stdout/超时,沙箱盯隔离边界——「原生态真跑,别转译假装」是核心心法。


延伸阅读

  • /wasm-javascript-interop/ — JS 互操作与对象桥接
  • /wasm-embedding-hosting-apis/ — 嵌入与托管 API
  • /wasm-wasmtime-runtime/ — Wasmtime 运行时详解
  • /wasm-ai-inference-browser/ — 浏览器端机器学习
  • /wasm-component-model-wit/ — 组件模型与跨语言互操作
  • [[ai-ml]] — AI 推理与浏览器 ML

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「wasm」更多文章

  1. WASM 构建与打包工具链:wasm-bindgen、wasm-pack 与前端集成
  2. WASM 媒体处理:音视频编解码、转码与滤镜
  3. WASM 密码学:WebCrypto、WASM 密码库与安全计算