Android Studio и язык C: как работать с нативным кодом через NDK

Android Studio поддерживает разработку на C и C++ через NDK (Native Development Kit) — если при создании проекта выбрать шаблон Native C++, среда сама настроит CMake, JNI-обёртку и сборку нативной библиотеки. Без NDK компилировать C-код под Android нельзя: стандартный toolchain Gradle работает только с Java и Kotlin.

Ниже разберём, как установить необходимые компоненты, создать проект с нативным кодом, подключить существующие C-исходники и устранить типичные ошибки сборки. Материал ориентирован на актуальные версии Android Studio, но пути меню могут незначительно отличаться в зависимости от версии IDE.

Зачем нужен C/C++ в Android-приложении

Нативный код применяют там, где критична производительность или нужна переносимость готовых библиотек. Типичные сценарии — игровые движки, обработка аудио и видео, криптография, физические расчёты, портирование кроссплатформенных библиотек, уже написанных на C/C++.

Для обычной бизнес-логики приложения нативный код не нужен: Kotlin и Java закрывают подавляющее большинство задач. Переход на C оправдан, когда профилирование показало узкое место, либо когда библиотека уже существует и переписывать её на Kotlin нерентабельно.

Установка NDK и CMake

Перед началом работы проверьте, что в SDK Manager установлены два компонента: NDK (Side by side) и CMake. Откройте Tools → SDK Manager, перейдите на вкладку SDK Tools и отметьте нужные пункты. Если NDK отсутствует, сборка проекта с нативным кодом завершится ошибкой вида «NDK not configured».

  • 🛠️ NDK (Side by side) — набор компиляторов и библиотек для сборки C/C++ под Android
  • 📦 CMake — система сборки, которую Android Studio использует для нативного кода по умолчанию
  • 🔧 LLDB — отладчик нативного кода, без него breakpoints в C-файлах работать не будут
  • 📁 Android SDK Build-Tools — обычно уже установлены, но проверить стоит
⚠️ Внимание: версия NDK фиксируется в файле build.gradle модуля через параметр ndkVersion. Если на CI или у коллеги установлена другая версия, Gradle может скачать её автоматически или выдать ошибку — зафиксируйте версию явно, чтобы сборка была воспроизводимой.
📊 Для какой задачи вы подключаете C/C++ в Android Studio?
Портирование готовой C-библиотеки
Производительные вычисления
Игровой движок / графика
Изучение JNI и NDK

Создание проекта с нативным кодом

Самый простой способ получить рабочую конфигурацию — создать новый проект через File → New → New Project и выбрать шаблон Native C++. Мастер предложит указать стандарт C++ (например, C++17) и сгенерирует минимальный рабочий пример.

В результате в проекте появятся три ключевых элемента: папка src/main/cpp с исходниками, файл CMakeLists.txt со сценарием сборки и блок externalNativeBuild в build.gradle, связывающий Gradle с CMake. Сгенерированный пример выводит строку из C++-функции в TextView — этого достаточно, чтобы проверить всю цепочку сборки.

☑️ Проверка готовности проекта к нативной сборке

Выполнено: 0 / 5

Структура CMakeLists.txt и сборка библиотеки

Файл CMakeLists.txt описывает, какие исходники компилировать и в какую библиотеку их собрать. Минимальная конфигурация выглядит так:

cmake_minimum_required(VERSION 3.22.1)

project("myapp")

add_library(myapp SHARED native-lib.cpp)

find_library(log-lib log)

target_link_libraries(myapp ${log-lib})

Команда add_library с типом SHARED собирает динамическую библиотеку .so, которая попадёт в APK. Для чистого C-кода достаточно указать файлы с расширением .c — CMake сам выберет компилятор C вместо C++. Если исходников много, их перечисляют списком или собирают через file(GLOB ...), хотя явное перечисление считается более надёжной практикой.

Связка Java/Kotlin с C через JNI

Вызов нативного кода из приложения происходит через JNI (Java Native Interface). В Kotlin-классе объявляется функция с ключевым словом external, а библиотека загружается через System.loadLibrary("myapp") в блоке init или companion object.

На стороне C имя функции должно соответствовать соглашению JNI: Java_пакет_Класс_метод, где точки пакета заменены подчёркиваниями. Функция принимает указатель JNIEnv* и объект jobject (или jclass для статических методов). Для C++ обязательно оборачивание в extern "C", иначе компилятор изменит имена символов (name mangling), и вызов завершится ошибкой UnsatisfiedLinkError.

