Spec-Zone.ru › Kotlin 1.6

Взаимодействие с 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.

Для типов, имеющих оба представления, у того, что имеет «левостороннее значение», есть свойство mutable .value для доступа к значению.

Типы указателей

Аргумент типа T типа CPointer<T> должен быть одним из типов «левосторонних значений», описанных выше, например, тип C struct S* отображается на CPointer<S>, int8_t* отображается на CPointer<int_8tVar>, а char** отображается на CPointer<CPointerVar<ByteVar>>.

Указатель C на ноль представлен как нулевое значение Kotlin null, и тип указателя 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> возвращает левостороннее значение типа T, на которое указывает этот указатель. Обратная операция — .ptr: она принимает левостороннее значение и возвращает указатель на него.

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)
    ...
}

В этом примере все значения, передаваемые в C-API new_menu(), имеют область видимости самого внутреннего memScope, к которому они принадлежат. Как только поток управления покидает область видимости memScoped, указатели C становятся недопустимыми.

Передача и получение структур по значению

Когда функция C принимает или возвращает структуру/объединение T по значению, соответствующий тип аргумента или возвращаемого значения представляется как CValue<T>.

CValue<T> — это непрозрачный тип, поэтому к полям структуры нельзя получить доступ с помощью соответствующих свойств Kotlin. Это возможно, если API использует структуры как дескрипторы, но если доступ к полям необходим, доступны следующие методы преобразования:

  • fun T.readValue(): CValue<T>. Преобразует (левостороннее значение) 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, которая закрепляет объект, выполняет блок и открепляет его при нормальном завершении и при возникновении исключения.

Последнее изменение: 07 апреля 2022
Начало работы с 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