Kotlin MultiplatformMiddleTechnical
Как работает interop с C/Objective-C/Swift и какие ограничения у exported API?
KMP экспортирует Kotlin-код в Objective-C/Swift через заголовочный файл .h, генерируемый компилятором Kotlin/Native. Ограничения: нет generics в Swift, suspend-функции превращаются в callback-based API, sealed-классы теряют exhaustive pattern matching.
Механизм interop Kotlin/Native с Objective-C и Swift
Kotlin/Native компилирует общий и iOS-специфичный код в нативный фреймворк (.framework). Внутри фреймворка находится заголовочный файл MyFramework.h, который Xcode автоматически импортирует как Swift-модуль. Все публичные Kotlin-классы, интерфейсы и функции доступны в Swift без дополнительных оберток.
Что экспортируется автоматически
- Классы и объекты (
object) — становятся Obj-C классами; - Интерфейсы — протоколы Swift;
enum class— перечисления с ограниченным mapping;- Top-level функции — статические методы на вспомогательном классе;
suspend-функции — генерируют два варианта: callback + completionHandler для Swift 5.5+.
Настройка экспорта в build.gradle.kts
kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
binaries.framework {
baseName = "SharedKit"
// Экспортировать зависимость, чтобы её классы были видны в Swift:
export(project(":analytics"))
// Статическая линковка (рекомендуется для скорости запуска):
isStatic = true
}
}
sourceSets {
val commonMain by getting {
dependencies {
api(project(":analytics")) // api, не implementation!
}
}
}
}
Аннотации для тонкой настройки
// Переименовать класс/метод для Swift:
@ObjCName("UserProfileSwift", swiftName = "UserProfile")
class UserProfileKt(val id: String, val name: String)
// Запретить экспорт внутреннего класса:
@HiddenFromObjC
class InternalHelper
// Пометить как throws для Swift:
@Throws(IllegalArgumentException::class)
fun parse(input: String): Int = input.toInt()
Interop с C через cinterop
// В файле src/nativeInterop/cinterop/libpng.def:
headers = png.h
compilerOpts = -I/usr/local/include
linkerOpts = -L/usr/local/lib -lpng
// В build.gradle.kts:
val iosMain by getting {
dependencies {
implementation(cinterop("libpng"))
}
}
Ключевые ограничения exported API
- Generics стираются:
List<String>в Swift становится[Any], нужны обёртки; - Suspend → callback: Swift Concurrency получает
async-версию только с Kotlin 1.9+ и включённым флагом-Xexport-kdoc; - Sealed-классы: Swift видит их как обычную иерархию классов,
switchне exhaustive; - Kotlin.collections:
MutableListприходит какNSMutableArray, изменения через Kotlin-интерфейс; - Exceptions: непомеченные
@Throwsисключения вызывают crash в Swift — правило «аннотируй всё публичное»; - Companion object: отображается как
MyClass.companion, а неMyClass.
Подводные камни
- Забыть
api()вместоimplementation()для зависимостей, классы которых нужны в Swift — они исчезнут из заголовка; - Использовать
export()безapi()вcommonMain— Gradle выдаст предупреждение, а в рантайме будетClassNotFoundException; - Suspend-функции без обёртки — Swift 5.5 async/await работает, но только если включить
-Xexport-kdocи использовать Kotlin ≥ 1.9; - Kotlin
Longотображается вKotlinLong(объект), а неInt64— арифметика в Swift неудобна; - Переименование классов через рефакторинг ломает уже написанный Swift-код без предупреждений на стороне Kotlin;
data class copy()не попадает в Obj-C API — Swift не сможет клонировать объект без ручной обёртки;- Динамические фреймворки (
isStatic = false) замедляют запуск приложения на iOS — Apple рекомендует статические; - Circular references между Kotlin и Swift объектами не управляются ARC/GC — возможны утечки памяти.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.