如何用 Spinel 把 Ruby 编译成 WebAssembly:浏览器与 wasmtime 部署完整指南
【免费下载链接】spinelRuby AOT compiler项目地址: https://gitcode.com/gh_mirrors/spin/spinel
Spinel 是一款 Ruby AOT(提前)编译器,可将 Ruby 源码编译为独立原生可执行文件,并支持--target=wasm32-wasi目标,把整个 Ruby 程序编译成标准 WebAssembly 模块,直接在 wasmtime、Node 或支持 WASI 的浏览器中运行。本指南带你从零完成编译、运行和部署,并详解 wasm32 目标下的关键限制。
为什么选择 WebAssembly 目标
Ruby 应用通常依赖解释器运行时,而 Spinel 采用"整程序类型推断 → 生成 C → 系统编译器链接"的路线:
Ruby (.rb) → libprism 解析 → 全程序类型推断 → 生成 C → clang (wasi-sdk) → .wasm编译到 wasm 时,编译器照常运行在宿主上,输出的 C 代码由 wasi-sdk 的 clang 针对目标编译,产出<name>.wasm模块。这意味着:
- 无需目标机器安装 Ruby:模块只依赖 WASI 引擎;
- 冷启动极快:编译产物是纯 Wasm,秒级加载;
- 部署简单:一个
.wasm文件 + 一个引擎(wasmtime / 浏览器)即可交付。
详细文档见 docs/wasm.md。
快速上手:三步编译出 .wasm
1. 准备工具链
| 依赖 | 说明 |
|---|---|
| wasi-sdk(34+) | 指向WASI_SDK环境变量,默认安装路径/opt/wasi-sdk |
| wasm 运行时归档 | 执行make wasm-rt构建lib/wasm32-wasi/libspinel_rt.a |
| 支持异常处理的引擎 | wasmtime 48(-W exceptions=y)、当前 Chrome / Firefox / Safari / Node |
关键点:Ruby 的raise/rescue底层是 C 的setjmp/longjmp,wasi-sdk 会将其降级到 wasm 的标准化exnref异常指令上,因此引擎必须实现该提案(见 docs/wasm.md 第 23-29 行)。
2. 编译你的程序
# 一次性构建 wasm 运行时归档(默认不构建) make wasm-rt # 编译 Ruby 程序为 WebAssembly spinel --target=wasm32-wasi app.rb # 生成 app.wasm # 用 wasmtime 运行 wasmtime run -W exceptions=y app.wasm若只想要 C 中间产物或指定输出路径,也可使用-c与-o参数。CI 中的完整验证流程可以参考 Makefile 里的wasm-test目标(Makefile):它对测试用例逐条执行spinel --target=wasm32-wasi编译,再用 wasmtime 运行并与期望输出比对。
3. 浏览器中运行
生成的模块是标准 WASI preview 1 模块,可在带 WASI shim 的浏览器中加载;Node 则可直接用node:wasi模块托管。
wasm32 目标下有什么不一样
这是部署前必须了解的"坑"清单,全部来自官方文档 docs/wasm.md:
Integer 变成 32 位
wasm32 指针是 32 位,sp_int随指针宽度变化,因此Integer 范围为-2**31 .. 2**31-1:2**40在默认--int-overflow=raise下会抛RangeError,超范围的字面量则自动成为 Bignum。同一份代码在 64 位宿主机上编译会得到 64 位 Integer——若你的测试假设了 64 位,请在文件首行标注# spinel: int64。相关机制详见 docs/int-overflow.md。
不支持 Fiber、Enumerator#next、Thread
wasm 核心指令没有栈切换,Fiber.new会抛FiberError;依赖它的对象(外部枚举器、Enumerator.new+Yielder)同样失败。使用Thread、Mutex、Queue的程序会在编译期被拒绝——wasm 线程需要 SharedArrayBuffer 和带线程的运行时归档,而当前均不提供。
无进程、信号、域名解析
WASI 没有这些能力:fork、spawn、反引号、system、Process.kill、Socket.getaddrinfo等不会变成链接错误,而是像 POSIX 上那样返回Errno::ENOSYS/Errno::ENOTSUP/SocketError。preview 1 中无法打开 socket;文件 I/O 只在宿主授权的目录内工作(wasmtime 用--dir=.授权当前目录)。
其他运行约束
- 栈:主线程栈是 8 MB 线性内存;wasmtime 另对模块帧的原生栈设 512 KB 上限,深度递归的程序请
wasmtime run -W max-wasm-stack=16777216提高; - GC:运行时 slab 分配器一次性占用 64 MB 线性内存且不归还(wasm 内存只增不减),超出后每块走
malloc; - 函数体大小:引擎对单个函数体有上限(wasmtime 约 7.6 MB)。含
rescue的大方法会把所有活跃局部变量存到内存,超大顶层程序可能超限——把这类代码移入独立方法即可修复; - FFI:WASI 无
dlopen,ffi_lib绑定不到东西;C 函数必须在模块内(包自带的 C 或用同一 sdk 编译后用--link链接的对象)。
内部机制一览
wasm 目标由几个关键部分组成,源码位置如下:
| 组成 | 位置 | 作用 |
|---|---|---|
| WASI 垫片 | lib/wasi/ | 补齐 wasi-libc 缺失的 POSIX 头文件(sys/wait.h、netdb.h、ucontext.h等) |
| 失败桩实现 | lib/wasi/sp_wasi.c | 定义以errno失败返回的调用 |
| 目标选择 | src/main.c | 选定编译器、归档、仿真宏与 sjlj 参数,--print-build可打印全部 |
| 运行时归档 | Makefile | wasm-rt目标编译lib/wasm32-wasi/libspinel_rt.a |
编译器会设置_WASI_EMULATED_SIGNAL、_WASI_EMULATED_MMAN等仿真宏,并用-mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh=false完成异常降级。
常见问题
Q:为什么make之后直接编译 wasm 会失败?A:wasm 运行时归档(lib/wasm32-wasi/libspinel_rt.a和各包的sp_*_wasi.o)默认不构建,需要显式执行make wasm-rt。
Q:浏览器能跑吗?A:可以。当前 Chrome、Firefox、Safari 和 Node 均已实现exnref异常指令,带 WASI shim 的浏览器可直接加载模块。
Q:多线程的 Ruby 服务能部署到 wasm 吗?A:目前不行。含Thread等并发原语的程序在编译期即被拒绝,参见上文"目标差异"部分。
总结
用 Spinel 把 Ruby 应用带上 WebAssembly 只需三步:装好 wasi-sdk →make wasm-rt→spinel --target=wasm32-wasi app.rb。部署目标覆盖 wasmtime、Node 和主流浏览器。记住三件事:wasm32 下Integer 是 32 位、没有 Fiber/Thread、没有网络 socket——先对照 docs/limitations.md 检查你的程序,再动手编译,就能一次通过。
完整特性清单与限制请阅读 docs/wasm.md 与 docs/limitations.md。
【免费下载链接】spinelRuby AOT compiler项目地址: https://gitcode.com/gh_mirrors/spin/spinel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考