Более надежная схема компиляции для модулей Kotlin Multiplatform
Читайте эту статью на других языках:
Текущий подход к компиляции проектов Kotlin Multiplatform работает, но иногда может приводить к неожиданному или труднопрогнозируемому поведению. Например:
- Анализ в IDE расходится с компилятором в отношении перегрузки, вывода типов или того, должен ли код вообще компилироваться — причем IDE ведет себя строже компилятора.
- Ваш код в commonTest может неожиданно вызывать что-то из исходного набора платформы, нарушая ваши предположения.
В Kotlin 2.5.0-Beta1 мы представили опциональный подход «раздельной компиляции» для KMP, который решает обе проблемы: делает результаты компиляции согласованными с анализом в IDE и более последовательно указывает на проблемные вызовы библиотечного кода из общих исходных наборов. В качестве бонуса этот подход позволяет нам реализовать инкрементальную компиляцию для общих исходных наборов.
Функция раздельной компиляции является экспериментальной и по умолчанию отключена. Чтобы включить ее, добавьте следующий параметр компилятора в файл gradle.properties (обратите внимание на известные проблемы):
Давайте подробнее рассмотрим проблему и ее решение.
Как мультиплатформенные объявления разрешаются внутри модуля
Внутри модуля мультиплатформенный код ведет себя предсказуемо:
Компилятор и IDE согласны друг с другом: код в commonMain также компилируется под другие платформы, например под Kotlin/JS, и в jsMain может не быть объявления fun foo(). expect/actual механизмы существуют как раз для решения этой проблемы: они явно связывают платформо-специфичные объявления с общими.
Можно ожидать, что ссылка также будет помечена как неразрешенная, когда функция foo() объявлена в зависимости (модуле в том же проекте или бинарном артефакте, таком как стандартная библиотека Kotlin или kotlinx-coroutines). К сожалению, при текущей схеме компиляции именно здесь IDE и компилятор расходятся во мнениях.
Как мультиплатформенные объявления разрешаются между модулями
Чтобы понять, откуда растут корни проблем, давайте внимательнее посмотрим на текущую настройку компиляции.
Настройка компиляции Kotlin Multiplatform
В проекте KMP модуль с общим кодом обычно состоит из нескольких исходных наборов: в примере выше это общий исходный набор (commonMain) и исходный набор платформы для объявленной цели (jvmMain для Kotlin/JVM).
При такой настройке компилятор может создавать следующие артефакты:
- Платформенный артефакт для каждого исходного набора платформы, такого как jvmMain — *.jar на JVM, *.klib на других платформах.
- Метаданные KLIB для каждого общего или промежуточного исходного набора (например, commonMain или nativeMain). Метаданные KLIB содержат все объявления из скомпилированного исходного набора без тел.
Так где же начинаются проблемы?
Общий код компилируется против платформенных артефактов; IDE не согласна
Во время компиляции исходного набора платформы как код внутри него (jvmMain), так и код во всех соответствующих разделяемых исходных наборах (commonMain и других промежуточных исходных наборах) компилируется против платформенных артефактов его зависимостей. Здесь код из commonMain получает возможность неявно разрешаться в объявление jvmMain.
Если вы ожидаете такого поведения, все в порядке. Однако было бы более прозрачно и предсказуемо, если бы commonMain мог вызывать только объявления из commonMain зависимости (как указано в метаданных KLIB зависимости).
Именно это уже предполагает анализ кода в IntelliJ IDEA. Поскольку объявления jvmMain не включены в метаданные KLIB, IntelliJ IDEA сообщает об ошибке неразрешенной ссылки, когда commonMain ссылается на что-то, не объявленное в общем коде явно:
Тот же механизм задействован и в исходных наборах commonTest, поскольку тесты также «зависят» от основного кода: commonTest, скомпилированный против jvmMain, может успешно вызывать объявление из платформенного кода, вместо того чтобы быть ограниченным объявлениями в commonMain. IDE сообщает о той же проблеме неразрешенной ссылки:
Давайте посмотрим, как раздельная компиляция решает эти проблемы.
Как раздельная компиляция решает проблему
Раздельная компиляция в KMP заставляет компилятор вести себя строже (согласуясь с ожиданиями IDE) при компиляции общих исходных наборов проектов KMP. Это делает общий опыт более предсказуемым, хотя вам может потребоваться скорректировать старый код для прохождения более строгих проверок во время компиляции.
Давайте рассмотрим конкретные примеры кода и то, что с ними происходит при переключении между схемами компиляции.
Красивый код в IDE, но ошибка во время компиляции
Когда вы включаете раздельную компиляцию, этот код перестает компилироваться, поскольку компилятор теперь разрешает вызовы общего кода строго с использованием объявлений метаданных KLIB:
Исправление заключается в том, чтобы показать явную связь: объявить expect-класс в lib/commonMain и сделать платформенный класс Foo actual-классом:
Аналогично для commonTest: при раздельной компиляции тестовый код не будет разрешаться в платформенный вызов в jvmMain. Если вы хотите протестировать именно это, объявите соответствующий expect в commonMain:
IDE и компилятор не согласны насчет выбранной перегрузки
Когда в lib/commonMain и lib/jvmMain объявлено несколько перегрузок, IDE в настоящее время интерпретирует вызов из app/commonMain иначе, чем компилятор:
IDE разрешает вызов foo("") в объявление fun foo(x: Any) в lib/commonMain, так как предполагает, что общий код может зависеть только от общих объявлений. Но во время фактической компиляции app/commonMain в настоящее время видит оба объявления и выбирает более специфичную перегрузку fun foo(x: String). Во время выполнения выводится «platform».
При новой схеме компиляции вызов разрешается в fun foo(x: Any) в lib/commonMain, и во время выполнения выводится «common».
Неожиданный вывод типов
Более сложная проблема может возникнуть, когда ошибка срабатывает не в месте вызова в app/commonMain, а в платформенном коде. В следующем примере IDE фактически не видит проблем, но компиляция JVM завершается неудачно.
actual-классы могут иметь другие супертипы, чем их expect-аналоги, поэтому вы вполне можете прийти к следующей структуре кода:
IDE сопоставляет только объявления commonMain, что позволяет ей (корректно) вывести возвращаемый тип функции currentEvent() как Event и разрешить ссылку .name.
Однако на JVM библиотека объявляет дополнительный супертип для каждого actual-класса. При компиляции app для JVM компилятор в настоящее время разрешает общий код относительно JAR-файла eventsLib, который содержит версию классов eventsLib/jvmMain. Из-за наличия двух супертипов вывод возвращаемого типа для вызова currentEvent() приходит к Any, у которого нет .name, и компиляция JVM завершается неудачно.
При включенной раздельной компиляции код app/commonMain разрешается только относительно объявлений eventsLib/commonMain. Таким образом, при компиляции JVM-таргета app компилятор уже имеет разрешенный общий код и вывел тип возвращаемого значения Event для функции currentEvent(). Компиляция JVM выполняется отдельно, ей ничего не нужно выводить, и она успешно завершается.
Как включить раздельную компиляцию
Функция раздельной компиляции является и по умолчанию отключена. Чтобы включить её, добавьте следующую опцию компилятора в ваш файл gradle.properties (обратите внимание на известные проблемы ниже):
Известные проблемы
Для корректной работы раздельной компиляции в проектах KMP важно, чтобы авторы мультиплатформенных библиотек публиковали метаданные KLIB. Это поведение по умолчанию для задач публикации плагина KMP Gradle, но новая схема компиляции делает метаданные KLIB обязательными.
Существуют некоторые известные проблемы с раздельной компиляцией KMP, которые всё ещё исправляются:
- Некоторые проблемы с общей компиляцией cinterop (например, KT-88178 или KT-41509). Вряд ли они затронут типичный проект KMP, но включайте режим раздельной компиляции с осторожностью, если вы используете взаимодействие с C.
- Модули KMP с общими наборами исходного кода и одной объявленной целью пока не затрагиваются раздельной компиляцией.
- Различные мелкие проблемы, такие как KT-88148.
Отправка отзывов
Мы будем благодарны за любые отзывы о раздельной компиляции: оставьте комментарий в специальном тикете YouTrack.






