| name | android-jni |
| description | Use when writing, reviewing, or debugging JNI (Java Native Interface) code that bridges Android Java/Kotlin and C/C++ native code. Covers function naming conventions, JNI type mappings, local and global references, string and array handling, exception propagation, threading with JavaVM, and JNI_OnLoad setup.
|
| argument-hint | <class-or-method> [write|review|debug] |
Android JNI — Java Native Interface
Practices for writing correct, safe JNI code in Android NDK projects targeting
Android Automotive OS (AAOS).
Source of truth: Android JNI Tips
and the JNI specification.
When to Use This Skill
- Implementing
native methods declared in Java or Kotlin.
- Reading/writing Java objects, strings, and arrays from C++.
- Calling Java methods from C++ (callbacks, listeners).
- Handling exceptions across the JNI boundary.
- Using JNI from background C++ threads.
Function Naming Convention
JNI functions are resolved by name. The pattern is:
Java_<fully_qualified_class_name_with_dots_replaced_by_underscores>_<methodName>
package com.example.audio;
public class AudioEngine {
public native int initialize(int sampleRate);
public native void shutdown();
public static native String getVersion();
}
#include <jni.h>
extern "C" JNIEXPORT jint JNICALL
Java_com_example_audio_AudioEngine_initialize(JNIEnv* env, jobject thiz, jint sampleRate) {
return 0;
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_audio_AudioEngine_shutdown(JNIEnv* env, jobject thiz) {
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_audio_AudioEngine_getVersion(JNIEnv* env, jclass clazz) {
return env->NewStringUTF("1.0.0");
}
Rules:
extern "C" prevents C++ name mangling — required.
JNIEXPORT and JNICALL are required macros from <jni.h>.
JNIEnv* is per-thread and must NOT be shared or stored across threads. Use JavaVM for cross-thread access (see below).
JNI Type Mappings
| Java type | JNI type | C type |
|---|
boolean | jboolean | uint8_t |
byte | jbyte | int8_t |
char | jchar | uint16_t |
short | jshort | int16_t |
int | jint | int32_t |
long | jlong | int64_t |
float | jfloat | float |
double | jdouble | double |
void | void | void |
String | jstring | — |
Object | jobject | — |
Class | jclass | — |
int[] | jintArray | — |
byte[] | jbyteArray | — |
Object[] | jobjectArray | — |
Strings
extern "C" JNIEXPORT void JNICALL
Java_com_example_Foo_processName(JNIEnv* env, jobject thiz, jstring jName) {
const char* name = env->GetStringUTFChars(jName, nullptr);
if (name == nullptr) return;
LOGI("Name: %s", name);
env->ReleaseStringUTFChars(jName, name);
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_Foo_getDeviceName(JNIEnv* env, jclass clazz) {
return env->NewStringUTF("AAOS Audio Device");
}
Rule: Every GetStringUTFChars must be paired with ReleaseStringUTFChars, even on error paths.
Arrays
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Foo_processBuffer(JNIEnv* env, jobject thiz, jbyteArray jBuffer) {
jsize len = env->GetArrayLength(jBuffer);
jbyte* buf = env->GetByteArrayElements(jBuffer, nullptr);
if (buf == nullptr) return -1;
env->ReleaseByteArrayElements(jBuffer, buf, JNI_ABORT);
return len;
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_example_Foo_getAudioData(JNIEnv* env, jobject thiz) {
const std::vector<int8_t> data = getNativeAudioData();
jbyteArray result = env->NewByteArray(static_cast<jsize>(data.size()));
if (result == nullptr) return nullptr;
env->SetByteArrayRegion(result, 0, static_cast<jsize>(data.size()),
reinterpret_cast<const jbyte*>(data.data()));
return result;
}
Local and Global References
The JVM garbage-collects Java objects. JNI references keep them alive.
| Reference type | Scope | Created by | Freed by |
|---|
| Local | Current JNI call frame | Returned by most JNI calls | Automatically on method return; manually with DeleteLocalRef |
| Global | Until explicitly deleted | NewGlobalRef | DeleteGlobalRef (you MUST call this) |
| Weak global | Until GC collects the object | NewWeakGlobalRef | DeleteWeakGlobalRef |
static jobject g_listener = nullptr;
extern "C" JNIEXPORT void JNICALL
Java_com_example_Foo_setListener(JNIEnv* env, jobject thiz, jobject listener) {
if (g_listener != nullptr) {
env->DeleteGlobalRef(g_listener);
}
g_listener = env->NewGlobalRef(listener);
}
void clearListener(JNIEnv* env) {
if (g_listener != nullptr) {
env->DeleteGlobalRef(g_listener);
g_listener = nullptr;
}
}
Rule: In loops that call JNI functions returning local refs, delete each local ref before the next iteration or you will exhaust the local reference table (default limit: 512 entries).
Exception Handling
JNI exceptions are pending — they don't unwind the C++ stack. You must check for them explicitly.
env->CallVoidMethod(obj, methodId, arg);
if (env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
return;
}
void throwJavaException(JNIEnv* env, const char* className, const char* message) {
jclass exClass = env->FindClass(className);
if (exClass != nullptr) {
env->ThrowNew(exClass, message);
}
}
throwJavaException(env, "java/lang/IllegalArgumentException", "sampleRate must be > 0");
return -1;
Rule: Never let a pending Java exception remain while calling further JNI functions (except ExceptionCheck, ExceptionOccurred, ExceptionDescribe, ExceptionClear). The behavior is undefined.
Calling Java Methods from C++
static jclass g_listenerClass = nullptr;
static jmethodID g_onAudioReady = nullptr;
jclass localClass = env->FindClass("com/example/audio/IAudioListener");
g_listenerClass = static_cast<jclass>(env->NewGlobalRef(localClass));
g_onAudioReady = env->GetMethodID(g_listenerClass, "onAudioReady", "([BI)V");
void notifyAudioReady(JNIEnv* env, jobject listenerObj, jbyteArray data, jint length) {
env->CallVoidMethod(listenerObj, g_onAudioReady, data, length);
}
Method signature string format: (<param-types>)<return-type>
| Type | Signature character |
|---|
int | I |
long | J |
boolean | Z |
byte | B |
void | V |
String | Ljava/lang/String; |
int[] | [I |
Object | Ljava/lang/Object; |
Threading — JNIEnv from Background Threads
JNIEnv* is thread-local. A background native thread has no JNIEnv* by default.
static JavaVM* g_jvm = nullptr;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
g_jvm = vm;
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
void nativeThreadCallback() {
JNIEnv* env = nullptr;
bool attached = false;
jint result = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
if (result == JNI_EDETACHED) {
g_jvm->AttachCurrentThread(&env, nullptr);
attached = true;
}
env->CallVoidMethod(g_listener, g_onAudioReady, ...);
if (attached) {
g_jvm->DetachCurrentThread();
}
}
Rule: Every AttachCurrentThread must be paired with DetachCurrentThread before the thread exits. Failing to detach causes the JVM to crash on thread exit.
Prerequisites
- Android Studio (Flamingo or newer) or AOSP build environment set up.
- Android SDK Platform-Tools installed (
adb on PATH).
- Target device or emulator running Android 11+ (API 30+).
- For AOSP modules:
repo tool, AOSP source synced, lunch target configured.
Step-by-Step Workflows
Step 1: Declare native methods in Java/Kotlin
Add external (Kotlin) or native (Java) methods; load with System.loadLibrary("name").
Step 2: Implement JNI functions in C/C++
Name them Java_<pkg>_<class>_<method>(JNIEnv*, jclass, ...).
Step 3: Map JNI types correctly
Use the JNI Type Mapping table below; convert jstring with GetStringUTFChars / ReleaseStringUTFChars.
Step 4: Manage local references
Delete local refs in loops with env->DeleteLocalRef(); use global refs for long-lived objects.
Step 5: Propagate exceptions
Check env->ExceptionCheck() after Java calls; throw with env->ThrowNew().
Troubleshooting
UnsatisfiedLinkError — library name in System.loadLibrary must match CMakeLists.txt add_library name (without lib prefix or .so suffix).
- JNI crash with no stack trace — build with NDK debug symbols; use
ndk-stack to symbolicate the tombstone.
- Memory leak on local references — local refs are not freed automatically in loops; call
env->DeleteLocalRef() in the loop body.
JNIEnv not valid in a new thread — attach the thread to the JVM with jvm->AttachCurrentThread(); detach before the thread exits.
Pre-Commit Checklist
References