Kotlin/Native в качестве динамической библиотеки
| Последнее обновление | 15 апреля 2019 |
В этом учебнике мы рассмотрим, как использовать код Kotlin/Native из существующих приложений или библиотек нативных языках. Для этого нам нужно скомпилировать наш код Kotlin в динамическую библиотеку, .so, .dylib, и .dll.
Kotlin/Native также имеет тесную интеграцию с технологиями Apple. В учебнике Kotlin/Native как фреймворк Apple рассказывается, как скомпилировать код Kotlin в фреймворк для Swift и Objective-C.
В этом учебнике мы будем:
- Компилировать код Kotlin в динамическую библиотеку
- Рассматривать сгенерированные заголовочные файлы C
- Использовать динамическую библиотеку Kotlin из C
- Компилировать и запускать пример на Linux и macOS и Windows
Создание библиотеки Kotlin
Компилятор Kotlin/Native может создать динамическую библиотеку из кода Kotlin. Динамическая библиотека часто поставляется с заголовочным файлом, файлом .h, который мы будем использовать для вызова скомпилированного кода из C.
Лучший способ понять эти техники — попробовать их на практике. Давайте создадим первую крошечную библиотеку Kotlin и используем её из программы на C.
Мы можем начать с создания файла библиотеки на Kotlin и сохранить его как hello.kt:
package example
object Object {
val field = "A"
}
class Clazz {
fun memberFunction(p: Int): ULong = 42UL
}
fun forIntegers(b: Byte, s: Short, i: UInt, l: Long) { }
fun forFloats(f: Float, d: Double) { }
fun strings(str: String) : String? {
return "That is '$str' from C"
}
val globalString = "A global String"
Хотя можно использовать командную строку, как напрямую, так и комбинируя её со скриптовым файлом (например, 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 build.gradle.kts файл сборки Gradle со следующим содержимым:
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
linuxX64("native") {
binaries {
sharedLib {
baseName = "native"
}
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
sharedLib {
baseName = "native"
}
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.21'
}
repositories {
mavenCentral()
}
kotlin {
mingwX64("native") {
binaries {
sharedLib {
baseName = "libnative"
}
}
}
}
wrapper {
gradleVersion = "5.3.1"
distributionType = "ALL"
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
linuxX64("native") {
binaries {
sharedLib {
baseName = "native"
}
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
macosX64("native") {
binaries {
sharedLib {
baseName = "native"
}
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
plugins {
kotlin("multiplatform") version "1.3.21"
}
repositories {
mavenCentral()
}
kotlin {
mingwX64("native") {
binaries {
sharedLib {
baseName = "libnative"
}
}
}
}
tasks.withType<Wrapper> {
gradleVersion = "5.3.1"
distributionType = Wrapper.DistributionType.ALL
}
Подготовленные файлы проекта можно загрузить непосредственно с GitHub. GitHub. GitHub. GitHub. GitHub. GitHub.
Переместим файлы исходного кода в папку src/nativeMain/kotlin в проекте. Это стандартный путь для расположения исходных файлов при использовании плагина kotlin-multiplatform. Мы используем следующий блок, чтобы указать и настроить проект на генерацию для нас динамической или общей библиотеки:
binaries {
sharedLib {
baseName = "native"
}
}
binaries {
sharedLib {
baseName = "native"
}
}
binaries {
sharedLib {
baseName = "libnative"
}
}
libnative используется в качестве имени библиотеки, префикса имени генерируемого заголовочного файла. Он также префиксует все объявления в заголовочном файле.
Теперь мы готовы открыть проект в IntelliJ IDEA и посмотреть, как исправить примерный проект. При этом мы рассмотрим, как функции C отображаются в объявлениях Kotlin/Native.
Давайте запустим задачу сборки linkNative Gradle в IDE или вызвав следующую командную строку:
./gradlew linkNative
./gradlew linkNative
gradlew.bat linkNative
Сборка генерирует следующие файлы в папке build/bin/native/debugShared в зависимости от ОС:
- macOS:
libnative_api.hиlibnative.dylib - Linux:
libnative_api.hиlibnative.so - Windows:
libnative_api.h,libnative_symbols.defиlibnative.dll
Те же правила используются компилятором Kotlin/Native для генерации файла .h для всех платформ.
Давайте рассмотрим C API нашей библиотеки Kotlin.
Сгенерированный заголовочный файл
В файле libnative_api.h, мы найдем следующий код. Мы рассмотрим код по частям, чтобы облегчить понимание.
Обратите внимание, что способ экспорта Kotlin/Native символов может изменяться без предварительного уведомления.
Самая первая часть содержит стандартные заголовки и подписи C/C++:
#ifndef KONAN_DEMO_H
#define KONAN_DEMO_H
#ifdef __cplusplus
extern "C" {
#endif
/// THE REST OF THE GENERATED CODE GOES HERE
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* KONAN_DEMO_H */
После ритуалов в libnative_api.h, у нас есть блок с общими определениями типов:
#ifdef __cplusplus typedef bool libnative_KBoolean; #else typedef _Bool libnative_KBoolean; #endif typedef unsigned short libnative_KChar; typedef signed char libnative_KByte; typedef short libnative_KShort; typedef int libnative_KInt; typedef long long libnative_KLong; typedef unsigned char libnative_KUByte; typedef unsigned short libnative_KUShort; typedef unsigned int libnative_KUInt; typedef unsigned long long libnative_KULong; typedef float libnative_KFloat; typedef double libnative_KDouble; typedef void* libnative_KNativePtr;
Kotlin использует префикс libnative_ для всех объявлений в созданном файле libnative_api.h. Давайте представим отображение типов более понятным способом:
| Kotlin Определение | Тип C |
|---|---|
libnative_KBoolean |
bool или _Bool
|
libnative_KChar | unsigned short |
libnative_KByte | signed char |
libnative_KShort | short |
libnative_KInt | int |
libnative_KLong | long long |
libnative_KUByte | unsigned char |
libnative_KUShort | unsigned short |
libnative_KUInt | unsigned int |
libnative_KULong | unsigned long long |
libnative_KFloat | float |
libnative_KDouble | double |
libnative_KNativePtr | void* |
Раздел определений показывает, как базовые типы Kotlin сопоставляются с базовыми типами C. Мы обсуждали обратное отображение в учебнике Преобразование примитивных типов данных из C.
Следующая часть файла libnative_api.h содержит определения типов, используемых в библиотеке:
struct libnative_KType;
typedef struct libnative_KType libnative_KType;
typedef struct {
libnative_KNativePtr pinned;
} libnative_kref_example_Object;
typedef struct {
libnative_KNativePtr pinned;
} libnative_kref_example_Clazz;
Синтаксис typedef struct { .. } TYPE_NAME используется в языке C для объявления структуры. Эта тема предоставляет больше объяснений этого шаблона.
Из этих определений видно, что объект Kotlin Object сопоставляется с libnative_kref_example_Object, а Clazz сопоставляется с libnative_kref_example_Clazz. Обе структуры содержат только поле pinned со указателем, тип поля libnative_KNativePtr определен как void* выше.
В C нет поддержки пространств имен, поэтому компилятор Kotlin/Native генерирует длинные имена, чтобы избежать возможных конфликтов с другими символами в существующем нативном проекте.
Значительная часть определений находится в файле libnative_api.h. Он включает определение мира нашей библиотеки Kotlin/Native:
typedef struct {
/* Service functions. */
void (*DisposeStablePointer)(libnative_KNativePtr ptr);
void (*DisposeString)(const char* string);
libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type);
/* User functions. */
struct {
struct {
struct {
void (*forIntegers)(libnative_KByte b, libnative_KShort s, libnative_KUInt i, libnative_KLong l);
void (*forFloats)(libnative_KFloat f, libnative_KDouble d);
const char* (*strings)(const char* str);
const char* (*get_globalString)();
struct {
libnative_KType* (*_type)(void);
libnative_kref_example_Object (*_instance)();
const char* (*get_field)(libnative_kref_example_Object thiz);
} Object;
struct {
libnative_KType* (*_type)(void);
libnative_kref_example_Clazz (*Clazz)();
libnative_KULong (*memberFunction)(libnative_kref_example_Clazz thiz, libnative_KInt p);
} Clazz;
} example;
} root;
} kotlin;
} libnative_ExportedSymbols;
Код использует объявления анонимных структур. Код struct { .. } foo объявляет поле во внешней структуре анонимного типа структуры, типа без имени.
В C также нет поддержки объектов. Люди используют указатели на функции для имитации семантики объектов. Указатель на функцию объявляется следующим образом RETURN_TYPE (* FIELD_NAME)(PARAMETERS). Это сложно читать, но мы должны иметь возможность увидеть поля указателей на функции в структурах выше.
Функции выполнения
Код читается следующим образом. У нас есть структура libnative_ExportedSymbols, которая определяет все функции, которые Kotlin/Native и наша библиотека предоставляют нам. Она активно использует вложенные анонимные структуры для имитации пакетов. Префикс libnative_ происходит от имени библиотеки.
Структура libnative_ExportedSymbols содержит несколько вспомогательных функций:
void (*DisposeStablePointer)(libnative_KNativePtr ptr); void (*DisposeString)(const char* string); libnative_KBoolean (*IsInstance)(libnative_KNativePtr ref, const libnative_KType* type);
Эти функции работают с объектами Kotlin/Native. Вызовите DisposeStablePointer для освобождения объекта Kotlin и DisposeString для освобождения строки Kotlin, которая имеет тип char* в C. Возможно использовать функцию IsInstance для проверки, является ли тип Kotlin или libnative_KNativePtr экземпляром другого типа. Фактический набор операций зависит от фактического использования.
В Kotlin/Native есть сборка мусора, но она не помогает нам работать с объектами Kotlin из языка C. Kotlin/Native имеет взаимодействие с Objective-C и Swift и интегрируется с их счетчиками ссылок. Статья документации Взаимодействие с Objective-C содержит более подробную информацию об этом. Также есть учебник Kotlin/Native как фреймворк Apple.
Функции нашей библиотеки
Давайте посмотрим на поле kotlin.root.example, оно имитирует структуру пакетов нашего кода Kotlin с префиксом kotlin.root..
Есть поле kotlin.root.example.Clazz, которое представляет собой Clazz из Kotlin. Clazz#memberFunction доступно с помощью поля memberFunction. Единственное различие заключается в том, что memberFunction принимает ссылку this в качестве первого параметра. Язык C не поддерживает объекты, и поэтому необходимо явно передавать указатель на this.
В поле Clazz (также известном как kotlin.root.example.Clazz.Clazz) есть конструктор, который является конструкторской функцией для создания экземпляра Clazz.
Kotlin object Object доступно как kotlin.root.example.Object. Есть функция _instance для получения единственного экземпляра объекта.
Свойства преобразуются в функции. Префикс get_ и set_ используется для именования функций-геттера и -сеттера соответственно. Например, только для чтения свойство globalString из Kotlin преобразуется в функцию get_globalString в C.
Глобальные функции forInts, forFloats, или strings преобразуются в указатели на функции в анонимной структуре kotlin.root.example.
Точка входа
Мы можем увидеть, как создается API. Для начала нам нужно инициализировать структуру libnative_ExportedSymbols. Давайте посмотрим на последнюю часть libnative_api.h для этого:
extern libnative_ExportedSymbols* libnative_symbols(void);
Функция libnative_symbols позволяет открыть путь из нативного кода к библиотеке Kotlin/Native. Это точка входа, которую мы используем. Имя библиотеки используется в качестве префикса для имени функции.
Обратите внимание, что ссылки на объекты Kotlin/Native не поддерживают многопоточный доступ. Возможно, потребуется размещение возвращаемого указателя libnative_ExportedSymbols* на поток.
Использование сгенерированных заголовков из C
Использование из C простое и несложное. Мы создаем файл main.c со следующим кодом:
#include "libnative_api.h"
#include "stdio.h"
int main(int argc, char** argv) {
//obtain reference for calling Kotlin/Native functions
libnative_ExportedSymbols* lib = libnative_symbols();
lib->kotlin.root.example.forIntegers(1, 2, 3, 4);
lib->kotlin.root.example.forFloats(1.0f, 2.0);
//use C and Kotlin/Native strings
const char* str = "Hello from Native!";
const char* response = lib->kotlin.root.example.strings(str);
printf("in: %s\nout:%s\n", str, response);
lib->DisposeString(response);
//create Kotlin object instance
libnative_kref_example_Clazz newInstance = lib->kotlin.root.example.Clazz.Clazz();
long x = lib->kotlin.root.example.Clazz.memberFunction(newInstance, 42);
lib->DisposeStablePointer(newInstance.pinned);
printf("DemoClazz returned %ld\n", x);
return 0;
}
Компиляция и запуск примера на Linux и macOS
На macOS 10.13 с Xcode мы компилируем C-код и связываем его с динамической библиотекой с помощью следующей команды:
clang main.c libnative.dylib
На Linux мы вызываем аналогичную команду:
gcc main.c libnative.so
Компилятор генерирует исполняемый файл a.out. Нам нужно запустить его, чтобы увидеть выполнение кода Kotlin из C-библиотеки. На Linux нам нужно включить . в LD_LIBRARY_PATH , чтобы приложение знало, как загрузить библиотеку libnative.so из текущей папки.
Компиляция и запуск примера на Windows
Для начала нам потребуется установленный компилятор Microsoft Visual C++ с поддержкой целевого x64_64. Самый простой способ сделать это — установить версию Microsoft Visual Studio на компьютере с Windows.
Мы будем использовать консоль x64 Native Tools Command Prompt <VERSION>. Вы найдете ярлык для открытия консоли в меню Пуск. Она входит в комплект пакета Microsoft Visual Studio.
В Windows динамические библиотеки включаются либо через сгенерированную статическую библиотеку-обертку, либо с помощью ручного кода, который работает с функциями LoadLibrary или аналогичными функциями Win32API. Мы будем следовать первому варианту и сгенерируем собственную статическую библиотеку-обертку для libnative.dll.
Мы вызываем lib.exe из инструментальной цепочки, чтобы сгенерировать статическую библиотеку-обертку libnative.lib, которая автоматизирует использование DLL из кода:
lib /def:libnative_symbols.def /out:libnative.lib
Теперь мы готовы скомпилировать наш main.c в исполняемый файл. Мы включаем сгенерированную libnative.lib в команду сборки и запускаем:
cl.exe main.c libnative.lib
Команда создаёт файл main.exe, который можно запустить.
Дальнейшие шаги
Динамические библиотеки являются основным способом использования кода Kotlin из существующих программ. Мы можем использовать их для совместного использования нашего кода на многих платформах или языках, включая JVM, Python, iOS, Android и другие.
Kotlin/Native также имеет тесную интеграцию с Objective-C и Swift. Это рассматривается в руководстве Kotlin/Native как фреймворк Apple.
© 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/dynamic-libraries.html