Spec-Zone.ru › Kotlin 1.8

Kotlin/Native в качестве динамической библиотеки – учебник

Узнайте, как использовать код Kotlin/Native из существующих приложений или библиотек нативных языках. Для этого необходимо скомпилировать код Kotlin в динамическую библиотеку, .so, .dylib, и .dll.

Kotlin/Native также имеет тесную интеграцию с технологиями Apple. Учебник Kotlin/Native как Apple Framework объясняет, как скомпилировать код 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.

Сначала создайте папку проекта. Все пути в этом руководстве будут относительными к этой папке. Иногда нужно создать отсутствующие каталоги перед добавлением новых файлов.

Используйте следующий файл сборки Gradle:

plugins {
    kotlin("multiplatform") version "1.8.0"
}

repositories {
    mavenCentral()
}

kotlin {
  linuxX64("native") { // on Linux 
  // macosX64("native") { // on x86_64 macOS
  // macosArm64("native") { // on Apple Silicon macOS
  // mingwX64("native") { // on Windows
    binaries {
      sharedLib {
        baseName = "native" // on Linux and macOS
        // baseName = "libnative" // on Windows
      }
    }
  }
}

tasks.wrapper {
  gradleVersion = "7.3"
  distributionType = Wrapper.DistributionType.ALL
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.8.0'
}

repositories {
    mavenCentral()
}

kotlin {
  linuxX64("native") { // on Linux
  // macosX64("native") { // on x86_64 macOS
  // macosArm64("native") { // on Apple Silicon macOS
  // mingwX64("native") { // on Windows
    binaries {
      sharedLib {
        baseName = "native" // on Linux and macOS
        // baseName = "libnative" // on Windows
      }
    }
  }
}

wrapper {
  gradleVersion = "7.3"
  distributionType = "ALL"
}

Переместите файлы исходного кода в папку src/nativeMain/kotlin в проекте. Это стандартный путь к исходному коду, когда используется плагин kotlin-multiplatform. Используйте следующий блок, чтобы настроить проект на генерацию динамической или общей библиотеки:

binaries {
  sharedLib {
    baseName = "native" // on Linux and macOS
    // baseName = "libnative" // on Windows
  }  
}

libnative используется как имя библиотеки, префикс имени сгенерированного заголовочного файла. Также он служит префиксом для всех объявлений в заголовочном файле.

Теперь вы можете открыть проект в IntelliJ IDEA, чтобы увидеть, как исправить пример проекта. При этом мы рассмотрим, как C-функции отображаются в объявлениях Kotlin/Native.

Запустите задачу сборки linkNative Gradle в IDE или вызовите следующую команду в консоли:

./gradlew 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.

END_OF_DOCUMENT_MARKER

Файл сгенерированных заголовков

В 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 для объявления структуры. Эта тема на Stackoverflow предоставляет больше объяснений этого шаблона.

Как видно из этих определений, объект 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.

Последнее изменение: 10 января 2023 г.
Библиотеки платформ Управление памятью 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/native-dynamic-libraries.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API