把一个连操作系统、图形界面和代码库都揉进单一内存快照的活体编程环境直接塞进网页标签页,代价是消息发送速度比原生版本慢 27 倍。开发者 Massimo Nocentini 近期上线了 Pharo World(位于 massimo-nocentini/pharo-vm 仓库的 pharo-12 分支),用 WebAssembly 在浏览器里直接跑起了现代 Smalltalk 环境 Pharo 15。这并非一次简单的编译移植,而是一场在浏览器沙盒里舍弃极限性能、换取现代工具链完整存活的架构妥协。

打开网页,系统默认加载 23.6 MB 的 Pharo 15 build 50 镜像(完整包名为 Pharo15.0-SNAPSHOT.build.50.sha.b85960fcc6.arch.64bit.zip),同时提供 23.6 MB 的 Pharo 14 build 785 和 23.1 MB 的 Pharo 13.1 build 749。用户可以直接往浏览器里拖拽导入 .st 源码、.zip 压缩包,或是成套的 .image、.changes 与 .sources 文件。

整个环境跑在浏览器的 IndexedDB 本地存储上,可用配额约 10240.1 MB,随附保存的文件上限为 256 MiB 和 50,000 个文件。更关键的突破在于,它支持配置自定义 CORS 代理,首次在网页端跑通了内置 Git 工具 Iceberg 的 https 克隆与推送,以及 Metacello 的 github:// 包加载。

为什么不用 JS 重写,非要走 Wasm 硬骨头

Smalltalk 是面向对象与图形化 IDE 的鼻祖,核心机制是把整个系统的对象状态、代码和开发工具封存在一个内存镜像(Image)里,交由虚拟机加载。过去要在网页里跑 Smalltalk,主流路径是像 SqueakJS 那样用纯 JavaScript 重写虚拟机。

但纯 JS 路线撞上了现代 Pharo 的演进高墙。SqueakJS 采用简单无优化的 Smalltalk-to-JS JIT,2023 年 11 月在 Apple Silicon Mac 浏览器中测得 benchFib 发送速率为 300 万到 700 万/秒(原生 Cog 约 3.1 亿/秒)。更致命的是,另一个衍生自 SqueakJS 的项目 SmalltalkJsVm 因为缺乏原生外部函数接口(FFI)支持,在启动现代 Pharo 时被迫直接跳过 Git 客户端 Iceberg。没有 Git 协同,现代 Pharo 就退化成了只能看不能写的玩具。

浏览器运行 Smalltalk 两条技术路线对比 纯 JS 重写路线 (SqueakJS 派生) • 核心机制:Smalltalk-to-JS 简易 JIT • 速率表现:300万–700万 sends/s • 架构短板:缺乏现代 64 位原生 FFI • 致命代价:被迫跳过 Iceberg Git 工具链 C 语言编译 Wasm (Pharo World) • 核心机制:Emscripten 编译 StackVM • 内存映射:MEMORY64=2 降维提速 15%–18% • 接口方案:单线程编译期静态注册 FFI • 核心收益:完整保全 Pharo 15 与 Git 工作流

为了保住工具链,Massimo Nocentini 选择通过 Emscripten 将官方生成的 C 语言虚拟机直接编译为 WebAssembly。我更在意的不是它能在网页里点开窗口,而是它为了跨过浏览器沙盒限制所动的三刀底层手术:

  • 砍掉 JIT 退回解释器.完全放弃 Pharo 原生的 Cog JIT 编译器,仅将生成的 C 语言 StackVM 解释器编译为 Wasm,同时禁用多线程 FFI,让所有 FFI 在同一线程运行。
  • 指针降维反常识提速.系统支持 64 位镜像和内部 64 位指针,但在浏览器端通过 MEMORY64=2 将其降低映射到 32 位 WebAssembly 内存,而 Node.js 端直接链接使用 memory64。反直觉的是,浏览器端的降维配置反而比原生 memory64 方案快 15%–18%。
  • 20 毫秒时间片轮询.浏览器在 Web Worker 中运行虚拟机,由 JavaScript 反复调用 vm_resume()。虚拟机通常以 20 ms 的时间片运行并交还控制权,以此处理输入输出和定时器,图形界面 Morphic 则通过 WebDisplayPlugin 绘制到 OffscreenCanvas。

慢 27 倍的性能账单与沙盒硬边界

砍掉 JIT 直接把性能拉回了二十年前的解释器时代。在 Node.js 25.2.1(使用 memory64)环境下测试,Pharo Wasm 的 tinyBenchmarks 字节码吞吐量为 1.545 亿/秒,比原生 StackVM 解释器的 9.41 亿/秒慢约 6.1 倍;消息发送为 950 万/秒,比原生 StackVM 的 4800 万/秒慢约 5.1 倍;26 benchFib 耗时 41 ms(原生 8 ms,慢约 5.1 倍);GC 压力测试 300 万数组耗时 2,510 ms(原生 1,460 ms,慢约 1.7 倍)。

