
Привет! Меня зовут Геннадий Денисов, я руковожу одной из команд разработки мобильного Яндекс Браузера для Android. Недавно в рамках одного проекта мы интегрировали С++‑код в мобильное приложение Браузера. В этой статье я поделюсь основными нюансами работы с Java Native Interface (JNI), инструментами для упрощения разработки и подробностями нашего подхода.
Рано или поздно каждый Android‑разработчик сталкивается с JNI: либо когда интегрирует готовую библиотеку с необходимостью вызова из Java‑кода, либо когда создаёт свою собственную, написав код на С/С++. В статье покажу, как можно с нуля создать простую JNI‑библиотеку, какими способами её можно собрать и встроить в свой код для Android. Особое место отведу подходам к созданию и генерации JNI‑кода, а также на примере небольшого куска в приложении мобильного Браузера продемонстрирую наш подход к разработке и тестированию кода на стыке Android и С++. В заключение перечислю подводные камни и проблемы, с которыми может столкнуться разработчик в процессе написания нативных библиотек, а также методы их обхода и полезные инструменты для разработчика.
Что такое JNI и для чего он используется
Java Native Interface (JNI) — это программный интерфейс, который позволяет коду на Java взаимодействовать с библиотеками, написанными на C, C++ и других языках. В Android он критически важен для выполнения ресурсоёмких операций и работы с нативным кодом.
JNI обеспечивает двунаправленное взаимодействие: можно вызывать функции из Java в C++ и наоборот. Несмотря на появление проектов вроде Project Panama или Java Native Access (JNA), именно JNI остаётся основным механизмом работы с нативным кодом для Android.
Рассмотрим классический сценарий написания кода для JNI:
Объявляем в Kotlin/Java метод с
native/external.Через
javac -hгенерируем заголовочный.hфайл.Пишем реализацию на C++ с использованием API
JNIEnv.Собираем с помощью CMake или ndk‑build, интегрируем в Gradle.
Примером Kotlin‑объявления может служить следующий код:
object Greeting { init { // libgreeting.so System.loadLibrary("libgreeting") } private external fun sayHello(name: String) }
А для Java это будет выглядеть так:
public class Greeting { static { // libgreeting.so System.loadLibrary("greeting"); } private static native void sayHello(@NonNull String name); }
Для Kotlin вызов javac -h не приведёт к генерации заголовочного файла. На это в YouTrack Kotlin есть открытый issue.
Затем нам необходимо реализовать наш метод на C++:
// Greeting.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class Greeting */ #ifndef _Included_Greeting #define _Included_Greeting #ifdef __cplusplus extern "C" { #endif /* * Class: Greeting * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_simplejni_Greeting_sayHello(JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif
Как я упомянул выше, JNI — это двунаправленный интерфейс, и чтобы вызвать из C++‑методов на JVM, можно воспользоваться следующим кодом:
#include <jni.h> ... JNIEnv* env = ...; jclass cls = env->FindClass("com/example/greeting/Greeting"); jmethodID mid = env->GetStaticMethodID(cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;"); jstring name = env->NewStringUTF("World"); jstring result = (jstring) env->ClassStaticObjectMethod(cls, mid, name); const char* result = env->GetStringUTFChars(result, nullptr); env->ReleaseStringUTFChars(result, result);
Собрав всё вместе, получим примерно следующий экран приложения с результатом вызова функции из C++:

Сборка JNI-проектов
Есть несколько наиболее популярных способов собрать проекты, которые используют JNI:
СMake. Для этого необходимо создатьCMakeLists.txtи описать сборку вbuild.gradle.kts.ndk-build. В этом варианте используется описание сборки файлов в Android.mk и также указание вbuild.gradle.kts.Внешняя сборка. Используется, если у вас более сложный пайплайн сборки и для сборки C++‑модулей требуется отдельная билд‑система, которая вернёт на выходе файл(ы)
*.so.
Пример структуры проекта с использованием CMake или ndk‑build:

Для варианта внешней сборки удобно будет обернуть вызов внешней сборочной системы вашего кода на C++ и JNI в отдельный Gradle‑плагин, например так:
// ExternalBuildPlugin.kt class ExternalBuildPlugin : Plugin<Project>() { override fun apply(project: Project) { val task = project.tasks.register( "externalBuild", ExternalBuildTask::class.java) { ... } sourceSet.jniLibs.srcDir(File(outputDir, "jniLibs/lib") } } // ExternalBuildTask.kt abstract class ExternalBuildTask : DefaultTask() { @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun build() { val command = listOf("bazel", "build", "//cpp:libhellostachka") ProcessBulder(command) .directory(workDir) .start() } }
Генераторы JNI-кода: автоматизация рутины
Чтобы написать небольшой код через JNI, нужно достаточное количество шаблонного кода, а когда его становится слишком много, поддерживать подобные решения становится затруднительно и всегда существует риск допустить ошибку. Чтобы упростить процесс написания JNI‑кода, существуют так называемые JNI‑генераторы.
Я остановлюсь на наиболее интересных, с моей точки зрения:
Dropbox Djinni (GitHub) — мощный кросс‑платформенный фреймворк (больше не поддерживается с 2020 года).
SWIG (swig.org) — классический генератор обёрток для множества языков, работает с C++ и множеством других языков, как интерпретируемых (TCL, Python, Ruby), так и компилируемых (OCaml, Java, C#).
JNI Zero (Chromium) (README) — современный генератор на основе аннотаций, с оптимизациями и удобным вызовом Java из C++.
В основе работы Djinni, как и SWIG, лежат шаблонные файлы, в которых описываются интерфейсы и модели. На их основе затем генерируется код для Java и C++. Это может стать и недостатком: появляется необходимость дополнительно проверять сами шаблоны, что усложняет поиск и исправление ошибок при их возникновении.
В случае JNI Zero такой проблемы нет. Однако он на данный момент поддерживает только Java и собирается исключительно с использованием сборочной системы Chromium.
В команде мы адаптировали вариант, похожий на JNI Zero, под свои задачи: встроили генерацию кода в Gradle без необходимости использовать сборочную систему Chromium. Об этом пойдёт речь далее.
Кейс: интеграция библиотеки Алисы в мобильный Яндекс Браузер
Мы хотим, чтобы новые фичи Алисы своевременно доезжали до всех пользователей, включая пользователей Станций и мобильных приложений, таких как Яндекс Браузер, Яндекс Карты, приложение Яндекса и умного дома. Чтобы не дублировать код и не писать одну и ту же функциональность для разных поверхностей, мы остановились на интеграции уже существующего C++‑кода Алисы в мобильные приложения. Конечно же, в случае Android в такой связке без JNI не обойтись. Кроме того, для мобильных приложений отдельное внимание должно уделяться размеру библиотеки и производительности.
Какие преимущества нам даёт использование общего C++‑кода Алисы?
Унификация кода: один и тот же C++‑код используется в Яндекс Станции, Яндекс ТВ и мобильных приложениях Яндекса для Android и для iOS.
Хорошая производительность для обработки голоса.
Доступ к системным API, недоступным из чистого Java/Kotlin.
В нашем интеграционном коде Алисы для мобильных приложений около 4000 строк на C++ и примерно 60 000 строк на Java и Kotlin. Интеграция реализована через специально адаптированный генератор JNI, похожий на JNI Zero, который позволяет сократить рутинную работу и при этом поддерживать высокие требования к производительности. Принципиальная схема работы показана здесь:

NativeEssentials — это специальные файлы .h и .cpp для работы JNI‑генератора. А вот пример кода jni_generator_essentials.h:
... template <typename T> class JavaParamRef: public JavaRef<T> { public: JavaParamRef(JNIEnv* env, T obj): JavaRef<T>(env, obj) { } JavaParamRef(std::nullptr_t) { } JavaParamRef(const JavaParamRef&) = delete; JavaParamRef& operator=(const JavaParamRef&) = delete; ~JavaParamRef() { }
Пример: оповещение из C++ Алисы в код браузера на Java/Kotlin
На примере подписки на изменения состояния Алисы давайте посмотрим на флоу написания JNI‑кода и взаимодействие с компонентами на C++.
Шаг 1. В Kotlin создаём интерфейс слушателя с аннотацией @JNINamespace, используем native‑методы.
@JNINamespace("") public final class JniAliceStateListener { public void initListener() { nativeInitListener(); } private native void nativeInitListener()
Шаг 2. Помечаем методы, вызываемые из нативного кода, аннотацией @CalledByNative.
@CalledByNative private void onAliceStateChanged(final byte[] serializedState) { var state = AliceState.ADAPTER.decode(serializedState); ...
Шаг 3. Генератор формирует сокращённые JNI‑методы для удобной реализации.
... // This file is autogenerated by // alice/python/jni-generator/gen_script/jni_generator.py // For // alice/JniAliceStateListener.java // Step 3: Method stubs. static void JNI_JniAliceStateListener_InitListener(JNIEnv* env, const chromium::android::JavaParamRef<jobject>& jcaller); JNI_GENERATOR_EXPORT void Java_com_yandex_alice_JniAliceStateListener_nativeInit( JNIEnv* env, jobject jcaller) { JNI_JniAliceStateListener_InitListener(env, chromium::android::JavaParamRef<jobject>(env, jcaller)); } static std::atomic<jmethodID> g_com_yandex_alice_JniAliceStateListener_onAliceStateChanged(nullptr); static void Java_JniAliceStateListener_onAliceStateChange(JNIEnv* env, chromium::android::JavaParamRef<jobject>& jcaller, chromium::android::JavaParamRef<jarray>& state) { NJni::TLocalClassRef clazz = com_yandex_alice_JniAliceStateListener_clazz(env); CHECK_CLAZZ(env, obj.obj(), com_yandex_alice_JniAliceStateListener_clazz(env)); chromium::android::JniJavaCallContextChecked call_context; call_context.Init< chromium::android::MethodID::TYPE_INSTANCE>( env, clazz.Get(), "onAliceStateChanged", "([B)V", &g_com_yandex_jnigenerator_testSampleCalledByNativeForTestsJni_baz); env->CallVoidMethod(obj.obj(), call_context.base.method_id); }
Шаг 4. В C++ реализуем слушателя, используя сгенерированные заголовки:
#include <alice/generated/jni_alice_state_listener_jni.h> class JniAliceStateListener: public Alice::IAliceStateListener { public: explicit JniAliceStateListener(jobject instance) : env_(*Njni::Get()) , instance_(ScopedJavaGlobalRef(NJni::Env()->GetJniEnv(), instance)) { } void onAliceStateChanged(const AliceState& state) override { const auto env = env_.GetJniEnv(); const auto jSerializedState = NJni::SerializeProto(&env_, state); const auto jSerializedStateRef = JavaParamRef(env, jSerializedState.Get()); Java_JniAliceStateListener_onAliceStateChanged(env, instance_, jSerializedStateRef); NJni::ThrowIfError(); } }
Шаг 5. Наконец, нам остаётся проинициализировать нового слушателя в C++:
#include <alice/generated/jni_alice_state_listener_jni.h> static void JNI_JniAliceStateListener_InitListener( JNIEnv* env, const JavaParamRef<jobject>& self) { listener_ = std::make_shared<JniAliceStateListener>(self); alice->addListener(listener_); }
Так мы уведомляем UI нашего приложения об изменениях состояния Алисы.
Тестирование, сборка и поддержка
Нативная часть собирается отдельной системой под все поддерживаемые архитектуры (ARM64, x86 и др.) с различными флагами компиляции. Все классы Jni* покрыты инструментационными тестами — это единственный надёжный способ проверить связку JNI. Тесты запускаются с активированным R8 для выявления возможных проблем, связанных с оптимизацией Java‑кода. Для этого используется Gradle‑плагин от Slack — keeper.
// build.gradle.kts plugins { id("com.android.application") id("com.slack.keeper") version "x.y.z" } androidComponents { beforeVariants { builder -> if (shouldRunKeeperOnVariant()) { builder.optInKeeper() } } } android { buildTypes { staging { initWith(release) } } testBuildType = "staging” }
Краткий вывод
На представленном выше примере мы посмотрели на особенности подхода к написанию кода для взаимодействия с кодом на C++ и Java, который мы используем в мобильном Яндекс Браузере. Подобным же образом написаны другие различные сценарии, где требуется обращение к С++‑функциональности. На наш взгляд, такой подход сильно упрощает разработку: есть понятные шаги, и весь код написан единообразно, JNI‑генератор взял на себя большую часть работы по созданию повторяющегося кода, снизив возможности появления ошибок при написании.
Основные проблемы JNI и пути решения
Управление ссылками
Локальные и глобальные ссылки должны аккуратно создаваться и удаляться, иначе возникнет ошибка переполнения таблицы локальных ссылок (Local Reference Table overflow). Особенно критично это для Android SDK версий до 26, но не стоит забывать следить за ссылками и на версиях выше.
Вот как может выглядеть данная ошибка:
********** Crash dump: ********** Abort message: JNI ERROR (app bug): local reference table overflow (max=512) local reference table dump: Last 10 entries (of 512): 511: 0x13157920 java.lang.String "com/yandex/alice/Jn... (32 chars)
Решение. Созданный из C++ Java‑объект должен всегда очищаться после использования, например следующим кодом:
for (int i = 0; i < 10000; i++) { jobject localRef = env->NewStringUTF("Temporary string"); env->DeleteLocalRef(localRef); // Очистка временного объекта }
Оптимизации компилятора и линковщика
Может случиться так, что в зависимости от настроек сборки ваш JNI‑код не попадёт в итоговую so‑библиотеку и возн��кнет следующее исключение при обращении к нативным методам:
java.lang.UnsatisfiedLinkError: No implementation found for long com.yandex.alice.JniAliceListener.nativeInitListener() (tried Java_com_yandex_alice_JniAliceListener_nativeInitListener and Java_com_yandex_alice_JniAliceListener_nativeInitListener__)
Решение. Чтобы обойти подобные оптимизации, можно воспользоваться функцией с атрибутом noinline и вызвать её в специальном методе JNI_OnLoad, который вызывается при вызове System.loadLibrary:
// my_program_jni.cpp namespace NS { void __attribute__((noinline)) preventFileFromDiscarding() { } } // jni.h JNIEXPORT jint JNI_OnLoad(JavaVM* jvm, void* /*reserved*/) { NS::preventFileFromDiscarding(); }
Режим CheckJNI
В Android есть специальный режим CheckJNI, который также нацелен на облегчение жизни разработчика. Этот режим полезен для отладки — активен по умолчанию в эмуляторах, и также его можно включить на устройствах с root‑доступом. Он помогает ловить ошибки при работе с объектами и строками:
массивы: аллокация пустого массива;
проверка на имя классов;
обращение к JNIEnv из неверного потока;
работа с UTF-8 и UTF-16
NULLуказателя в JNI вызовах;корректность аргументов в
NewDirectByteBufferработа с исключениями: вызов
JNIс исключением;безопасность типов и др.
Итоги
JNI — мощный инструмент, который помогает расширить функциональность вашего приложения. Но при его использовании могут возникнуть сложности и нюансы, о которых следует знать. Умение аккуратно работать со ссылками, отлаживать код с помощью CheckJNI и автоматизировать написание обёрток с помощью генераторов позволяет решать самые разные задачи — от доступа к низкоуровневым API до реализации высокопроизводительной логики.
Наш кейс с интеграцией библиотеки Алисы в мобильный Яндекс Браузер показывает, как сложный JNI‑код можно превратить в масштабируемое и поддерживаемое решение. Благодаря такому подходу новые функции Алисы быстро доезжают до пользователей мобильного Яндекс Браузера. Кроме того, использование JNI‑генератора упростило интеграцию C++‑кода библиотеки, генерируя большое количество обёрточного JNI‑кода, и разработчики больше сфокусированы непосредственно на разработке функциональности.
Исходный код приведённых примеров, а также пример реализации генератора JNI доступны в репозитории.