Тип JNIТип Java/KotlinТип C
jintIntint32_t
jlongLongint64_t
jbooleanBooleanuint8_t
jstringStringconst char* (через GetStringUTFChars)
jobjectArrayArray<Any>массив jobject

Работа со строками требует особого внимания: указатель, полученный через GetStringUTFChars, необходимо освобождать вызовом ReleaseStringUTFChars, иначе возможны утечки памяти. Это одна из самых частых ошибок при первом знакомстве с JNI.

Как передавать сложные структуры между Kotlin и C

Через JNI удобно передавать примитивы и строки. Для сложных данных обычно используют один из подходов: сериализацию в байтовый массив (ByteArray), передачу указателя в виде long с хранением объекта на стороне C++, либо прямой доступ к полям объекта через GetFieldID/SetFieldID. Последний способ хрупкий — он зависит от имён полей, которые могут быть обфусцированы R8/ProGuard, поэтому для production-кода чаще выбирают ByteArray или указатели.

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

Ошибка UnsatisfiedLinkError возникает, когда JVM не находит нативный метод. Проверьте три вещи: совпадает ли имя библиотеки в loadLibrary с именем в add_library, правильно ли сформировано JNI-имя функции и не забыт ли extern "C" в C++-файле.

Ошибки вида undefined reference на этапе линковки означают, что функция объявлена, но её реализация не попала в сборку. Убедитесь, что файл с реализацией перечислен в add_library, а нужные системные библиотеки подключены через target_link_libraries — например, log для логирования или android для системных API.

⚠️ Внимание: нативный код собирается отдельно под каждую ABI (arm64-v8a, armeabi-v7a, x86_64). Если устройство или эмулятор имеет архитектуру, которой нет среди собранных ABI, приложение упадёт при загрузке библиотеки. Список ABI настраивается в build.gradle через блок ndk { abiFilters }.
  • 🐛 UnsatisfiedLinkError — проверьте имя библиотеки, JNI-сигнатуру и extern "C"
  • 🔗 undefined reference — исходник не добавлен в CMakeLists.txt или не хватает target_link_libraries
  • 📱 Падение только на реальном устройстве — вероятно, несовпадение ABI; эмулятор часто x86_64, а телефоны — arm64
  • 🧱 Ошибка «CMake not found» — CMake не установлен в SDK Manager или зафиксирована несовместимая версия
⚠️ Внимание: падение нативного кода (SIGSEGV) не выдаёт привычного Java-стектрейса — приложение просто завершается. Ищите диагностику в Logcat по строкам DEBUG и backtrace, а для пошаговой отладки выбирайте конфигурацию запуска Dual или Native в настройках Run Configuration, если она доступна в вашей версии IDE.

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

Для вывода сообщений из C-кода используется библиотека liblog: подключите заголовок <android/log.h> и вызывайте __android_log_print(ANDROID_LOG_INFO, "TAG", "сообщение"). Логи появятся в Logcat наряду с обычными сообщениями приложения.

Точки останова в C/C++-файлах работают при установленном LLDB и типе отладчика, включающем нативный код. Если breakpoint игнорируется, проверьте, что сборка выполнена в debug-варианте: release-сборка с оптимизациями и обрезанными символами отлаживается существенно хуже.

FAQ: частые вопросы

Можно ли писать на чистом C без C++ в Android Studio?

Да. CMake компилирует файлы .c компилятором C автоматически. Для чистого C не нужен extern "C" — это требование C++. JNI-заголовки полностью совместимы с C.

Чем ndk-build отличается от CMake?

ndk-build — старая система сборки на основе make-файлов (Android.mk). CMake — современный вариант, который Android Studio использует по умолчанию. Оба поддерживаются, но для новых проектов рекомендуется CMake.

Нужен ли отдельный NDK для каждого проекта?

Нет. NDK устанавливается один раз через SDK Manager и может храниться в нескольких версиях параллельно. Конкретная версия задаётся параметром ndkVersion в build.gradle модуля.

Почему приложение падает на устройстве, но работает на эмуляторе?

Наиболее вероятная причина — разные архитектуры: эмулятор обычно x86_64, а смартфоны — arm64-v8a. Проверьте, что в настройках сборки включены ABI для реальных устройств, и что .so-библиотека собрана под нужную архитектуру.

Ускорит ли перенос кода на C работу приложения?

Не гарантированно. Выигрыш есть в вычислительно тяжёлых задачах, но сам вызов через JNI имеет накладные расходы. Перед переносом кода стоит профилировать приложение и убедиться, что узкое место действительно в вычислениях, а не в вводе-выводе или работе с UI.