Spec-Zone.ru › Kotlin 1.7

Взаимодействие с C

Kotlin/Native следует общей традиции Kotlin, обеспечивая отличную совместимость с существующим программным обеспечением платформ. В случае с нативной платформой, наиболее важной целью совместимости является библиотека C. Поэтому Kotlin/Native поставляется с инструментом cinterop, который можно использовать для быстрого создания всего необходимого для взаимодействия с внешней библиотекой.

Следующий рабочий процесс ожидается при взаимодействии с нативной библиотекой:

  1. Создайте файл .def, описывающий, что следует включить в привязки.

  2. Используйте инструмент cinterop, чтобы создать привязки Kotlin.

  3. Запустите компилятор 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, какие из включённых заголовков необходимо импортировать. Отдельные объявления из других заголовков также могут быть импортированы в случае прямых зависимостей.

Фильтрация заголовков по шаблонам

Можно фильтровать заголовки по шаблонам. Значение свойства headerFilter из файла .def обрабатывается как список шаблонов, разделённых пробелами. Если включённый заголовок соответствует любому из шаблонов, то объявления из этого заголовка включаются в привязки.

Шаблоны применяются к путям заголовков относительно соответствующих элементов пути включения, например, time.h или curl/curl.h. Таким образом, если библиотека обычно включается с #include <SomeLibrary/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 будут анализироваться с -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 в вашей программе библиотека подключается автоматически.

END_OF_DOCUMENT_MARKER

Связывания

Основные типы взаимодействия

Все поддерживаемые типы 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.

Чтобы пропустить автоматическое преобразование и гарантировать использование исходных указателей в связываниях, можно использовать инструкцию 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 }
    
END_OF_DOCUMENT_MARKER

Обратные вызовы

Для преобразования функции 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 и const char* представляют собой разделенные пробелами списки перечислений, которые должны быть сгенерированы как перечисление 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, которая фиксирует объект, выполняет блок и открепляет его по нормальному и исключательному путям.

Последнее изменение: 25 августа 2021
Начало работы с Kotlin/Native с помощью компилятора командной строки Преобразование примитивных типов данных из C – учебник

© 2010–2022 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

Spec-Zone.ru

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