Как вызывать нативный C-код из Kotlin на Android
Короткий ответ: объявите в Kotlin external‑методы и загрузите .so через System.loadLibrary, а в C реализуйте функции по JNI‑сигнатуре (Java_пакет_Класс_метод) или зарегистрируйте их через RegisterNatives; соберите через NDK/CMake и тестируйте на нужных ABI. Ниже — пошагово с примерами и советами.
Быстрая настройка проекта и сборка
- Установите Android NDK (r26+) в SDK Manager.
- В модуле app подключите externalNativeBuild (CMake) и фильтры ABI:
android {
defaultConfig {
ndk {
abiFilters += listOf("arm64-v8a","armeabi-v7a")
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
}
}
}
- Создайте 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})
- Запишите 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 за час и быстро переходить к профилированию и улучшению производительности.