Загрузка
Все статьи

Kotlin/JNI: вызов C++ из Kotlin — от Hello World до продакшена

Практический гайд по вызову C++ кода из Kotlin через JNI. Разбираю настройку, передачу типов, управление памятью и реальные продакшен-паттерны на опыте бэкенд-разработчика.

За последний год я переписал три проекта, где Kotlin общался с C++ через JNI. Начинал я, как и все, с простого Hello World, а закончил продакшен-пайплайном обработки изображений, который крутится на сервере под нагрузкой 500 RPS. В этой статье собрал всё, что знаю о JNI — от базовой настройки до реальных граблей, на которые наступал.

Зачем вообще JNI?

Kotlin отличный язык, но бывают задачи, где его производительности не хватает. В моём случае это была обработка изображений: ресайз, конвертация цветовых пространств, детекция лиц. На чистом Kotlin один запрос обрабатывался 120 мс, а на C++ — 15 мс. Разница в 8 раз. Если у вас похожие задачи — криптография, сигнальная обработка, тяжёлые вычисления — JNI может стать решением.

JNI (Java Native Interface) — это мост между JVM и нативным кодом. Да, Kotlin работает на JVM, поэтому весь API JNI доступен из Kotlin напрямую. Никаких магических библиотек — только стандартный jni.h и CMake.

Базовая настройка: Hello World

Начнём с минимального примера. Сначала Kotlin-сторона — объявляем нативную функцию:

class NativeLib {
    companion object {
        init {
            System.loadLibrary("native_lib")
        }
    }

    external fun stringFromJNI(): String
    external fun processImage(data: ByteArray, width: Int, height: Int): ByteArray
}

Теперь C++ реализация. Заметьте — имя функции строго регламентировано: Java_пакет_класс_метод:

#include 
#include 

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_NativeLib_stringFromJNI(JNIEnv *env, jobject /* this */) {
    std::string hello = "Привет из C++, Артём!";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_example_NativeLib_processImage(
    JNIEnv *env, jobject,
    jbyteArray data, jint width, jint height
) {
    jsize len = env->GetArrayLength(data);
    jbyte *pixels = env->GetByteArrayElements(data, nullptr);

    // Обработка пикселей (инвертирование)
    for (jsize i = 0; i < len; i++) {
        pixels[i] = 255 - pixels[i];
    }

    jbyteArray result = env->NewByteArray(len);
    env->SetByteArrayRegion(result, 0, len, pixels);
    env->ReleaseByteArrayElements(data, pixels, 0);
    return result;
}

CMakeLists.txt для сборки

Без CMake ничего не скомпилируется. Вот минимальный конфиг, который я использую во всех проектах:

cmake_minimum_required(VERSION 3.22.1)
project("native_lib" CXX)

find_library(log-lib log)

add_library(native_lib SHARED
    native_lib.cpp
)

target_link_libraries(native_lib
    ${log-lib}
)

target_compile_features(native_lib PRIVATE cxx_std_17)

Передача строк и примитивов

Примитивы (int, long, double) передаются напрямую — никаких проблем. А вот со строками нужно быть аккуратным. Когда я первый раз передал строку из Kotlin в C++ и забыл освободить память через ReleaseStringUTFChars, сервер упал через 4 часа под нагрузкой с OutOfMemoryError.

Правило простое: каждый GetStringUTFChars должен иметь парный ReleaseStringUTFChars. Каждый NewStringUTF возвращает LocalRef, который JNI очистит автоматически, но если вы создаёте много объектов в цикле — лучше вызывать DeleteLocalRef вручную.

GlobalRef vs LocalRef

Это самая частая причина крашей, которую я видел у новичков. LocalRef живёт только внутри одного нативного вызова. Если вы хотите сохранить ссылку на Java-объект между вызовами — нужно создавать GlobalRef:

static jclass globalCallbackClass = nullptr;
static jmethodID globalCallbackMethod = nullptr;

extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeLib_initCallback(
    JNIEnv *env, jobject, jobject callback
) {
    // Сохраняем класс и метод как глобальные ссылки
    jclass localClass = env->GetObjectClass(callback);
    globalCallbackClass = (jclass)env->NewGlobalRef(localClass);
    env->DeleteLocalRef(localClass);

    globalCallbackMethod = env->GetMethodID(
        globalCallbackClass, "onResult", "(I)V"
    );
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_NativeLib_doWork(JNIEnv *env, jobject) {
    // Глобальная ссылка живёт между вызовами
    if (globalCallbackClass && globalCallbackMethod) {
        env->CallVoidMethod(
            env->NewObject(globalCallbackClass,
                env->GetMethodID(globalCallbackClass, "", "()V")),
            globalCallbackMethod, 42
        );
    }
}

Продакшен-пример: обработка изображений

В реальном проекте мы вызывали C++ библиотеку для обработки изображений из Kotlin-сервиса на Ktor. Пайплайн такой: Kotlin получает multipart-запрос с изображением, передаёт байты в C++, C++ ресайзит, накладывает водяной знак и возвращает готовые байты. Всю тяжёлую работу делает OpenCV в C++, а Kotlin занимается маршрутизацией и бизнес-логикой.

Главный урок: оборачивайте каждый JNI-вызов в try-catch на Kotlin-стороне и проверяйте исключения через env->ExceptionCheck() на C++-стороне. JNI не бросает C++ исключения — он просто устанавливает флаг pending exception, и если его не обработать, JVM упадёт при следующем JNI-вызове.

Типичные ошибки

За год я собрал свой личный топ граблей. Первая — утечки памяти. JNI-объекты, созданные в нативном коде, не собираются GC автоматически. Забыли DeleteGlobalRef? Память утекает. Вторая — краш JVM из-за nullptr. Если FindClass вернул nullptr, а вы продолжили работу — краш гарантирован. Третья — потокобезопасность. JNIEnv* привязан к потоку. Если вы вызываете JNI-функцию из другого потока, нужно сначала получить env через JavaVM->AttachCurrentThread.

JNI — не самый удобный инструмент, но когда нужен максимум производительности и у вас уже есть C++ код, альтернативы немного. В следующих статьях разберу Kotlin Native cinterop и передачу сложных структур. Удачи в нативных дебрях!

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

CMake: модульная архитектура для большого C++ проекта — без ad-hoc include путей

Как мы реорганизовали CMake-структуру проекта на 15+ модулей, избавились от хаотичных include_directories и перешли на современный CMake с target-based зависимостями.

20 июня 2026 10 мин

Kotlin + C++: реальный кейс — видеопроцессинг на сервере через JNI

Как мы построили сервер видеотранскодинга на Kotlin REST API + C++ FFmpeg через JNI. Прокачка JNI_OnLoad, передача фреймов через DirectByteBuffer, обработка ошибок и бенчмарки.

19 июня 2026 13 мин

C++: потоки и потокобезопасность — мьютексы, atomics и deadlock-ы на практике

Разбираем многопоточность в C++ на реальных примерах из продакшена: мьютексы, lock_guard, unique_lock, std::atomic, deadlock-ы и как их избежать. Сравнение с корутинами Kotlin.

18 июня 2026 11 мин