Spec-Zone.ru › Kotlin 1.4

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

Последнее обновление 15 апреля 2019
Компилирование кода 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, связанные с 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

Spec-Zone.ru

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