Spec-Zone.ru › Kotlin 2

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

Импорт библиотек C находится в статусе бета-версии. Все объявления Kotlin, созданные инструментом cinterop из библиотек C, должны иметь аннотацию @ExperimentalForeignApi.

Для некоторых API платформенных библиотек, поставляемых с Kotlin/Native (например, Foundation, UIKit и POSIX), требуется только явное согласие на использование.

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

Инструмент анализирует заголовочные файлы C и создает простое соответствие типов, функций и строк C типам Kotlin. Затем созданные заглушки можно импортировать в IDE, чтобы включить автодополнение кода и переход к объявлениям.

Kotlin также поддерживает взаимодействие с Objective-C. Библиотеки Objective-C тоже импортируются с помощью инструмента cinterop. Подробнее см. в разделе Взаимодействие со Swift/Objective-C.

Настройка проекта

Ниже приведен общий порядок действий при работе с проектом, которому требуется использовать библиотеку C:

  1. Создайте и настройте файл определения. В нем указывается, что инструмент cinterop должен включить в привязки Kotlin.

  2. Настройте файл сборки Gradle, чтобы включить cinterop в процесс сборки.

  3. Скомпилируйте и запустите проект, чтобы создать итоговый исполняемый файл.

Чтобы попробовать это на практике, пройдите руководство Создание приложения с использованием взаимодействия с C.

Во многих случаях настраивать взаимодействие с библиотекой 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 — примитивный тип 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. Поэтому в привязку, ожидающую строку C, можно передать любую строку Kotlin.

Также доступны инструменты для преобразования строк 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, можно использовать следующий код:

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, пройдя следующие руководства:

First step Сопоставление примитивных типов данных из C
Second step Сопоставление типов структур и объединений из C
Third step Сопоставление указателей на функции из C
Fourth step Сопоставление строк из C

Начать

1 сентября 2026 г.
Взаимодействие со Swift с помощью экспорта SwiftСопоставление примитивных типов данных из 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

Spec-Zone.ru

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