WebAssembly (WASM) продолжает завоевывать популярность как компактный и быстрый формат для выполнения кода в различных средах. Рост потребности в мобильных приложениях, способных работать с WASM-модулями, породил необходимость адаптации существующих WASM-решений для мобильных платформ. Одним из ярких примеров такой адаптации стал проект портирования компилятора Chicory, написанного на Java, на Android. Этот процесс ознаменовал собой ряд серьезных технических вызовов и нашел интересные решения, которые сегодня могут быть полезны разработчикам, работающим с WASM на мобильных устройствах. Этот материал раскрывает подробности создания Android-бэкенда для Chicory, особенности Dalvik и ART, ограничения среды и способы обхода проблем с тестированием и отладкой.
Chicory представляет собой чистую Java-реализацию WebAssembly-рантайма, включающую как интерпретатор, так и компилятор. Компилятор переводит WASM в Java-байткод, который традиционно запускается на JVM. Такой подход отлично работает на настольных и серверных платформах с классической JVM, но Android накладывает некоторые ограничения, связанные с особенностями своей виртуальной машины и байткода. В Android используется Dalvik и более современный ART — тонко настроенная виртуальная машина с собственным форматом байткода (DEX-файлы), который существенно отличается от классического Java-байткода. Основным вызовом при портировании Chicory на Android стало преобразование Java-байткода, генерируемого компилятором, в Dalvik-байткод.
Несмотря на то, что Android SDK включает инструмент d8, который транслирует Java-байткод в DEX на этапе сборки, динамическая генерация кода во время исполнения и его загрузка в Dalvik-рантайм требуют других решений, так как стандартные загрузчики классов Android не поддерживают на‑лету добавление Java-байткода. Одним из ключевых инструментов, которые помогли преодолеть этот разрыв, стал DexMaker — библиотека для динамической генерации Dalvik-байткода непосредственно в рантайме Android. DexMaker широко применяется для создания динамических прокси и моков в тестировании, но получилось переориентировать её для генерации Dalvik байткода из WASM-кода в Chicory. Переход с Java-байткода на Dalvik требует глубокого понимания архитектурных отличий. Прежде всего, Java-байткод — стековая машина, где инструкции манипулируют стэком операндов, а Dalvik использует регистры как основной механизм работы с данными.
Это означает, что компилятор должен уметь не только генерировать Dalvik-код, но и эффективно переворачивать логику со стековой в регистровую модель с учетом оптимизаций и контроля потока. Пример с циклом, реализованным как на Java-байткоде, так и на Dalvik, демонстрирует принципиальные различия. В JVM параметры цикла и переменные хранятся преимущественно на стеке, что упрощает управление такими структурами. На Dalvik пришлось выделять регистры для каждой переменной с сохранением их значений в регистрах сквозь итерации, что усложняет логику, но делает возможной эффективную работу на Android. Ещё одна значимая сложность связана с ограничениями среды выполнения Android.
Virtual Machine на платформе ART имеет ограничение по размеру стека на потоки, который по умолчанию равен 1 мегабайту. Для WASM-модулей с глубокой рекурсией или большим числом вложенных вызовов легко добиться StackOverflowError. Опыт показал, что для нормальной работы существенно помогает запуск вычислений в собственных потоках с увеличенным размером стека, однако такой подход иногда приводит к падениям приложения с ошибками сегментации, что требует дальнейших исследований и тестирования. Недостатки памяти также проявились в высоком потреблении heap для самого компилятора, особенно при генерации больших DEX-файлов. DexMaker сохраняет в памяти промежуточное представление всех сгенерированных методов до финальной сборки DEX, что создает значительную нагрузку на оперативную память.
Снижение границ по количеству методов в одной «чанке» класса до примерно 200 помогло значительно уменьшить использование памяти, что в итоге позволило без проблем запускать реальные WASM-приложения, такие как mcp.run fetch servlet. Тестирование портированного компилятора на Android стало настоящей проблемой. Типичные Android-тесты используют Instrumentation framework, который имеет существенные накладные расходы. Запуск большого количества юнит-тестов с таким подходом был слишком медленным и непрактичным при интенсивной разработке.
Кроме того, JVM, работающая на хост-машине, не способна загружать DEX байткод, что отсекает возможность тестирования компилятора вне Android. Для преодоления этих замечаний было реализовано решение с использованием классической командной строки ART на Android через dalvikvm64. Такой запуск позволяет обойти Instrumentation framework и стартовать юнит-тесты напрямую в Dalvik-окружении, что заметно ускоряет тестирование. Важным нюансом является необходимость явного указания класспаса к APK с тестами и правильным конфигурированием зависимостей, чтобы включать в сборку всю необходимую инфраструктуру, в том числе JUnit 5, которая официально не поддерживается Android, но может быть интегрирована неофициально. Данный подход позволяет уменьшить время запуска тестов до порядка 14 секунд, что примерно в 30 раз быстрее, чем при использовании Instrumentation framework, а с дальнейшей кастомизацией тестового запуска, без JUnit, удалось приблизиться к рекордным 7 секундам выполнения набора тестов прямо на устройстве.
Отладка компилятора и сгенерированного Dalvik-кода также становится возможной при помощи подключения дебаггера через настроенный плагин JVM TI и JDWP сервер. Для этого достаточно запустить dalvikvm64 с правильными параметрами запуска, открывающими порт для удаленной отладки, и подключиться к нему из привычной IDE с поддержкой отладки Java. Этот метод значительно облегчает исследования сложных проблем и поиск ошибок в работе Android-бэкенда компилятора. Итоговым результатом проделанной работы стал экспериментальный, но функционирующий Android бэкенд компилятора Chicory, позволяющий запускать и компилировать WASM-модули непосредственно на Android-устройствах с лучшей производительностью, чем при интерпретации. Цель дальнейших исследований — оптимизация использования регистров в Dalvik, решение вопросов со стабильностью работы с увеличенным стеком, а также расширение и улучшение инструментария тестирования и отладки.