Spec-Zone.ru › Kotlin 1.9

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

Узнайте, как использовать код 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-interop, связанные с Kotlin/Native и многоплатформенными сборками с Gradle.

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

Используйте следующий build.gradle(.kts) файл Gradle:

plugins {
    kotlin("multiplatform") version "1.9.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.6"
  distributionType = Wrapper.DistributionType.ALL
}
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.9.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.6"
  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-фреймворк.

Последнее изменение: 21 октября 2022 г.
Библиотеки платформ Управление памятью в 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