Взаимодействие с 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", есть свойство 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— примитивный тип KotlinArray<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 со сроком жизни, привязанным к определённой 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)
...
}
В этом примере все значения, передаваемые в 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). Также она может предоставить лямбду вместо ссылки на функцию. Функция или лямбда не должны захватывать какие-либо значения.
Передача пользовательских данных в обратные вызовы
Часто API C позволяют передавать некоторые пользовательские данные в обратные вызовы. Такие данные обычно предоставляются пользователем при настройке обратного вызова. Они передаются в некоторую функцию 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