Kotlin/Native как фреймворк Apple
| Последнее обновление | 15 апреля 2019 |
Kotlin/Native обеспечивает двунаправленную совместимость с Objective-C/Swift. Фреймворки и библиотеки Objective-C могут использоваться в коде Kotlin. Модули Kotlin могут использоваться в коде Swift/Objective-C. Кроме того, Kotlin/Native имеет C Interop. Также есть учебник Kotlin/Native как динамическая библиотека для получения дополнительной информации.
В этом руководстве мы рассмотрим, как использовать код Kotlin/Native из приложений Objective-C и Swift на macOS и iOS. Мы создадим фреймворк из кода Kotlin.
В этом руководстве мы:
- создадим библиотеку Kotlin и скомпилируем её в фреймворк
- рассмотрим сгенерированный код Objective-C и Swift API
- используем фреймворк из 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 через плагин Gradle с плагином kotlin-multiplatform.
Мы рассмотрели основы настройки проекта, совместимого с IDE, с Gradle в руководстве Простой пример приложения Kotlin/Native. Пожалуйста, ознакомьтесь с ним, если вам нужны подробные начальные шаги и инструкции о том, как начать новый проект Kotlin/Native и открыть его в IntelliJ IDEA. В этом руководстве мы рассмотрим расширенные случаи использования C interop Kotlin/Native и многоплатформенные сборки с Gradle.
Сначала создадим папку проекта. Все пути в этом руководстве будут относительными к этой папке. Иногда необходимо создать недостающие каталоги перед добавлением новых файлов.
Мы будем использовать следующий файл build.gradle build.gradle.kts Gradle с содержанием:
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
Подготовленные исходные файлы можно загрузить напрямую с GitHub. GitHub.
Переместим файлы исходников в папку src/nativeMain/kotlin в проекте. Это стандартный путь, где располагаются исходные файлы, когда используется плагин kotlin-multiplatform. Мы используем следующий блок для настройки проекта на генерацию динамической или общей библиотеки:
binaries {
framework {
baseName = "Demo"
}
}
Наряду с macOS X64, Kotlin/Native поддерживает iOS arm32, arm64 и X64 целевые платформы. Мы можем заменить macosX64 соответствующими функциями, как показано в таблице:
| Целевая платформа/устройство | Функция Gradle |
|---|---|
| macOS x86_64 | macosX64() |
| iOS ARM 32 | iosArm32() |
| iOS ARM 64 | iosArm64() |
| iOS симулятор (x86_64) | iosX64() |
Запустим задачу Gradle linkNative для сборки библиотеки в IDE или вызвав следующую команду в консоли:
./gradlew linkNative
./gradlew linkNative
gradlew.bat linkNative
В зависимости от варианта, сборка генерирует фреймворк в папки build/bin/native/debugFramework и build/bin/native/releaseFramework. Давайте посмотрим, что внутри
Сгенерированные заголовки фреймворка
Каждый созданный фреймворк содержит файл заголовков в <Framework>/Headers/Demo.h. Заголовки не зависят от целевой платформы (по крайней мере, с Kotlin/Native v.0.9.2). Он содержит определения для нашего кода Kotlin и несколько общих объявлений Kotlin.
Обратите внимание, что способ экспорта символов Kotlin/Native может быть изменён без предварительного уведомления.
Объявления времени выполнения 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 | Простой тип |
|---|---|---|---|
- |
KotlinNumber |
<Package>Number |
- |
Byte |
KotlinByte |
<Package>Byte |
char |
UByte |
KotlinUByte |
<Package>UByte |
unsigned char |
Short |
KotlinShort |
<Package>Short |
short |
UShort |
KotlinUShort |
<Package>UShort |
unsigned short |
Int |
KotlinInt |
<Package>Int |
int |
UInt |
KotlinUInt |
<Package>UInt |
unsigned int |
Long |
KotlinLong |
<Package>Long |
long long |
ULong |
KotlinULong |
<Package>ULong |
unsigned long long |
Float |
KotlinFloat |
<Package>Float |
float |
Double |
KotlinDouble |
<Package>Double |
double |
Boolean |
KotlinBoolean |
<Package>Boolean |
BOOL/Bool |
Каждый тип числа имеет метод класса для создания нового экземпляра из соответствующего простого типа. Также есть метод экземпляра для извлечения простого значения обратно. Схематически объявления выглядят так:
__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;
Мы видим, что Kotlin String и Objective-C NSString* сопоставляются прозрачно. Аналогично, тип Unit из Kotlin сопоставляется с void. Мы видим, что примитивные типы сопоставляются напрямую. Необязательные примитивные типы сопоставляются прозрачно. Обязательные примитивные типы сопоставляются в типы 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 Interop
Xcode и зависимости фреймворка
Для использования нашего фреймворка необходимо настроить проект Xcode. Конфигурация зависит от целевой платформы.
Xcode для macOS-цели
Во-первых, нам нужно включить фреймворк в раздел General конфигурации цели. Есть раздел Linked Frameworks and Libraries для включения нашего фреймворка. Это позволит Xcode просмотреть наш фреймворк и разрешить импорты как из Objective-C, так и из Swift.
Второй шаг - настроить путь поиска фреймворка для сгенерированного двоичного файла. Он также известен как rpath или путь поиска во время выполнения. Двоичный файл использует путь для поиска необходимых фреймворков. Не рекомендуется устанавливать дополнительные фреймворки в ОС, если это не требуется. Мы должны понимать структуру нашего будущего приложения, например, у нас может быть папка Frameworks внутри пакета приложения со всеми используемыми фреймворками. Параметр @rpath можно настроить в Xcode. Для этого нужно открыть конфигурацию проекта и найти раздел Runpath Search Paths. Здесь мы указываем относительный путь к скомпилированному фреймворку.
Xcode для iOS-целей
Во-первых, нам нужно добавить скомпилированный фреймворк в проект Xcode. Для этого мы добавляем фреймворк в блок Embedded Binaries раздела General конфигурации цели.
Второй шаг — включить путь к фреймворку в блок Framework Search Paths раздела Build Settings страницы конфигурации target. Для упрощения настройки можно использовать макрос $(PROJECT_DIR).
Симулятору iOS требуется фреймворк, скомпилированный для ios_x64 target, папка iOS_sim в нашем случае.
В обсуждении на Stack Overflow содержатся дополнительные рекомендации. Также для автоматизации процесса может быть полезен менеджер пакетов CocoaPods.
Следующие шаги
Kotlin/Native обеспечивает двустороннее взаимодействие с языками Objective-C и Swift. Kotlin-объекты интегрируются с Objective-C/Swift механизмом подсчёта ссылок. Неиспользуемые Kotlin-объекты автоматически удаляются. Статья Objective-C Interop содержит более подробную информацию об особенностях реализации взаимодействия.
Kotlin/Native поддерживает также взаимодействие с C. Для этого ознакомьтесь с руководством Kotlin/Native как динамическая библиотека, или прочитайте статью C Interop в документации.
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/tutorials/native/apple-framework.html