На iOS нельзя делать JIT-компиляцию — это главная причина, почему эмулятор Dolphin не появился в App Store. Но Apple дала одно исключение: браузеры. JavaScriptCore и WebAssembly спокойно используют JIT. Что если вместо машинного кода генерировать Wasm-байткод, а браузер сам скомпилирует его в нативный? Автор вдохновился постом Энди Уинго и сделал для диплома эмулятор Game Boy — сначала интерпретатор, потом JIT-to-Wasm. Назвал проект WATaBoy.
Реализация на Rust. Для генерации Wasm-байткода используется крейт wasm-encoder. Готовый модуль нужно слинковать с основным, но напрямую из Wasm вызвать компиляцию нельзя — приходится через JavaScript. Автор написал функцию linkNewModule: она читает байткод из памяти основного модуля, компилирует его через WebAssembly.Module и Instance, добавляет экспортированную функцию в таблицу косвенных вызовов основного модуля. Вызов JIT-функции идёт через инструкцию call_indirect, которую вставляют inline-ассемблером в Wasm (экспериментальная фича Nightly Rust). Чтобы экспортировать и расширять таблицу функций, в build.rs добавляют флаги --export-table и --growable-table.
Результаты бенчмарков на MacBook Air M2 с Safari, Chrome и Firefox. Запускали три ROM: Pokémon Blue, Zelda: Link’s Awakening и Tobu Tobu Girl. JIT-to-Wasm оказался в среднем в 1,2 раза быстрее интерпретатора, запущенного нативно, и в 1,5 раза быстрее интерпретатора в Wasm. Лучший результат показал Safari — приятный бонус для iOS, где только WebKit.
Автор отмечает, что основной тормоз сейчас — эмуляция PPU (нужно предсказывать прерывания, иначе JIT падает в интерпретатор). Планирует доделать аудио, поддержку Game Boy Color и оптимизировать PPU. Признаёт, что кодогенерация Wasm пока неудобна: нет инструментов вроде DynASM, всё пишут вручную. И есть фундаментальные ограничения — например, fastmem из Dolphin не реализовать, потому что в Wasm нет восстановления после неверного доступа к памяти. Но как proof of concept подход работает: если вам нужен CPU-bound эмулятор на iOS, JIT-to-Wasm — вполне рабочий путь.