如果拿去和包含 Cog JIT 的完整原生虚拟机硬碰硬,差距会被进一步拉大。该 Wasm 移植版在 tinyBenchmarks 上的字节码吞吐量慢约 14 倍,消息发送吞吐量更是慢约 27 倍。需要注意,这套跑分是在 Node.js 下测得的,且由于测试硬件与宿主环境不同,其 950 万/秒的速率不能直接与 SqueakJS 的 300 万–700 万/秒横向比快慢,但数量级上的折损已成定局。

Pharo Wasm 性能折损与沙盒内存硬指标 27 倍 消息发送吞吐差距 相比含 Cog JIT 原生 VM 字节码吞吐亦慢约 14 倍 950 万/秒 Wasm 消息发送速率 Node.js 25.2.1 实测值 比原生解释器慢约 5.1 倍 710 MB 启动后线性内存占用 老年代上限 512 MiB 不支持永久空间分配

比算力折损更难绕开的,是浏览器沙盒铁板一块的资源与系统边界。启动后的虚拟机直接吃掉约 710 MB 线性内存,老年代空间(old space)默认上限被锁在 512 MiB,且不支持永久空间(permanent space)。

在系统能力上,它不支持 dlopen 或外部动态插件,FFI 只能调用编译期静态注册的库和函数;没有 OSProcess、不能 fork 进程,也没有 SSH 和原生 SSL 插件。由于单线程时间片轮询的限制,长时间运行的原语或 FFI 回调根本无法让出控制权。一旦代码卡死,用户点击页面上的 Stop 按钮,若 3 秒后未响应,宿主环境会直接粗暴替换掉整个 Web Worker,未保存的工作当场灰飞烟灭。

复活 Git 的代价:代理服务器上的明文钥匙

舍弃二十七倍算力事小,把代码仓库钥匙交给第三方代理事大。

在没有原生网络套接字和 SSL 插件的浏览器里跑通 Iceberg Git,靠的是编译期静态绑定 FFI 与浏览器自身的网络传输结合。但浏览器天然有跨域(CORS)封锁。除非代码托管商原生放行跨域请求,否则用户必须在 Settings 里配置自定义的 CORS 代理,把请求拼成代理 URL 加 github.com/pharo-project/pharo.git 这类路径转发出去。

这就引出了整个架构里最拧巴的安全悖论。庄子讲“有机械者必有机事,有机事者必有机心”,技术上用代理绕过浏览器同源策略的巧思,立刻在信任边界上砸出一个大洞。

早期 PC 互联网刚普及时,开发者为了穿过企业防火墙常借道公共 HTTP 代理,结果频频引发凭证截留事故;今天的浏览器沙盒把当年网络层的尴尬原封不动地重演了一遍。原文设置面板里不得不写下一段极其直白的警告:代理服务器能看到流经它的所有代码,以及推送(push)时输入的用户名和 Token。系统虽然限制了凭证绝不走站点默认配置、只走用户填写的 My proxy 或直连,但只要你填了一个非自己掌控的第三方 CORS 代理,就等于把代码库大门的钥匙双手奉上。

  • 风险.切勿在公共或来路不明的 CORS 代理下执行 Git push,你的账号 Token 与私有源码会在代理节点彻底裸奔;写完代码务必随时手动保存到 IndexedDB,防备 3 秒超时强杀 Worker。

谁真正受益,接下来看什么

把一头 64 位的传统语言巨兽硬塞进网页,哪怕掉了九成五的牙,它依然能咬动现代工程。对于 Smalltalk 与 Pharo 社区以及高校教学而言,23.6 MB 镜像点开即用、自带完整 Git 协同的体验,直接铲平了过去劝退新手的本地环境配置门槛。对于研究 WebAssembly 与语言虚拟机的开发者,它提供了一份极具参考价值的工程样本,展示了如何用 MEMORY64=2 降维和 20 毫秒时间片,去驯服一个重度依赖内存快照与自定义渲染的老派系统。

但这项工程目前只能算跑通了架构闭环,远未到能替代桌面主力 IDE 的时刻。接下来真正决定它生死的有两个硬指标:一是社区能否在 Wasm 端引入真正的 JIT 编译支持,把那 14 到 27 倍的性能鸿沟填平;二是 WebAssembly 内存与网络标准演进,能否把启动即占 710 MB 的内存胃口降下来,并在不裸奔 Git 凭证的前提下解决跨域通信。在此之前,打开那个网页链接,它依然是人类软件史上最优雅的活体系统在现代浏览器里最倔强的一次呼吸。