Взаимодействие с C
В этом документе рассматриваются общие аспекты взаимодействия Kotlin с C. В состав Kotlin/Native входит инструмент cinterop, с помощью которого можно быстро создать всё необходимое для работы с внешней библиотекой C.
Инструмент анализирует заголовочные файлы C и создает простое соответствие типов, функций и строк C типам Kotlin. Затем созданные заглушки можно импортировать в IDE, чтобы включить автодополнение кода и переход к объявлениям.
Настройка проекта
Ниже приведен общий порядок действий при работе с проектом, которому требуется использовать библиотеку C:
Создайте и настройте файл определения. В нем указывается, что инструмент cinterop должен включить в привязки Kotlin.
Настройте файл сборки Gradle, чтобы включить cinterop в процесс сборки.
Скомпилируйте и запустите проект, чтобы создать итоговый исполняемый файл.
Во многих случаях настраивать взаимодействие с библиотекой C вручную не нужно. Вместо этого можно использовать API из стандартизованных привязок, доступных на платформе и называемых платформенными библиотеками. Например, таким образом доступны POSIX на платформах Linux/macOS, Win32 на платформе Windows или фреймворки Apple на macOS/iOS.
Привязки
Основные типы взаимодействия
Все поддерживаемые типы 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 как null, а тип указателя CPointer<T> не допускает значения null, тогда как CPointer<T>? допускает. Значения этого типа поддерживают все операции Kotlin для работы с null, например ?:, ?., !! и так далее:
val path = getenv("PATH")?.toKString() ?: ""
Поскольку массивы тоже сопоставляются с CPointer<T>, для доступа к значениям по индексу поддерживается оператор []:
import kotlinx.cinterop.*
@OptIn(ExperimentalForeignApi::class)
fun shift(ptr: CPointer<ByteVar>, 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>, например:
import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val intPtr = bytePtr.reinterpret<IntVar>()
Или:
import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val intPtr: CPointer<IntVar> = bytePtr.reinterpret()
Как и в C, такие приведения .reinterpret небезопасны и могут привести к трудно обнаружимым проблемам с памятью в приложении.
Кроме того, доступны небезопасные приведения между CPointer<T>? и Long, предоставляемые методами расширения .toLong() и .toCPointer<T>():
val longValue = ptr.toLong() val originalPtr = longValue.toCPointer<T>()
Выделение памяти
Для выделения нативной памяти можно использовать интерфейс NativePlacement, например:
@file:OptIn(ExperimentalForeignApi::class) import kotlinx.cinterop.* val placement: NativePlacement = // See below for placement examples val byteVar = placement.alloc<ByteVar>() val bytePtr = placement.allocArray<ByteVar>(5)
Наиболее логичное место для этого — объект nativeHeap. Он соответствует выделению нативной памяти с помощью malloc и предоставляет дополнительную операцию .free() для освобождения выделенной памяти:
@file:OptIn(ExperimentalForeignApi::class)
import kotlinx.cinterop.*
fun main() {
val size: Long = 0
val buffer = nativeHeap.allocArray<ByteVar>(size)
nativeHeap.free(buffer)
}
nativeHeap требует вручную освобождать память. Однако часто бывает полезно выделять память со временем жизни, ограниченным лексической областью видимости. В таком случае удобно, если память освобождается автоматически.
Для этого можно использовать memScoped { }. Внутри фигурных скобок временное размещение доступно как неявный получатель, поэтому можно выделять нативную память с помощью alloc и allocArray. Выделенная память будет автоматически освобождена при выходе из области видимости.
Например, функцию C, возвращающую значения через параметры-указатели, можно использовать так:
@file:OptIn(ExperimentalForeignApi::class)
import kotlinx.cinterop.*
import platform.posix.*
val fileSize = memScoped {
val statBuf = alloc<stat>()
val error = stat("/", statBuf.ptr)
statBuf.st_size
}
Передача указателей в привязки
Хотя указатели C сопоставляются с CPointer<T> type, параметры-указатели на функции 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. Поэтому в привязку, ожидающую строку C, можно передать любую строку Kotlin.
Также доступны инструменты для преобразования строк 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, можно использовать следующий код:
import kotlinx.cinterop.*
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
memScoped {
LoadCursorA(null, "cursor.bmp".cstr.ptr) // for ASCII or UTF-8 version
LoadCursorW(null, "cursor.bmp".wcstr.ptr) // for UTF-16 version
}
Указатели, локальные для области видимости
С помощью свойства расширения CValues<T>.ptr, доступного в memScoped {}, можно создать указатель в представлении C, стабильный в пределах области видимости, для экземпляра CValues<T>. Это позволяет использовать API, которым требуются указатели C со временем жизни, ограниченным определенным MemScope. Например:
import kotlinx.cinterop.*
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
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 } fun cValue(initialize: T.() -> Unit): CValue<T>применяет переданную функциюinitializeдля выделения памяти подTи преобразует результат вCValue<T>.fun CValue<T>.copy(modify: T.() -> Unit): CValue<T>создает измененную копию существующегоCValue<T>. Исходное значение размещается в памяти, изменяется с помощью функцииmodify(), а затем преобразуется обратно в новоеCValue<T>.fun CValues<T>.placeTo(scope: AutofreeScope): CPointer<T>помещаетCValues<T>вAutofreeScopeи возвращает указатель на выделенную память. Выделенная память автоматически освобождается при удаленииAutofreeScope.
Обратные вызовы
Чтобы преобразовать функцию Kotlin в указатель на функцию C, можно использовать staticCFunction(::kotlinFunction). Вместо ссылки на функцию также можно передать лямбду. Функция или лямбда не должна захватывать какие-либо значения.
Передача пользовательских данных в обратные вызовы
Часто API C позволяют передавать в обратные вызовы некоторые пользовательские данные. Обычно пользователь указывает такие данные при настройке обратного вызова. Например, их передают в функцию C (или записывают в структуру) как void*. Однако ссылки на объекты Kotlin нельзя напрямую передавать в C. Поэтому перед настройкой обратного вызова их необходимо обернуть, а в самом обратном вызове — развернуть, чтобы безопасно передавать их из Kotlin в Kotlin через мир C. Для такой обертки можно использовать класс StableRef.
Чтобы обернуть ссылку:
import kotlinx.cinterop.* @OptIn(ExperimentalForeignApi::class) val stableRef = StableRef.create(kotlinReference) val voidPtr = stableRef.asCPointer()
Здесь voidPtr — это COpaquePointer, которое можно передать в функцию C.
Чтобы развернуть ссылку:
@OptIn(ExperimentalForeignApi::class) val stableRef = voidPtr.asStableRef<KotlinClass>() val kotlinReference = stableRef.get()
Здесь kotlinReference — исходная обернутая ссылка.
Созданный StableRef необходимо впоследствии удалить вручную с помощью метода .dispose(), чтобы избежать утечек памяти:
stableRef.dispose()
После этого он становится недействительным, поэтому развернуть voidPtr больше нельзя.
Макросы
Каждый макрос C, раскрывающийся в константу, представлен свойством Kotlin.
Макросы без параметров поддерживаются в случаях, когда компилятор может вывести тип:
int foo(int); #define FOO foo(42)
В этом случае FOO доступен в Kotlin.
Чтобы поддержать другие макросы, можно вручную открыть к ним доступ, обернув их в поддерживаемые объявления. Например, функциональный макрос FOO можно представить в виде функции foo(), добавив пользовательское объявление в библиотеку:
headers = library/base.h
---
static inline int foo(int arg) {
return FOO(arg);
}
Переносимость
Иногда в библиотеках 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:
import kotlinx.cinterop.*
import platform.posix.*
@OptIn(ExperimentalForeignApi::class)
fun zeroMemory(buffer: COpaquePointer, size: Int) {
memset(buffer, 0, size.convert<size_t>())
}
Кроме того, параметр типа может выводиться автоматически, поэтому в некоторых случаях его можно опустить.
Закрепление объектов
Объекты Kotlin можно закрепить: тогда их положение в памяти гарантированно остается неизменным до снятия закрепления, а указатели на внутренние данные таких объектов можно передавать функциям C.
Можно воспользоваться одним из следующих способов:
-
Используйте функцию расширения
.usePinned(), которая закрепляет объект, выполняет блок кода и снимает закрепление как при обычном выполнении, так и при возникновении исключения:import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readData(fd: Int) { 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. } } }Здесь
pinned— объект специального типаPinned<T>. Для него предусмотрены полезные функции расширения, например.addressOf(), позволяющая получить адрес тела закрепленного массива. -
Используйте функцию расширения
.refTo(), которая выполняет похожую работу внутри, но в некоторых случаях позволяет сократить шаблонный код:import kotlinx.cinterop.* import platform.posix.* @OptIn(ExperimentalForeignApi::class) fun readData(fd: Int) { val buffer = ByteArray(1024) while (true) { val length = recv(fd, buffer.refTo(0), buffer.size.convert(), 0).toInt() if (length <= 0) { break } // Now `buffer` has raw data obtained from the `recv()` call. } }Здесь
buffer.refTo(0)имеет типCValuesRef, который закрепляет массив перед вызовом функцииrecv(), передает функции адрес его нулевого элемента и снимает закрепление с массива после завершения вызова.
Предварительные объявления
Для импорта предварительных объявлений используйте пакет cnames. Например, чтобы импортировать предварительное объявление cstructName, объявленное в библиотеке C с помощью library.package, используйте специальный пакет предварительных объявлений: import cnames.structs.cstructName.
Рассмотрим две библиотеки cinterop: одна содержит предварительное объявление структуры, а другая — ее фактическую реализацию в другом пакете:
// First C library
#include <stdio.h>
struct ForwardDeclaredStruct;
void consumeStruct(struct ForwardDeclaredStruct* s) {
printf("Struct consumed\n");
}
// Second C library
// Header:
#include <stdlib.h>
struct ForwardDeclaredStruct {
int data;
};
// Implementation:
struct ForwardDeclaredStruct* produceStruct() {
struct ForwardDeclaredStruct* s = malloc(sizeof(struct ForwardDeclaredStruct));
s->data = 42;
return s;
}
Чтобы передавать объекты между двумя библиотеками, используйте в коде Kotlin явное приведение as:
// Kotlin code:
fun test() {
consumeStruct(produceStruct() as CPointer<cnames.structs.ForwardDeclaredStruct>)
}
Что дальше
Узнайте, как типы, функции и строки сопоставляются между Kotlin и C, пройдя следующие руководства:
Сопоставление примитивных типов данных из C
Сопоставление типов структур и объединений из C
Сопоставление указателей на функции из C
Сопоставление строк из C
© 2010–2026 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