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.
Сначала создайте папку проекта. Все пути в этом учебнике будут относительными к этой папке. Иногда необходимо создать отсутствующие директории перед добавлением новых файлов.
Используйте следующий build.gradle(.kts) файл сборки Gradle:
plugins {
kotlin("multiplatform") version "1.7.20"
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
tasks.wrapper {
gradleVersion = "6.7.1"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.7.20'
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
wrapper {
gradleVersion = "6.7.1"
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 32 |
|
iOS ARM 64 |
|
iOS-симулятор (x86_64) |
|
Запустите задачу 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 |
Простой тип |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Каждый числовой тип имеет метод класса для создания нового экземпляра из соответствующего простого типа. Также существует метод экземпляра для извлечения простого значения обратно. Схематически объявления выглядят так:
__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 объекты автоматически удаляются. Дополнительную информацию об implementation details интерфейса можно найти в статье Объектно-ориентированный интерфейс с Objective-C. Конечно, можно импортировать существующий фреймворк и использовать его из Kotlin. Kotlin/Native поставляется с хорошим набором предварительно импортированных системных фреймворков.
Kotlin/Native также поддерживает интерфейс C. Ознакомьтесь с руководством Kotlin/Native в качестве динамической библиотеки для этого.
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/apple-framework.html