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.8.0"
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
tasks.wrapper {
gradleVersion = "7.3"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.8.0'
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
framework {
baseName = "Demo"
}
}
}
}
wrapper {
gradleVersion = "7.3"
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. Для этого необходимо открыть конфигурацию проекта и найти раздел Пути поиска 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. Kotlin/Native поставляется с хорошим набором предварительно импортированных системных фреймворков.
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