Взаимодействие с C
Kotlin/Native следует общей традиции Kotlin, обеспечивая отличную межплатформенную совместимость существующего программного обеспечения. В случае с нативной платформой наиболее важной целью межплатформенной совместимости является библиотека C. Поэтому Kotlin/Native поставляется с инструментом cinterop, который можно использовать для быстрого создания всего необходимого для взаимодействия с внешней библиотекой.
Следующий рабочий процесс ожидается при взаимодействии с нативной библиотекой:
Создайте файл
.def, описывающий, что следует включить в привязки.Используйте инструмент
cinterop, чтобы сгенерировать привязки Kotlin.Запустите компилятор Kotlin/Native на приложении, чтобы получить конечный исполняемый файл.
Инструмент межплатформенной совместимости анализирует заголовки C и создает «естественное» отображение типов, функций и констант в мир Kotlin. Сгенерированные заглушки можно импортировать в IDE для автодополнения кода и навигации.
Межплатформенная совместимость с Swift/Objective-C также предоставляется и описана в разделе по совместимости с Objective-C.
Платформенные библиотеки
Обратите внимание, что во многих случаях нет необходимости использовать механизмы создания пользовательских библиотек межплатформенной совместимости, описанные ниже, так как для доступных на платформе API можно использовать стандартизированные привязки, называемые платформенными библиотеками. Например, POSIX на платформах Linux/macOS, Win32 на платформе Windows или фреймворки Apple на macOS/iOS доступны таким образом.
Простой пример
Установите libgit2 и подготовьте заглушки для библиотеки git:
cd samples/gitchurn ../../dist/bin/cinterop -def src/nativeInterop/cinterop/libgit2.def \ -compiler-option -I/usr/local/include -o libgit2
Компилируем клиент:
../../dist/bin/kotlinc src/gitChurnMain/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, какие из включенных заголовков следует импортировать. Отдельные объявления из других заголовков также можно импортировать в случае прямых зависимостей.
Фильтрация заголовков по шаблонам
Возможно фильтровать заголовки по шаблонам с использованием свойств фильтра из файла .def. Они обрабатываются как список шаблонов, разделенных пробелом.
-
Для включения объявлений из заголовков используйте свойство
headerFilter. Если включенный заголовок соответствует одному из шаблонов, объявления включаются в привязки.Шаблоны применяются к путям заголовков относительно соответствующих элементов путей включения, например,
time.hилиcurl/curl.h. Поэтому, если библиотека обычно включается с#include <SomeLibrary/Header.h>, вероятно, будет правильно отфильтровать заголовки с помощью следующего фильтра:headerFilter = SomeLibrary/**
Если
headerFilterне указан, все заголовки включаются. Однако мы рекомендуем использоватьheaderFilterи указывать шаблон максимально точно. В этом случае сгенерированная библиотека содержит только необходимые объявления. Это может помочь избежать различных проблем при обновлении Kotlin или инструментов вашей среды разработки. -
Для исключения конкретных заголовков используйте свойство
excludeFilter.Это может быть полезно для удаления избыточных или проблемных заголовков и оптимизации компиляции, так как объявления из указанных заголовков не включаются в привязки.
excludeFilter = SomeLibrary/time.h
Фильтрация заголовков по картам модулей
Некоторые библиотеки имеют соответствующие файлы 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 будут анализироваться с -DBAR=bar -DFOO=foo1 на Linux и с -DBAR=bar -DFOO=foo2 на macOS. Обратите внимание, что любой параметр файла определения может иметь как общую, так и платформоспецифичную часть.
Добавление пользовательских объявлений
Иногда требуется добавить пользовательские объявления 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» имеет изменяемое свойство .value для доступа к значению.
Типы указателей
Аргумент типа T CPointer<T> должен быть одним из типов «lvalue», описанных выше, например, тип C struct S* отображается на CPointer<S>, int8_t* отображается на CPointer<int_8tVar>, а char** отображается на CPointer<CPointerVar<ByteVar>>.
Указатель C на ноль представлен как нулевой указатель Kotlin, а тип указателя CPointer<T> не является nullable, но CPointer<T>? является nullable. Значения этого типа поддерживают все операции 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(): Stringval String.cstr: CValuesRef<ByteVar>.
Для получения указателя нужно выделить .cstr в нативной памяти, например:
val cString = kotlinString.cstr.getPointer(nativeHeap)
Во всех случаях предполагается, что C-строка закодирована в UTF-8.
Для пропуска автоматического преобразования и обеспечения использования сырых указателей в связываниях можно использовать инструкцию noStringConversion в файле .def , например:
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 с ограниченным сроком действия в определённой области видимости. Например:
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)
...
}
В этом примере все значения, передаваемые в API C 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). Также она может предоставить лямбду вместо ссылки на функцию. Функция или лямбда не должны захватывать какие-либо значения.
Передача пользовательских данных в обратные вызовы
Часто 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–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/native-c-interop.html