Как вы можете знать, уже широко известный язык программирования Kotlin предоставляет возможность создания программ для различных платформ: JVM, JS и Native, последний из которых на сегодняшний день подвергается активному развитию. С релизом Kotlin 1.3 проект Kotlin Native вышел в стадию beta.
И как можно догадаться из названия, Kotlin Native позволяет писать и компилировать код на Kotlin под нативные платформы, например iOS или Linux, что предоставляет широкие возможности для создания мультиплатформенных проектов с единой кодовой базой.
Помимо компиляции Kotlin под конкретную платформу, имеется возможность использовать уже готовые нативные библиотеки, например, на языке C, что избавляет разработчиков от необходимости заново переписывать целые проекты.
В данной статье приводится пример простейшего проекта для Kotlin Native, в котором производится вызов функций из уже скомпилированной пользовательской динамической библиотеки, которая была написана на языке С.
Что используется:
- ОС Ubuntu 16.04
- IDE IntelliJ IDEA 2018.3.2 (Community Edition)
- Gradle плагин для сборки проекта kotlin-multiplatform 1.3.40
Очень важно, что статья актуальна для конкретной версии Gradle плагина для сборки проекта. Поскольку на момент написания статьи технология Kotlin Multiplatform всё ещё является экспериментальной, то в будущем вполне возможны изменения как в плагине для сборки, в самой технологии, так и непосредственно в языке Kotlin.
Пользовательская динамическая библиотека
Для данного примера я у себя нашел небольшую простую библиотеку, которая содержит единственную функцию для подсчета энтропии Реньи для массива вещественных чисел double. В каталоге с проектом (в моем случае: /home/tetraquark/dev/c/entropylib ) для данной библиотеки находятся файлы исходников, заголовочные файлы и уже скомпилированная динамическая библиотека libentropy.so. Содержание заголовочного файла очень простое:
1 2 3 4 5 6 7 8 |
#ifndef LIBENTROPY_H_ #define LIBENTROPY_H_ #include "entropylib.h" double renyi_entropy(double alpha, double *dataVector, int vectorLength, int start_index); #endif /* LIBENTROPY_H_ */ |
Теперь необходимо создать проект Kotlin Native, в котором будет использоваться нативная библиотека.
Проект Kotlin Native
При создании проекта в IDEA выбираем раздел Kotlin и тип проекта Kotlin/Native.
Умная IDE сама создаст нужные файлы и каталоги. Но для успешного подключения сторонней нативной библиотеки необходимо самостоятельно подкорректировать скрипт build.gradle для используемой системы сборки Gradle. Ниже приведен полныйкод скрипта build.gradle, который у меня получился:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// Плагин сборки нужной версии plugins { id 'kotlin-multiplatform' version '1.3.40' } repositories { mavenCentral() } kotlin { // Цель сборки для Kotlin Native linuxX64("linux") { binaries { // Параметры для итогового бинарника executable { entryPoint = 'sample.main' // Указание линковщику необходимых параметров linkerOpts = ['-lentropy', '-L/home/tetraquark/dev/c/entropylib'] } } compilations.main { // Параметры для cinterop cinterops { // Указывается название сторонней библиотеки (может быть любое) entropylib { // Параметры для компилятора compilerOpts '-I/home/tetraquark/dev/c/entropylib' } } } } sourceSets { linuxMain { // Внешние зависимости проекта (пусто) } linuxTest { // Внешние зависимости проекта (пусто) } } } // Сгенерированная Gradle задача IDEA для запуска программы, не так важно task runProgram { def buildType = 'release' dependsOn "link${buildType.capitalize()}ExecutableLinux" doLast { def programFile = kotlin.targets.linux.compilations.main.getBinary('EXECUTABLE', buildType) exec { executable programFile args '' } } } |
После редактирования скрипта необходимо выполнить синхронизацию проекта. Внутри блока cinterops последовательно перечисляются все подключаемые нативные библиотеки. В данном случае используется единственная динамическая библиотека, поэтому присутствует единственный блок с названием entropylib. Внутри данного блока указываются различные параметры, например, опции для компилятора с указанием каталога для заголовочных файлов. Всё указывается как для gcc.
Далее для успешной генерации всех нужных файлов для cinterop необходимо выполнить следующее:
1) В каталоге src необходимо создать каталог nativeInterop;
2) В только что созданном каталоге nativeInterop, необходимо создать еще один каталог cinterop;
3) В каталоге nativeInterop/cinterop создаем текстовый файл entropylib.def (имена .def файлов должна совпадать с названиями блоков, которые описаны внутри cinterops в build.gradle);
4) В данном файле описывается параметры для подключаемой конкретной библиотеки, указываются необходимые заголовочные файлы, по которым будут создаваться Kotlin обертки, которые уже используются в Kotlin проекте. В данном случае содержание файла очень простое (указывается единственный заголовочный файл библиотеки):
1 |
headers = libentropy.h |
В итоге, в нашем случае структура проекта выглядит следующим образом:
Теперь можно вызывать cinterop. Для этого воспользуемся IDEA, открыв окно задач Gradle. И в разделе задач Tasks выполняем задачу interop/cinteropEntropylibLinux (в случае других названий блоков в cinterops в build.gradle, название задачи interop будет слегка отличаться).
Либо запускаем задачу через gradlew в терминале:
1 |
$ ./gradlew cinteropEntropylibLinux |
В результате успешной работы cinterop, будет создана библиотека .klib и Kotlin обертки над подключаемой нативной библиотекой. Можно открыть и посмотреть, как выглядит эта обертка, которую можно найти примерно тут:
Теперь можно начать писать код для Kotlin проекта с использованием динамической библиотеки. IDE при создании проекта любезно автоматически создала файл src/linuxMain/SampleLinux.kt, в котором находится функция main. Там я и написал следующие строки кода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package sample import kotlinx.cinterop.cValuesOf import kotlinx.cinterop.memScoped fun main(args: Array<String>) { println("Hello from Kotlin Native!") memScoped { // Создаем массив вещественных чисел и получаем указатель на него val dataArray = cValuesOf(0.5, 0.2, 1.0) // Вызываем функцию из библиотеки entropylib val entropy = entropylib.renyi_entropy(2.0, dataArray, dataArray.size, 0) println("Entropy = $entropy") } } |
Сборка и запуск проекта
Осталось собрать получившийся проект и запустить программу. Я проводил сборку с помощью Gradle. Сборку можно выполнить, запустив Gradle задачу build:
Либо запускаем задачу через gradlew в терминале:
1 |
$ ./gradlew build |
В случае операционной системы Linux после успешной компиляции в терминале необходимо добавить к переменной среды LD_LIBRARY_PATH полный путь до файла динамической библиотеки, в моем случае это выглядит следующим образом:
1 |
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/tetraquark/dev/c/entropylib |
Всё готово, можно проверять. Результаты компиляции (исполняемые файлы с расширением .kexe) находятся в проекте Kotlin Native в каталоге build/bin. В моем случае в каталоге build/bin/linux. Я запускал через терминал так:
1 |
$ ./build/bin/linux/debugExecutable/tetraquark_shared_lib.kexe |
Результаты работы:
Полезные ссылки:
- Статья посвященная работе с cinterop на официальном сайте: https://kotlinlang.org/docs/reference/native/c_interop.html
- Инструкция по сборке мультиплатформенного проекта на официальном сайте: https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html
- Много примеров для проектов Kotlin Multiplatform и Kotlin Native на Github: https://github.com/JetBrains/kotlin-native/tree/master/samples
Интересно, нужно попробовать. Если все настолько просто, то это очень круто. В Python создание обертки для динамической библиотеки — это прямо боль (или я еще не научился это правильно делать))