打开网易新闻 查看精彩图片

一个 ahead-of-time(提前编译)的 WebAssembly 编译器,最近两个月干了件挺有意思的事——它开始啃 Scheme 代码了。不是玩具示例,是真刀真枪的 Hoot Scheme-to-Wasm 编译器输出。结果?三个坑,其中一个还是写博客的时候自己发现的。

坑一:浏览器白送的大整数,得自己造轮子

坑一:浏览器白送的大整数,得自己造轮子

Hoot 最初是给浏览器写的。浏览器里有 BigInt,大整数运算直接调宿主环境就行。Hoot 生成的 Wasm 文件用 externref(外部引用类型)包一层,哈希、身份判断都能做。

Wastrel 没这待遇。它是 ahead-of-time 编译器,跑在裸机上,没有浏览器兜底。

作者翻了翻仓库,找来了 mini-gmp——GNU 多精度运算库的精简版。加减乘除先顶上,以后真要性能了,可以无缝切完整版 GMP。这是 Wastrel 第一个不是 Wasm 模块自己定义的托管数据类型,藏在 externref 后面。

类型码分配机制因此补了一刀。以后弱引用、ephemeron(短暂关联对象)这些宿主类型,都走这个口子。

作者顺便吐槽了一句:大整数早该进 Wasm 标准了,像 stringref(字符串引用提案)那样多好——可惜后者已经凉了。

坑二:异常处理标准换了,Hoot 还在用旧版

坑二:异常处理标准换了,Hoot 还在用旧版

打开网易新闻 查看精彩图片

Hoot 之前 emit(生成)的是预标准化时代的异常处理指令集。去年 7 月 Wasm 异常处理正式定稿,Hoot 一直没跟进。

Wastrel 作者干脆先更新 Hoot,再实现新异常。理由很现实:新规范在 Wastrel 里更好做。

Chris Fallin 在 Wasmtime 里折腾过的那些麻烦,Wastrel 躲过了大半。Wasmtime 是 JIT(即时编译),实例动态创建,异常标签的类型码得运行时分配。Wastrel 实例全集编译期已知,类型码静态分配就行。

后端更省事——直接用 setjmp/longjmp,不用自己折腾栈展开。

坑三:写博客时发现代码有 bug,C99 规范救了场

作者原本只想在博客里轻描淡写提一句:setjmp 够用,因为我们不复用临时变量,全靠 GCC 自动回收寄存器和栈槽。按 C99 规范,setjmp 和 longjmp 之间的局部变量如果被修改,必须用 volatile 声明,否则值未定义。

作者琢磨:Wastrel 生成的代码不会在这之间修改变量,安全。

写到一半,突然僵住——local.set 呢?

打开网易新闻 查看精彩图片

Wasm 的局部变量赋值(local.set)对应 C 的自动存储期对象修改。如果源码 Wasm 里有局部变量在 setjmp 之后、longjmp 之前被改写,而 Wastrel 没加 volatile,行为未定义。

「因为写这篇博客,我回去修了 bug。」作者在草稿里直接记了一笔。

这个发现链条挺典型:先假设安全→写文档梳理→发现反例→回滚修复。没这篇博客,坑可能埋到生产环境才炸。

接下来干什么

接下来干什么

大整数基础设施有了,异常处理对齐新标准了,setjmp 的 volatile 问题堵上了。作者列了几个待办:弱引用、ephemeron、更多宿主类型。

以及那个反复出现的念头——推动大整数进 Wasm 标准,哪怕是个缩水版。

最后一个细节:mini-gmp 的链接选项留着,性能不够时随时换 GMP。这种「先跑起来,再换引擎」的策略,跟整个 Wastrel 的渐进式路线一致。

所以问题来了:如果你写一个 ahead-of-time 编译器,会为了省事直接用 setjmp,还是从头实现一套异常栈机制?