WATaBoy:将游戏机说明 JIT 到 WASM 超越本地解释器
发布于 2026 年 6 月 28 日 背景 本文假设读者对即时编译的概念有所了解。Dolphin 没有出现在 iOS 上,因为在 iOS 上不能执行 JIT 编译。这是 OatmealDome 博客文章 "为什么 Dolphin 不会出现在 App Store" 的简要总结。自从阅读那篇文章后,我一直在想,要让像 Dolphin 这样 CPU 密集型的模拟器在 iOS 上工作需要什么。我们就必须等几年,直到 iPhone 的 CPU 足够快,能通过解释器运行 Dolphin 吗?好吧,Apple 对其 JIT 限制有一个例外:网页浏览器。JavaScriptCore,WebKit 的 JS 引擎,利用 JIT 编译来提高性能。因此,如果一个 JS 函数被调用足够多次,最终会被优化并编译成本地机器代码。WebAssembly 也是如此。那么,如果我们就利用这一点呢?我们可以生成 Wasm 字节码,而不是直接生成本地机器代码,最终由网页浏览器将其编译成本地机器代码。在阅读 Andy Wingo 的博客文章 "WebAssembly 中的即时代码生成" 后,我知道这样的事情是可能的。事实上,一些项目已经使用了这一技术,即 Jiterpreter 和 v86,但在撰写时,还没有任何游戏主机模拟器使用它,也没有人将其性能与本地运行的解释器进行比较,看看哪个更快。所以,作为我的本科毕业设计项目,我决定构建一个 Game Boy 模拟器,首先使用解释器,然后使用 JIT 到 Wasm。这个项目主要用作概念验证和比较每种方法性能的基准。对于这篇博文的其余部分,我将称其为 “JIT 到 Wasm” 而不是 “Wasm JIT”,以避免与 JS 引擎本身的操作(重新编译 Wasm 到机器代码)混淆。 WATaBoy 的屏幕截图,一个将 SM83 编译为 Wasm 的 Game Boy 模拟器。任何对模拟器有些了解的人看到这一点都会翻白眼,因为一个 Game Boy 模拟器如何能够从 JIT 编译中受益?幸运的是,GameRoy 的博客文章准确描述了如何在保持周期准确性的同时实现这一点:预测何时会发生中断,每当 JIT 块可能被中断时,缓慢回退到一个解释器,懒惰地评估任何通过 MMIO 访问的非 CPU 非 Game Boy 组件。GameRoy 的 JIT 仅针对 x86,但几乎所有优化技术仍适用于我们的 JIT 到 Wasm。如果你对 Game Boy 模拟方面的具体细节感兴趣,绝对值得一看;这是我很大的灵感来源。然而,Game Boy 模拟器从 JIT 编译中所受益的程度不及第六代主机。但它制作起来要容易得多,而且确实符合我本科毕业设计项目的范围。 实现 现在,为了缩小这篇博客文章的范围,我将带你了解 WATaBoy 中我在其他地方找不到的最广泛适用的部分:从 Rust 内部的 Wasm 代码生成和延迟链接。WATaBoy 有很多有趣的地方,特别是从 Game Boy 模拟的角度来看(例如,SIMD 瓦片渲染),但那些实现细节值得单独写。不过,如果不感兴趣,可以跳到结果部分。 通常我们会使用工具如 wasm-bindgen 和 wasm-pack 在 Rust 和 JavaScript 之间生成粘合代码。但这些工具在低层次上处理 Wasm 时会引发一些易用性问题。因此,我使用了一种类似于 "一种艰难的 Rust 到 WebAssembly" 中描述的方法。这仅仅意味着我们将通过 C ABI 传递数据,使用指针和缓冲区长度,而不是 JavaScript 对象。提醒一下,您需要 Nightly Rust,因为稍后我们将使用一点内联 Wasm。因此,请运行: rustup default nightly 想要切换回去,只需再次运行,然后将“nightly”换成“stable”。 创建一个新的库: cargo new --lib jit-to-wasm 嘿,看看,我们这里已经有一些代码: pub fn add(left: u64, right: u64) -> u64 { left + right } 对于我们的简单示例,让我们尝试在运行时生成一些做相同事情的 Wasm 字节码。Wasm 代码生成 wasm-encoder crate 将是我们的唯一依赖项。有了它,我们可以使用一种构建器模式发出 Wasm 指令的字节。它并不是为了我们的 JIT 用例设计的,所以有一些易用性问题和一点样板,但这总比手动编写原始字节数组强。 :) [package] name = "jit-to-wasm" version = "0.1.0" edition = "2024" [lib] # 必须生成 .wasm 文件 crate-type = ["cdylib"] [dependencies] wasm-encoder = "0.252.0" 现在,让我们用它生成一个包含 ‘add’ 函数的 Wasm 模块的字节码。这就是我提到的样板代码: use wasm_encoder::*; fn make_add_module() -> Vec<u8> { let mut module = Module::new(); // 编码 add 函数的类型部分。 // 参数:32 位整型 lef
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