Understanding JNI and how Java/Kotlin code interacts with native C/C++ libraries
The Java Native Interface (JNI) is the bridge that allows Java and Kotlin code to call and be called by native C/C++ code. It’s the fundamental mechanism that enables NDK development on Android.
// Java String to C stringjstring jstr = /* from Java */;const char* cstr = (*env)->GetStringUTFChars(env, jstr, NULL);// Use cstr...(*env)->ReleaseStringUTFChars(env, jstr, cstr);// C string to Java Stringconst char* message = "Hello from native";jstring result = (*env)->NewStringUTF(env, message);return result;// For large strings, use direct buffer accessconst jchar* chars = (*env)->GetStringCritical(env, jstr, NULL);jsize len = (*env)->GetStringLength(env, jstr);// Process chars...(*env)->ReleaseStringCritical(env, jstr, chars);
Array operations
// Access primitive arrayjintArray arr = /* from Java */;jsize len = (*env)->GetArrayLength(env, arr);jint* elements = (*env)->GetIntArrayElements(env, arr, NULL);// Modify elementsfor (int i = 0; i < len; i++) { elements[i] *= 2;}// Commit changes back to Java array (0 = copy back and free)(*env)->ReleaseIntArrayElements(env, arr, elements, 0);// Create new arrayjintArray newArr = (*env)->NewIntArray(env, 10);jint buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};(*env)->SetIntArrayRegion(env, newArr, 0, 10, buffer);
Object operations
// Find classjclass stringClass = (*env)->FindClass(env, "java/lang/String");// Get field ID and access fieldjfieldID fid = (*env)->GetFieldID(env, clazz, "count", "I");jint value = (*env)->GetIntField(env, obj, fid);(*env)->SetIntField(env, obj, fid, value + 1);// Call methodjmethodID mid = (*env)->GetMethodID(env, clazz, "toString", "()Ljava/lang/String;");jstring result = (*env)->CallObjectMethod(env, obj, mid);// Create objectjmethodID constructor = (*env)->GetMethodID(env, clazz, "<init>", "()V");jobject newObj = (*env)->NewObject(env, clazz, constructor);
Exception handling
// Check for exceptionsjstring str = (*env)->NewStringUTF(env, "text");if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); // Print to logcat (*env)->ExceptionClear(env); return NULL;}// Throw exceptionjclass exceptionClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException");(*env)->ThrowNew(env, exceptionClass, "Invalid argument");return NULL; // Must return from native method
JNIEXPORT jobject JNICALLJava_Example_createObject(JNIEnv* env, jobject thiz) { jclass clazz = (*env)->FindClass(env, "java/lang/String"); // clazz is a local reference, automatically freed when method returns jstring str = (*env)->NewStringUTF(env, "hello"); // Can explicitly free to save memory in long methods // (*env)->DeleteLocalRef(env, clazz); return str; // Return value is caller's responsibility}
Local references are freed when the native method returns. Do not store them for later use. The JVM has a limit on local references (typically 512).
// Anti-pattern: JNI overhead dominatesexternal fun add(a: Int, b: Int): Intfun sumArray(arr: IntArray): Int { var sum = 0 for (value in arr) { sum = add(sum, value) // JNI call per element! } return sum}
// Better: Single JNI callexternal fun sumArray(arr: IntArray): Intfun processData(arr: IntArray): Int { return sumArray(arr) // One JNI call, bulk processing}
jbyte* data = (*env)->GetPrimitiveArrayCritical(env, jarray, NULL);if (data != NULL) { // CRITICAL: No JNI calls allowed here! // GC may be disabled, must be very fast process_data(data, length); (*env)->ReleasePrimitiveArrayCritical(env, jarray, data, 0);}
Between GetPrimitiveArrayCritical and ReleasePrimitiveArrayCritical, do not: