Когда я только начал связывать Kotlin с C++,JNI казался единственным вариантом. Но потом в одном проекте мне не нужна была JVM — чистый бэкенд на Kotlin Native, и тут я открыл для себя cinterop. Это оказалось настолько проще, что я решил написать отдельную статью. Если ваш проект не привязан к JVM — возможно, JNI вам вообще не нужен.
JNI vs Kotlin Native: два разных мира
Давайте честно: JNI — это боль. Вы пишете C++ код с JNI-сигнатурами, управляете JNIEnv, следите за LocalRef/GlobalRef, обрабатываете исключения через ExceptionCheck. Каждый вызов через JNI имеет оверхед около 100-200 наносекунд просто на трансляцию. Для одиночных вызовов это незаметно, но в tight loop с тысячами итераций — съедает производительность.
Kotlin Native компилируется напрямую в нативный код через LLVM. Нет JVM, нет сборщика мусора (вместо него automatic reference counting), нет JNI-оверхеда. Вызов C-функции из Kotlin Native — это буквально обычный вызов функции на уровне машинного кода.
Настройка cinterop через .def файл
Всё начинается с .def файла — это описание C-API, которое генерирует Kotlin-биндинги. Вот реальный пример из моего проекта:
// native/image_processor.def
headers = image_processor.h
headerFilter = image_processor.h
package = com.example.native
И对应的 C-заголовок:
// native/image_processor.h
#ifndef IMAGE_PROCESSOR_H
#define IMAGE_PROCESSOR_H
#include
typedef struct {
uint8_t* data;
int width;
int height;
int channels;
} ImageData;
ImageData* image_create(int width, int height, int channels);
void image_process(ImageData* img, const char* filter_type);
void image_destroy(ImageData* img);
int image_get_pixel(ImageData* img, int x, int y, int channel);
#endif
А вот так это выглядит на Kotlin-стороне — просто вызываете C-функции как обычные Kotlin-функции:
// Выглядит как обычный Kotlin, но под капотом — нативный вызов
fun processImageNative(width: Int, height: Int): ByteArray {
memScoped {
val img = image_create(width, height, 4)
image_process(img, "grayscale")
val result = ByteArray(width * height * 4)
img?.data?.readBytes(result)
image_destroy(img)
return result
}
}
Gradle конфигурация
Настройка Kotlin Native с cinterop в Gradle достаточно проста, но есть нюансы с таргетами. Вот мой рабочий build.gradle.kts:
kotlin {
linuxX64 {
compilations["main"].cinterops {
create("image_processor") {
defFile = file("native/image_processor.def")
includeDirs("native")
}
}
}
sourceSets {
val nativeMain by getting
}
}
tasks.withType {
compilerOpts.add("-I${projectDir}/native")
}
Ограничения: только C ABI
Вот тут приходит главное разочарование. cinterop работает только с C ABI. Это значит: никаких C++ классов, никаких std::string, никаких шаблонов. Если у вас C++ библиотека — вам придётся написать C-обёртку (C API facade). В моём проекте это выглядело так: я создал image_processor.h с чистыми C-функциями, а внутри image_processor.cpp использовал полноценный C++ с OpenCV.
Паттерн простой:对外暴露 C functions, внутренне используем C++. Да, это дополнительный слой, но он намного проще и чище, чем JNI-обёртки с их JNIEnv и сигнатурами.
Бенчмарк: JNI vs Kotlin Native
Я замерил оба подхода на одной задаче — вызов простой C-функции, которая инвертирует массив из 10000 байт, 100000 итераций. Результаты на Linux x86_64, Ryzen 9:
JNI (Kotlin/JVM 17): 10 000 байт — 340 мс, 100 000 байт — 890 мс. Kotlin Native (cinterop): 10 000 байт — 85 мс, 100 000 байт — 210 мс. Разница примерно в 3.5–4 раза. На больших массивах разница меньше, потому что доминирует работа с памятью, а не оверхед вызова.
Но честно — для большинства бэкенд-приложений этот оверхед не критичен. Если вы делаете один вызов в C++ на HTTP-запрос, разница в 200 наносекунд вас не убьёт. А вот если вы вызываете C++ функцию в цикле миллион раз — тогда да, Kotlin Native выигрывает.
Когда что выбирать?
Мое правило такое: если у вас уже есть JVM-стек (Spring, Ktor на JVM, Android) — используйте JNI, не усложняйте. Если вы строите нативный сервис (Ktor на Kotlin Native, Kubernetes-бинарник) — cinterop будет проще и быстрее. Если вам нужны C++ классы и сложные типы — JNI даёт больше гибкости, хоть и ценой боли. Если вам нужна Kotlin Multiplatform с общим нативным кодом — cinterop единственный вариант.
В следующей статье разберу, как настроить CMake + Gradle для JNI-проекта со всеми платформами. Это отдельная песня, поверьте.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье