Kotlin/Native в качестве фреймворка Apple – учебник
Kotlin/Native обеспечивает двустороннюю совместимость с Objective-C/Swift. Фреймворки и библиотеки Objective-C могут использоваться в коде Kotlin. Модули Kotlin могут использоваться в коде Swift/Objective-C. Помимо этого, Kotlin/Native имеет C-совместимость. Также есть учебник Kotlin/Native как динамическая библиотека для получения дополнительной информации.
В этом руководстве вы узнаете, как использовать код Kotlin/Native из приложений Objective-C и Swift на macOS и iOS.
В этом руководстве вы:
создадите библиотеку Kotlin и скомпилируете её в фреймворк
рассмотрите сгенерированный код API Objective-C и Swift
используйте фреймворк из Objective-C и Swift
Настройте Xcode для использования фреймворка для macOS и iOS
Создание библиотеки Kotlin
Компилятор Kotlin/Native может создать фреймворк для macOS и iOS на основе кода Kotlin. Созданный фреймворк содержит все объявления и двоичные файлы, необходимые для его использования с Objective-C и Swift. Лучший способ понять эти техники – попробовать это самостоятельно. Сначала создадим крошечную библиотеку Kotlin и используем её из программы Objective-C.
Создайте файл hello.kt с содержимым библиотеки:
package example
object Object {
val field = "A"
}
interface Interface {
fun iMember() {}
}
class Clazz : Interface {
fun member(p: Int): ULong? = 42UL
}
fun forIntegers(b: Byte, s: UShort, i: Int, l: ULong?) { }
fun forFloats(f: Float, d: Double?) { }
fun strings(str: String?) : String {
return "That is '$str' from C"
}
fun acceptFun(f: (String) -> String?) = f("Kotlin/Native rocks!")
fun supplyFun() : (String) -> String? = { "$it is cool!" }
Хотя возможно использование командной строки, либо непосредственно, либо в сочетании со скриптовым файлом (таким как .sh или .bat файл), этот подход не масштабируется для крупных проектов с сотнями файлов и библиотек. Поэтому лучше использовать компилятор Kotlin/Native с системой сборки, так как это помогает загружать и кэшировать двоичные файлы и библиотеки компилятора Kotlin/Native с транзитивными зависимостями и запускать компилятор и тесты. Kotlin/Native может использовать систему сборки Gradle через плагин kotlin-multiplatform.
Основы настройки проекта, совместимого с IDE, с помощью Gradle, были рассмотрены в учебнике Базовое приложение Kotlin/Native. Пожалуйста, ознакомьтесь с ним, если вы хотите получить подробные начальные шаги и инструкции о том, как начать новый проект Kotlin/Native и открыть его в IntelliJ IDEA. В этом руководстве мы рассмотрим расширенное использование C-совместимости в Kotlin/Native и многоплатформенные сборки с Gradle.
Сначала создайте папку проекта. Все пути в этом руководстве будут относительными к этой папке. Иногда необходимо создать отсутствующие каталоги перед добавлением новых файлов.
Используйте следующий файл сборки Gradle:
plugins {
kotlin("multiplatform") version "1.9.0"
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
tasks.wrapper {
gradleVersion = "7.6"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.9.0'
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
wrapper {
gradleVersion = "7.6"
distributionType = "ALL"
}
Переместите файлы исходных кодов в папку src/nativeMain/kotlin в рамках проекта. Это стандартный путь расположения исходных кодов при использовании плагина kotlin-multiplatform. Используйте следующий блок для настройки проекта на генерацию динамической или общей библиотеки:
binaries {
framework {
baseName = "Demo"
}
}
Наряду с macOS X64, Kotlin/Native поддерживает macos arm64 и iOS arm32, arm64 и X64 целевые платформы. Вы можете заменить macosX64 соответствующими функциями, как показано в таблице:
Целевая платформа/устройство |
Функция Gradle |
|---|---|
macOS x86_64 |
|
macOS ARM 64 |
|
iOS ARM 64 |
|
iOS симулятор (x86_64) |
|
iOS симулятор (arm64) |
|
Запустите задачу linkNative Gradle для сборки библиотеки в IDE или выполните следующую команду в консоли:
./gradlew linkNative
В зависимости от варианта, сборка генерирует фреймворк в папки build/bin/native/debugFramework и build/bin/native/releaseFramework. Давайте посмотрим, что внутри.
Заголовки, генерируемые фреймворком
Каждый из созданных фреймворков содержит заголовочный файл в <Framework>/Headers/Demo.h. Заголовки не зависят от целевой платформы (по крайней мере, в Kotlin/Native v.0.9.2). Он содержит определения для нашего кода Kotlin и несколько объявлений в масштабе всего Kotlin.
Объявления среды выполнения Kotlin/Native
Внимательно посмотрите на объявления среды выполнения Kotlin:
NS_ASSUME_NONNULL_BEGIN
@interface KotlinBase : NSObject
- (instancetype)init __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
+ (void)initialize __attribute__((objc_requires_super));
@end;
@interface KotlinBase (KotlinBaseCopying) <NSCopying>
@end;
__attribute__((objc_runtime_name("KotlinMutableSet")))
__attribute__((swift_name("KotlinMutableSet")))
@interface DemoMutableSet<ObjectType> : NSMutableSet<ObjectType>
@end;
__attribute__((objc_runtime_name("KotlinMutableDictionary")))
__attribute__((swift_name("KotlinMutableDictionary")))
@interface DemoMutableDictionary<KeyType, ObjectType> : NSMutableDictionary<KeyType, ObjectType>
@end;
@interface NSError (NSErrorKotlinException)
@property (readonly) id _Nullable kotlinException;
@end;
Классы Kotlin имеют базовый класс KotlinBase в Objective-C, класс расширяет класс NSObject. Также существуют обёртки для коллекций и исключений. Большинство типов коллекций сопоставляются с аналогичными типами коллекций с другой стороны:
Kotlin |
Swift |
Objective-C |
|---|---|---|
List |
Array |
NSArray |
MutableList |
NSMutableArray |
NSMutableArray |
Set |
Set |
NSSet |
Map |
Dictionary |
NSDictionary |
MutableMap |
NSMutableDictionary |
NSMutableDictionary |
Числа Kotlin и NSNumber
Следующая часть <Framework>/Headers/Demo.h содержит сопоставления числовых типов между Kotlin/Native и NSNumber. Существует базовый класс, называемый DemoNumber в Objective-C и KotlinNumber в Swift. Он расширяет NSNumber. Также существуют дочерние классы для каждого числового типа Kotlin:
Kotlin |
Swift |
Objective-C |
Simple type |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Каждый числовой тип имеет метод класса для создания нового экземпляра из соответствующего простого типа. Кроме того, существует метод экземпляра для извлечения простого значения обратно. Схематически объявления выглядят так:
__attribute__((objc_runtime_name("Kotlin__TYPE__")))
__attribute__((swift_name("Kotlin__TYPE__")))
@interface Demo__TYPE__ : DemoNumber
- (instancetype)initWith__TYPE__:(__CTYPE__)value;
+ (instancetype)numberWith__TYPE__:(__CTYPE__)value;
@end;
Где __TYPE__ — одно из имен простых типов, а __CTYPE__ — соответствующий тип Objective-C, например, initWithChar(char).
Эти типы используются для отображения упакованных числовых типов Kotlin в Objective-C и Swift. В Swift вы можете просто вызвать конструктор для создания экземпляра, например, KotlinLong(value: 42).
Классы и объекты из Kotlin
Давайте посмотрим, как class и object сопоставляются с Objective-C и Swift. Сгенерированный файл <Framework>/Headers/Demo.h содержит точные определения для Class, Interface и Object:
NS_ASSUME_NONNULL_BEGIN
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Object")))
@interface DemoObject : KotlinBase
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
+ (instancetype)object __attribute__((swift_name("init()")));
@property (readonly) NSString *field;
@end;
__attribute__((swift_name("Interface")))
@protocol DemoInterface
@required
- (void)iMember __attribute__((swift_name("iMember()")));
@end;
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Clazz")))
@interface DemoClazz : KotlinBase <DemoInterface>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (DemoLong * _Nullable)memberP:(int32_t)p __attribute__((swift_name("member(p:)")));
@end;
Код полон атрибутов Objective-C, которые предназначены для того, чтобы помочь использовать фреймворк как из языков Objective-C, так и Swift. DemoClazz, DemoInterface и DemoObject создаются для Clazz, Interface и Object соответственно. Interface преобразуется в @protocol, как class, так и object представлены как @interface. Префикс Demo происходит от параметра -output компилятора kotlinc-native и имени фреймворка. Здесь вы можете увидеть, что возвращаемый тип с возможностью NULL ULong? преобразуется в DemoLong* в Objective-C.
Глобальные объявления из Kotlin
Все глобальные функции из Kotlin преобразуются в DemoLibKt в Objective-C и в LibKt в Swift, где Demo — это имя фреймворка, устанавливаемое параметром -output в kotlinc-native.
NS_ASSUME_NONNULL_BEGIN
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LibKt")))
@interface DemoLibKt : KotlinBase
+ (void)forIntegersB:(int8_t)b s:(int16_t)s i:(int32_t)i l:(DemoLong * _Nullable)l __attribute__((swift_name("forIntegers(b:s:i:l:)")));
+ (void)forFloatsF:(float)f d:(DemoDouble * _Nullable)d __attribute__((swift_name("forFloats(f:d:)")));
+ (NSString *)stringsStr:(NSString * _Nullable)str __attribute__((swift_name("strings(str:)")));
+ (NSString * _Nullable)acceptFunF:(NSString * _Nullable (^)(NSString *))f __attribute__((swift_name("acceptFun(f:)")));
+ (NSString * _Nullable (^)(NSString *))supplyFun __attribute__((swift_name("supplyFun()")));
@end;
Вы видите, что String Kotlin и NSString* Objective-C отображаются прозрачно. Аналогично, тип Unit из Kotlin сопоставляется с void. Мы видим, что примитивные типы отображаются напрямую. Примитивные типы без возможности NULL отображаются прозрачно. Примитивные типы с возможностью NULL отображаются в типы Kotlin<TYPE>*, как показано в таблице выше. Обе функции высшего порядка acceptFunF и supplyFun включены и принимают блоки Objective-C.
Дополнительную информацию о всех остальных деталях сопоставления типов можно найти в статье документации Objective-C Interop
Сбор мусора и подсчёт ссылок
Objective-C и Swift используют подсчёт ссылок. Kotlin/Native также имеет собственную сборку мусора. Сборка мусора Kotlin/Native интегрирована с подсчётом ссылок Objective-C/Swift. Вам не нужно использовать ничего особенного для управления временем жизни экземпляров Kotlin/Native из Swift или Objective-C.
Использование кода из Objective-C
Давайте вызовем фреймворк из Objective-C. Для этого создайте файл main.m со следующим содержимым:
#import <Foundation/Foundation.h>
#import <Demo/Demo.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
[[DemoObject object] field];
DemoClazz* clazz = [[ DemoClazz alloc] init];
[clazz memberP:42];
[DemoLibKt forIntegersB:1 s:1 i:3 l:[DemoULong numberWithUnsignedLongLong:4]];
[DemoLibKt forIntegersB:1 s:1 i:3 l:nil];
[DemoLibKt forFloatsF:2.71 d:[DemoDouble numberWithDouble:2.71]];
[DemoLibKt forFloatsF:2.71 d:nil];
NSString* ret = [DemoLibKt acceptFunF:^NSString * _Nullable(NSString * it) {
return [it stringByAppendingString:@" Kotlin is fun"];
}];
NSLog(@"%@", ret);
return 0;
}
}
Здесь вы вызываете классы Kotlin напрямую из кода Objective-C. Класс Kotlin object имеет функцию метода класса object, которая позволяет нам получить единственный экземпляр объекта и вызывать методы Object на нём. Широко распространённый шаблон используется для создания экземпляра класса Clazz. Вы вызываете [[ DemoClazz alloc] init] в Objective-C. Вы также можете использовать [DemoClazz new] для конструкторов без параметров. Глобальные объявления из исходников Kotlin находятся в области видимости класса DemoLibKt в Objective-C. Все методы преобразуются в методы класса этого класса. Функция strings преобразуется в функцию DemoLibKt.stringsStr в Objective-C, вы можете передать NSString напрямую ей. Возвращаемое значение также отображается как NSString.
Использование кода из Swift
Фреймворк, который вы скомпилировали с помощью Kotlin/Native, имеет вспомогательные атрибуты, чтобы упростить его использование с Swift. Преобразуйте предыдущий пример на Objective-C в Swift. В результате вы получите следующий код в main.swift:
import Foundation
import Demo
let kotlinObject = Object()
assert(kotlinObject === Object(), "Kotlin object has only one instance")
let field = Object().field
let clazz = Clazz()
clazz.member(p: 42)
LibKt.forIntegers(b: 1, s: 2, i: 3, l: 4)
LibKt.forFloats(f: 2.71, d: nil)
let ret = LibKt.acceptFun { "\($0) Kotlin is fun" }
if (ret != nil) {
print(ret!)
}
Код на Kotlin преобразуется в очень похожий код на Swift. Однако есть некоторые небольшие различия. В Kotlin любой object имеет только один экземпляр. Kotlin object Object теперь имеет конструктор в Swift, и мы используем синтаксис Object() для доступа к единственному экземпляру. Экземпляр всегда один и тот же в Swift, поэтому Object() === Object() является истинным. Имена методов и свойств переводятся без изменений. Kotlin String преобразуется в Swift String тоже. Swift также скрывает от нас NSNumber* упаковку. Мы также можем передать Swift замыкание в Kotlin и вызвать Kotlin лямбда-функцию из Swift.
Дополнительную документацию по отображению типов можно найти в статье Взаимодействие с Objective-C.
Xcode и зависимости фреймворка
Вам необходимо настроить проект Xcode для использования нашего фреймворка. Настройка зависит от целевой платформы.
Xcode для macOS
Во-первых, в вкладке Общие настроек цели, в разделе Связанные фреймворки и библиотеки, необходимо включить наш фреймворк. Это позволит Xcode просматривать наш фреймворк и разрешать импорты как из Objective-C, так и из Swift.
Второй шаг заключается в настройке пути поиска фреймворка сгенерированного двоичного файла. Он также известен как rpath или путь поиска во время выполнения. Двоичный файл использует путь для поиска необходимых фреймворков. Мы не рекомендуем устанавливать дополнительные фреймворки в ОС, если это не требуется. Вы должны понимать структуру вашего будущего приложения, например, у вас может быть папка Frameworks в папке приложения со всеми используемыми фреймворками. Параметр @rpath можно настроить в Xcode. Вам нужно открыть настройки проекта и найти раздел Пути поиска runpath. Здесь вы указываете относительный путь к скомпилированному фреймворку.
Xcode для iOS
Сначала необходимо включить скомпилированный фреймворк в проект Xcode. Для этого добавьте фреймворк в раздел Фреймворки, библиотеки и встроенное содержимое вкладки Общие настроек цели.
Затем второй шаг — включить путь к фреймворку в раздел Пути поиска фреймворков вкладки Параметры сборки настроек цели. Для упрощения настройки можно использовать макрос $(PROJECT_DIR).
Для симулятора iOS требуется фреймворк, скомпилированный для целевой платформы ios_x64, в нашем случае папка iOS_sim.
Эта ветка Stackoverflow содержит несколько дополнительных рекомендаций. Также может быть полезен менеджер пакетов CocoaPods для автоматизации процесса.
Следующие шаги
Kotlin/Native имеет двустороннее взаимодействие с языками Objective-C и Swift. Kotlin объекты интегрируются с Objective-C/Swift подсчетом ссылок. Неиспользуемые объекты Kotlin автоматически удаляются. Статья Взаимодействие с Objective-C содержит дополнительную информацию о подробностях реализации взаимодействия.
Kotlin/Native также поддерживает взаимодействие с C. Ознакомьтесь с руководством Kotlin/Native как динамической библиотеки для этого.
© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/apple-framework.html