Kotlin/Native взаимодействие
Введение
Kotlin/Native следует общей традиции Kotlin, обеспечивая отличную межплатформенную совместимость существующего программного обеспечения. В случае родной платформы наиболее важной целью межплатформенной совместимости является библиотека C. Таким образом, Kotlin/Native поставляется с инструментом cinterop, который можно использовать для быстрого создания всего необходимого для взаимодействия с внешней библиотекой.
При взаимодействии с родной библиотекой ожидается следующий рабочий процесс.
- создание файла
.def, описывающего, что включать в привязку - использование инструмента
cinteropдля создания привязок Kotlin - запуск компилятора Kotlin/Native для приложения для создания конечного исполняемого файла
Инструмент межплатформенной совместимости анализирует заголовки C и генерирует «естественное» отображение типов, функций и констант в мир Kotlin. Сгенерированные заглушки можно импортировать в IDE для целей автодополнения кода и навигации.
Межплатформенная совместимость со Swift/Objective-C также предоставляется и описана в отдельном документе OBJC_INTEROP.md.
Библиотеки платформы
Обратите внимание, что во многих случаях нет необходимости использовать механизмы создания пользовательских библиотек межплатформенной совместимости, описанные ниже, так как для доступных API на платформе можно использовать стандартизированные привязки, называемые библиотеками платформы. Например, POSIX на платформах Linux/macOS, Win32 на платформе Windows или фреймворки Apple на платформах macOS/iOS доступны таким образом.
Простой пример
Установите libgit2 и подготовьте заглушки для библиотеки git:
cd samples/gitchurn ../../dist/bin/cinterop -def src/main/c_interop/libgit2.def \ -compiler-option -I/usr/local/include -o libgit2
Скомпилируйте клиент:
../../dist/bin/kotlinc src/main/kotlin \ -library libgit2 -o GitChurn
Запустите клиент:
./GitChurn.kexe ../..
Создание привязок для новой библиотеки
Чтобы создать привязки для новой библиотеки, начните с создания файла .def. Структурно это простой текстовый файл, который выглядит следующим образом:
headers = png.h headerFilter = png.h package = png
Затем запустите инструмент cinterop с чем-то подобным (обратите внимание, что для библиотеки хоста, которая не включена в пути поиска sysroot, могут потребоваться заголовки):
cinterop -def png.def -compiler-option -I/usr/local/include -o png
Эта команда создаст скомпилированную библиотеку png.klib и директорию png-build/kotlin, содержащую исходный код Kotlin для библиотеки.
Если поведение для определенной платформы нужно изменить, можно использовать формат, подобный compilerOpts.osx или compilerOpts.linux, чтобы предоставить платформенно-специфичные значения для параметров.
Обратите внимание, что сгенерированные привязки, как правило, специфичны для платформы, поэтому, если вы разрабатываете для нескольких целей, привязки необходимо перегенерировать.
После генерации привязки их можно использовать в IDE как прокси-представление родной библиотеки.
Для типичной Unix-библиотеки с скриптом конфигурации compilerOpts, скорее всего, будет содержать вывод скрипта конфигурации с флагом --cflags (возможно, без точных путей).
Вывод скрипта конфигурации с --libs будет передан как значение флага -linkedArgs kotlinc (в кавычках) при компиляции.
Выбор заголовков библиотеки
При импорте заголовков библиотеки в программу C с директивой #include, все заголовки, включенные этими заголовками, также включаются в программу. Таким образом, все зависимости заголовков также включаются в сгенерированные заглушки.
Это поведение корректно, но может быть очень неудобно для некоторых библиотек. Поэтому можно указать в файле .def, какие из включенных заголовков следует импортировать. Также можно импортировать отдельные объявления из других заголовков в случае прямых зависимостей.
Фильтрация заголовков с помощью шаблонов
Можно фильтровать заголовки с помощью шаблонов. Значение свойства headerFilter из файла .def обрабатывается как список шаблонов, разделенных пробелами. Если включенный заголовок соответствует одному из шаблонов, то объявления из этого заголовка включаются в привязки.
Шаблоны применяются к путям заголовков относительно элементов пути включения, например, time.h или curl/curl.h. Поэтому, если библиотека обычно включается с #include <SomeLbrary/Header.h>, то, вероятно, будет правильно фильтровать заголовки с помощью
headerFilter = SomeLibrary/**
Если шаблон headerFilter не указан, то все заголовки включаются.
Фильтрация по картам модулей
Некоторые библиотеки имеют соответствующие файлы module.modulemap или module.map в своих заголовках. Например, системные библиотеки и фреймворки macOS и iOS. Файл карты модуля описывает соответствие между заголовочными файлами и модулями. Когда карты модулей доступны, заголовки из модулей, которые не включены непосредственно, могут быть отфильтрованы с использованием экспериментального параметра excludeDependentModules файла .def:
headers = OpenGL/gl.h OpenGL/glu.h GLUT/glut.h compilerOpts = -framework OpenGL -framework GLUT excludeDependentModules = true
Если одновременно используются excludeDependentModules и headerFilter, они применяются как пересечение.
Параметры компилятора и компоновщика C
Параметры, передаваемые компилятору C (используемые для анализа заголовков, таких как определения препроцессора) и компоновщику (используемые для компоновки конечных исполняемых файлов), могут быть переданы в файле определения как compilerOpts и linkerOpts соответственно. Например
compilerOpts = -DFOO=bar linkerOpts = -lpng
Также можно указать параметры, специфичные для целевой платформы, применимые только к определенной целевой платформе, например
compilerOpts = -DBAR=bar compilerOpts.linux_x64 = -DFOO=foo1 compilerOpts.mac_x64 = -DFOO=foo2
и так, заголовки C на Linux будут анализироваться с -DBAR=bar -DFOO=foo1, а на macOS с -DBAR=bar -DFOO=foo2. Обратите внимание, что любой параметр файла определения может иметь как общую, так и платформенно-специфическую часть.
Добавление пользовательских объявлений
Иногда требуется добавить пользовательские объявления C в библиотеку перед генерацией привязок (например, для макросов). Вместо создания дополнительного заголовочного файла с этими объявлениями, вы можете включить их непосредственно в конец файла .def после разделяющей строки, содержащей только последовательность разделителя ---:
headers = errno.h
---
static inline int getErrno() {
return errno;
}
Обратите внимание, что эта часть файла .def обрабатывается как часть заголовочного файла, поэтому функции с телом должны быть объявлены как static. Объявления анализируются после включения файлов из списка headers.
Включение статической библиотеки в ваш klib
Иногда удобнее поставлять статическую библиотеку с вашим продуктом, а не полагаться на ее наличие в среде пользователя. Для включения статической библиотеки в .klib используйте пункты staticLibrary и libraryPaths. Например:
headers = foo.h staticLibraries = libfoo.a libraryPaths = /opt/local/lib /usr/local/opt/curl/lib
При указании приведенного выше фрагмента инструмент cinterop будет искать libfoo.a в /opt/local/lib и /usr/local/opt/curl/lib, и если он найдет, включит бинарный файл библиотеки в klib.
При использовании такой klib в вашей программе библиотека подключается автоматически.
Использование привязок
Основные типы взаимодействия
Все поддерживаемые типы C имеют соответствующие представления в Kotlin:
- Знаковые, беззнаковые целочисленные и числа с плавающей точкой отображаются на свои аналоги Kotlin с той же шириной.
- Указатели и массивы отображаются на
CPointer<T>?. - Перечисления могут быть отображены либо как перечисления Kotlin, либо как целочисленные значения, в зависимости от эвристики и подсказок файла определения.
- Структуры/объединения отображаются на типы, имеющие поля, доступные через обозначение точки, т.е.
someStructInstance.field1. -
typedefпредставлены какtypealias.
Кроме того, любой тип C имеет тип Kotlin, представляющий lvalue этого типа, т.е. значение, расположенное в памяти, а не просто неизменное самодостаточное значение. Подумайте о ссылках C++, как о похожей концепции. Для структур (и typedef к структурам) это представление является основным и имеет то же имя, что и структура, для перечислений Kotlin оно называется ${type}Var, для CPointer<T> это CPointerVar<T>, а для большинства других типов это ${type}Var.
Для типов, имеющих оба представления, у представления "lvalue" есть свойство mutable .value для доступа к значению.
Типы указателей
Аргумент типа T для CPointer<T> должен быть одним из типов "lvalue", описанных выше, например, тип C struct S* отображается на CPointer<S>, int8_t* отображается на CPointer<int_8tVar>, а char** отображается на CPointer<CPointerVar<ByteVar>>.
Указатель C null представлен как null Kotlin, а тип указателя CPointer<T> не является nullable, но CPointer<T>? является. Значения этого типа поддерживают все операции Kotlin, связанные с обработкой null, например ?:, ?., !! и т.д.:
val path = getenv("PATH")?.toKString() ?: ""
Поскольку массивы также отображаются на CPointer<T>, он поддерживает оператор [] для доступа к значениям по индексу:
fun shift(ptr: CPointer<BytePtr>, length: Int) {
for (index in 0 .. length - 2) {
ptr[index] = ptr[index + 1]
}
}
Свойство .pointed для CPointer<T> возвращает lvalue типа T, указанного этим указателем. Обратная операция — это .ptr: она принимает lvalue и возвращает указатель на него.
void* сопоставлен с COpaquePointer — специальный тип указателя, который является супертипом для любого другого типа указателя. Таким образом, если C-функция принимает void*, то Kotlin-связывание принимает любой CPointer.
Приведение указателя (включая COpaquePointer) можно выполнить с помощью .reinterpret<T>, например:
val intPtr = bytePtr.reinterpret<IntVar>()
или
val intPtr: CPointer<IntVar> = bytePtr.reinterpret()
Как и в C, эти приведения типов с переинтерпретацией небезопасны и могут потенциально привести к скрытым проблемам с памятью в приложении.
Также доступны небезопасные приведения между CPointer<T>? и Long, предоставляемые методами расширения .toLong() и .toCPointer<T>():
val longValue = ptr.toLong() val originalPtr = longValue.toCPointer<T>()
Обратите внимание, что если тип результата известен из контекста, аргумент типа можно опустить, как обычно, из-за вывода типов.
Выделение памяти
Встроенная память может быть выделена с помощью интерфейса NativePlacement, например:
val byteVar = placement.alloc<ByteVar>()
или
val bytePtr = placement.allocArray<ByteVar>(5)
Наиболее «естественное» расположение — в объекте nativeHeap. Это соответствует выделению встроенной памяти с помощью malloc и предоставляет дополнительную операцию .free() для освобождения выделенной памяти:
val buffer = nativeHeap.allocArray<ByteVar>(size) <use buffer> nativeHeap.free(buffer)
Однако, срок жизни выделенной памяти часто связан со лексическим объёмом. Можно определить такой объём с помощью memScoped { ... }. Внутри фигурных скобок временное размещение доступно как неявный получатель, поэтому можно выделить встроенную память с помощью alloc и allocArray, и выделенная память будет автоматически освобождена после выхода за пределы области видимости.
Например, C-функция, возвращающая значения через параметры указателей, может быть использована следующим образом:
val fileSize = memScoped {
val statBuf = alloc<stat>()
val error = stat("/", statBuf.ptr)
statBuf.st_size
}
Передача указателей в связывания
Хотя C-указатели отображаются на тип CPointer<T>, параметры типа указателя C-функции отображаются на CValuesRef<T>. При передаче CPointer<T> в качестве значения такого параметра, он передаётся C-функции как есть. Однако вместо указателя может быть передана последовательность значений. В этом случае последовательность передаётся «по значению», т. е. C-функция получает указатель на временную копию этой последовательности, которая действительна только до возврата функции.
Представление параметров-указателей CValuesRef<T> предназначено для поддержки C-массивов-литералов без явного выделения памяти. Для создания неизменяемой самодостаточной последовательности C-значений предоставляются следующие методы:
-
${type}Array.toCValues(), гдеtype— тип Kotlin-примитива -
Array<CPointer<T>?>.toCValues(),List<CPointer<T>?>.toCValues() -
cValuesOf(vararg elements: ${type}), гдеtype— примитив или указатель
Например:
C:
void foo(int* elements, int count);
...
int elements[] = {1, 2, 3};
foo(elements, 3);
Kotlin:
foo(cValuesOf(1, 2, 3), 3)
Работа со строками
В отличие от других указателей, параметры типа const char* представлены как Kotlin-String. Поэтому можно передать любую Kotlin-строку в связывание, ожидающее C-строку.
Также доступны некоторые инструменты для ручного преобразования между Kotlin- и C-строками:
fun CPointer<ByteVar>.toKString(): String-
val String.cstr: CValuesRef<ByteVar>.Для получения указателя необходимо выделить
.cstrв памяти, например:val cString = kotlinString.cstr.getPointer(nativeHeap)
Во всех случаях C-строка должна быть закодирована в UTF-8.
Чтобы пропустить автоматическое преобразование и гарантировать, что в связываниях используются сырые указатели, в файле .def можно использовать инструкцию noStringConversion, например:
noStringConversion = LoadCursorA LoadCursorW
Таким образом, любое значение типа CPointer<ByteVar> можно передать в качестве аргумента типа const char*. Если необходимо передать Kotlin-строку, можно использовать код:
memScoped {
LoadCursorA(null, "cursor.bmp".cstr.ptr) // for ASCII version
LoadCursorW(null, "cursor.bmp".wcstr.ptr) // for Unicode version
}
Указатели с областью видимости
Возможна создание стабильного в рамках области видимости указателя представления C-инстанса CValues<T> с помощью свойства расширения CValues<T>.ptr, доступного в memScoped { ... }. Это позволяет использовать API, требующие C-указателей с ограниченным сроком жизни в определённой MemScope области видимости. Например:
memScoped {
items = arrayOfNulls<CPointer<ITEM>?>(6)
arrayOf("one", "two").forEachIndexed { index, value -> items[index] = value.cstr.ptr }
menu = new_menu("Menu".cstr.ptr, items.toCValues().ptr)
...
}
В этом примере все значения, передаваемые в C-API new_menu(), имеют срок жизни самого внутреннего memScope блока, к которому они относятся. После выхода потока управления за пределы области видимости memScoped C-указатели становятся недоступны.
Передача и получение структур по значению
Когда C-функция принимает или возвращает структуру/объединение T по значению, соответствующий тип аргумента или возвращаемого значения представлен как CValue<T>.
CValue<T> — это непрозрачный тип, поэтому к полям структуры нельзя получить доступ с соответствующими Kotlin-свойствами. Возможность доступа к полям структуры может быть реализована, если API использует структуры как обработчики, но если необходим доступ к полям, доступны следующие методы преобразования:
-
fun T.readValue(): CValue<T>. Преобразует (lvalue)TвCValue<T>. Таким образом, чтобы создатьCValue<T>, можно выделитьT, заполнить его и затем преобразовать вCValue<T>. -
CValue<T>.useContents(block: T.() -> R): R. Временно помещаетCValue<T>в память и затем выполняет переданный лямбда-выражение с помещенным значениемTв качестве получателя. Для чтения одного поля можно использовать следующий код:val fieldValue = structValue.useContents { field }
Обратные вызовы
Для преобразования Kotlin-функции в указатель на C-функцию можно использовать staticCFunction(::kotlinFunction). Он также может предоставить лямбда-выражение вместо ссылки на функцию. Функция или лямбда-выражение не должны захватывать какие-либо значения.
Если обратный вызов не выполняется в главном потоке, необходимо инициализировать среду выполнения Kotlin/Native, вызвав kotlin.native.initRuntimeIfNeeded().
Передача пользовательских данных обратным вызовам
Часто C-API позволяют передавать некоторые пользовательские данные обратным вызовам. Такие данные обычно предоставляются пользователем при настройке обратного вызова. Они передаются какой-либо C-функции (или записываются в структуру), например, как void*. Однако ссылки на Kotlin-объекты нельзя напрямую передавать в C. Поэтому они требуют обертывания перед настройкой обратного вызова и последующего распаковки в самом обратном вызове, чтобы безопасно перейти от Kotlin к Kotlin через мир C. Такое обертывание возможно с классом StableRef.
Для обертывания ссылки:
val stableRef = StableRef.create(kotlinReference) val voidPtr = stableRef.asCPointer()
где voidPtr — это COpaquePointer и может быть передана в C-функцию.
Для распаковки ссылки:
val stableRef = voidPtr.asStableRef<KotlinClass>() val kotlinReference = stableRef.get()
где kotlinReference — это оригинальная обернутая ссылка.
Созданный StableRef должен в конечном итоге быть вручную удалён с помощью метода .dispose(), чтобы избежать утечек памяти:
stableRef.dispose()
После этого он становится недоступным, поэтому voidPtr больше нельзя распаковать.
См. samples/libcurl для получения дополнительной информации.
Макросы
Каждый C-макрос, раскрывающийся до константы, представлен как свойство Kotlin. Другие макросы не поддерживаются. Однако их можно вручную экспонировать, обернув их поддерживаемыми объявлениями. Например, макрос типа функции FOO можно экспонировать как функцию foo, добавив дополнительное объявление в библиотеку:
headers = library/base.h
---
static inline int foo(int arg) {
return FOO(arg);
}
Подсказки файла определения
Файл .def поддерживает несколько вариантов настройки сгенерированных связываний.
-
Значение свойства
excludedFunctionsопределяет список разделённых пробелами имён функций, которые следует игнорировать. Это может потребоваться, потому что функция, объявленная в C-заголовке, как правило, не гарантируется реально вызываемой, и часто выяснить это автоматически сложно или невозможно. Этот параметр также можно использовать для обхода ошибки в самом механизме взаимодействия. -
Значения свойств
strictEnumsиnonStrictEnumsявляются разделёнными пробелами списками перечислений, которые должны быть сгенерированы как перечисление Kotlin или как целочисленные значения соответственно. Если перечисление не включено ни в один из этих списков, оно генерируется по умолчанию с помощью эвристик. -
Значение свойства
noStringConversion— это разделённые пробелами списки функций, параметрыconst char*которых не должны автоматически преобразовываться в Kotlin-строки.
Переносимость
Иногда C-библиотеки имеют параметры функций или поля структур платформенно-зависимого типа, например, long или size_t. Сам Kotlin не предоставляет ни неявных преобразований целых чисел, ни преобразований целых чисел в стиле C (например, (size_t) intValue), поэтому для упрощения написания портабельного кода в таких случаях предоставляется метод convert:
fun ${type1}.convert<${type2}>(): ${type2}
где каждый из type1 и type2 должен быть целочисленного типа, либо со знаком, либо без знака.
.convert<${type}> имеет ту же семантику, что и один из методов .toByte, .toShort, .toInt, .toLong, .toUByte, .toUShort, .toUInt или .toULong, в зависимости от type.
Пример использования convert:
fun zeroMemory(buffer: COpaquePointer, size: Int) {
memset(buffer, 0, size.convert<size_t>())
}
Кроме того, параметр типа может быть автоматически выведен, и поэтому может быть опущен в некоторых случаях.
Фиксация объектов
Объекты Kotlin могут быть зафиксированы, т.е. их позиция в памяти гарантируется стабильной до открепления, и указатели на внутренние данные таких объектов могут передаваться в функции C. Например
fun readData(fd: Int): String {
val buffer = ByteArray(1024)
buffer.usePinned { pinned ->
while (true) {
val length = recv(fd, pinned.addressOf(0), buffer.size.convert(), 0).toInt()
if (length <= 0) {
break
}
// Now `buffer` has raw data obtained from the `recv()` call.
}
}
}
Здесь мы используем служебную функцию usePinned, которая фиксирует объект, выполняет блок и открепляет его при нормальном завершении и при возникновении исключения.
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/native/c_interop.html