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.6.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.6.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 |
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. Вам нужно открыть конфигурацию проекта и найти раздел Пути поиска Rpath. Здесь вы указываете относительный путь к скомпилированному фреймворку.
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. 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