Как вызывать нативный C-код из Kotlin на Android

Короткий ответ: объявите в Kotlin external‑методы и загрузите .so через System.loadLibrary, а в C реализуйте функции по JNI‑сигнатуре (Java_пакет_Класс_метод) или зарегистрируйте их через RegisterNatives; соберите через NDK/CMake и тестируйте на нужных ABI. Ниже — пошагово с примерами и советами.

Быстрая настройка проекта и сборка

  1. Установите Android NDK (r26+) в SDK Manager.
  2. В модуле app подключите externalNativeBuild (CMake) и фильтры ABI:
android {
  defaultConfig {
    ndk {
      abiFilters += listOf("arm64-v8a","armeabi-v7a")
    }
  }
  externalNativeBuild {
    cmake {
      path = file("src/main/cpp/CMakeLists.txt")
    }
  }
}
  1. Создайте src/main/cpp/CMakeLists.txt:
cmake_minimum_required(VERSION 3.22.1)
add_library(native-lib SHARED native-lib.c)
find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})
  1. Запишите C-код в src/main/cpp/native-lib.c и соберите ./gradlew assembleDebug. Результат — libnative-lib.so в app/build/outputs.

JNI добавляет накладные расходы на каждый вызов (~20–50 мкс). Используйте нативный код для тяжёлых вычислений (большие блоки, обработка массивов), а не для частых мелких вызовов.

Объявление методов в Kotlin и реализация на C

В Kotlin создайте класс с external-методами и загрузкой библиотеки:

class NativeBridge {
  external fun stringFromJNI(): String
  external fun computeSum(a: Int, b: Int): Int

  companion object {
    init { System.loadLibrary("native-lib") }
  }
}

Вызов из Activity:

val sum = NativeBridge().computeSum(5,10) // 15

В C реализуйте функции по сигнатуре JNI (замените пакет на свой):

#include <jni.h>

JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeBridge_stringFromJNI(JNIEnv *env, jobject thiz) {
    return (*env)->NewStringUTF(env, "Hello from C!");
}

JNIEXPORT jint JNICALL
Java_com_example_myapp_NativeBridge_computeSum(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a + b;
}

Альтернатива: реализуйте методы с произвольными именами и используйте RegisterNatives в JNI_OnLoad — удобно при обфускации или сложных названиях пакетов.

Передача данных, безопасность памяти и многопоточность

  • Примитивы передаются напрямую. Для массивов используйте Get/Release (GetIntArrayElements / ReleaseIntArrayElements) или GetPrimitiveArrayCritical для быстрого доступа.
  • Для строк: GetStringUTFChars и ReleaseStringUTFChars.
  • Всегда проверяйте исключения: if ((env)->ExceptionCheck(env)) { / обработка */ }.
  • Освобождайте локальные ссылки DeleteLocalRef в долгих циклах, чтобы избежать переполнения локальной таблицы.
  • Потоки: если нативный поток вызывает JNI, предварительно вызовите (*vm)->AttachCurrentThread(vm, &env, NULL) и DetachCurrentThread при завершении.

Для больших буферов и частого обмена предпочтительнее выделять native heap и передавать указатели как jlong; это уменьшает накладные расходы на копирование.

Отладка и профилирование

  • Используйте LLDB в Android Studio для брейкпоинтов в нативе.
  • ndk-stack или stacktrace из tombstone помогают сопоставить креши с исходниками.
  • Android Profiler показывает время выполнения нативных функций в CPU trace.
  • Тестируйте на реальном устройстве с целевой ABI — эмулятор может вести себя иначе.

Сравнение подходов (кратко)

ПодходНакладные расходыСложностьКогда применять
JNI (C/C++)Низкие на большие блокиВысокаяOpenCV, крипто, DSP
Kotlin/NativeМеньше JVM-зависимостейСредняяМультиплатформенность
JNAВысокиеНизкаяБыстрое прототипирование

Частые ошибки

  • UnsatisfiedLinkError из‑за неверной сигнатуры/имени функции или неправильного пути System.loadLibrary.
  • Утечки локальных ссылок при частых создании объектов в цикле.
  • Неправильный ABI при сборке — .so не загружается на устройстве.
  • Отсутствие AttachCurrentThread в нативном потоке — crashes при вызове JNI.

FAQ

  • Как найти правильную JNI‑сигнатуру? — Самый простой путь: скомпилировать stub Java/Kotlin и использовать javap -s или генерацию заголовков через javah-подобные инструменты, либо использовать RegisterNatives.
  • Нужно ли писать на C или C++? — C++ удобнее для обёрток и библиотек; C проще и универсальнее. В Android принято использовать C++ (NDK) и extern "C" для C ABI.
  • Как минимизировать накладные расходы? — Сократите количество вызовов JNI, передавайте большие буферы, используйте GetPrimitiveArrayCritical и native heap.

С этими шагами вы сможете подключить и оптимизировать C‑код в Kotlin‑приложении на Android за час и быстро переходить к профилированию и улучшению производительности.