Бэкенды компилятора: JVM, Native, JS, Wasm
Один язык — четыре способа превратиться в исполняемый код; разбираем, что это значит на практике.
Compiler backend — часть компилятора Kotlin, которая переводит общее промежуточное представление в конкретный целевой формат: байткод JVM, нативный объектный код, JavaScript или WebAssembly.
Зачем разбираться в бэкендах
На первый взгляд это деталь, но именно бэкенды объясняют, почему какой-то код «работает на Android, но не собирается под iOS». Понимание целей экономит часы недоумения над ошибками компиляции.
Kotlin/JVM
Самый зрелый бэкенд. Kotlin превращается в .class-файлы — байткод, исполняемый виртуальной машиной Java. Это то, на чём работает Android и серверный Kotlin. Здесь доступна вся экосистема JVM: библиотеки Java, рефлексия, привычный сборщик мусора. На Android именно сюда попадает androidMain.
Kotlin/Native
Компилирует Kotlin в нативный машинный код через LLVM — без виртуальной машины. Это путь на iOS, macOS, watchOS, а также Linux и Windows. У Native свой рантайм со сборщиком мусора (современный — конкурентный, без старых ограничений «замораживания» объектов). Результат для iOS — это либо .framework, либо XCFramework, который Xcode подключает как обычную зависимость.
commonMain --(JVM backend)--> байткод .class (Android)
--(Native/LLVM)--> нативный .framework (iOS)
--(JS backend)---> .js (Web)
--(Wasm backend)-> .wasm (Web)Kotlin/JS и Kotlin/Wasm
Kotlin/JS транслирует код в JavaScript для запуска в браузере или Node.js. Kotlin/Wasm — более новая цель, компилирующая в WebAssembly; на ней, в частности, работает веб-вариант Compose Multiplatform. Для мобильной разработки эти цели часто не нужны, но они показывают широту охвата.
Как работает под капотом
До бэкендов есть общий фронтенд: компилятор разбирает исходник, проверяет типы и строит промежуточное представление (IR — intermediate representation). Это IR — единая «правда» о вашем коде, из которой каждый бэкенд генерирует свой результат. Поэтому семантика языка одинакова везде: List, корутина, data class ведут себя одинаково на Android и iOS, хотя байты под ними совершенно разные.
Различается стандартная библиотека по целям. Общий kotlin.* (коллекции, строки, базовые типы) есть везде. А вот kotlin.io для файлов или конкретные обёртки над платформенным API — нет. Поэтому проект делит код на исходные наборы (source sets): общий и платформенные.
Частые ошибки
Сборка под Android идёт быстро, и появляется иллюзия, что «всё работает». Но первая же сборка под iOS через Kotlin/Native может вскрыть, что в общий код просочился JVM-только класс. Правило: проверяйте сборку под все цели регулярно, а не только под Android. Вторая ошибка — считать Native «медленнее» из-за отсутствия JIT; на практике для мобильных задач разница незаметна, а старта приложения даже выигрывает.
Итоги
- JVM — Android и сервер; Native — iOS и десктоп; JS/Wasm — веб.
- Единый фронтенд строит общий IR, бэкенды генерируют из него разные форматы.
- Семантика языка одинакова, но стандартная библиотека по целям различается.
- Регулярно собирайте под iOS, а не только под Android.