Advanced AOSP Subsystems
3 min read

JNI Data Passing

Passing Data Between Java and Native Code

A fundamental task in JNI is transferring data efficiently between the Java Virtual Machine and native C/C++ code. Understanding how types map and how memory is handled is crucial for building performant AOSP components.

Primitive Type Mapping

Java primitive types have direct equivalents in JNI, defined in <jni.h>. These are mostly typedefs for standard C/C++ integer types, ensuring predictable sizes across different architectures (32-bit vs. 64-bit).

Java TypeJNI TypeC/C++ EquivalentSize
booleanjbooleanunsigned char8 bits
bytejbytesigned char8 bits
charjcharunsigned short16 bits
shortjshortshort16 bits
intjintint32 bits
longjlonglong long64 bits
floatjfloatfloat32 bits
doublejdoubledouble64 bits

Because primitives are passed by value, there is no memory management overhead when passing them back and forth.

String Passing

Java strings (java.lang.String) are objects containing UTF-16 encoded characters. C/C++ typically uses UTF-8 (char*) or wide characters. JNI provides functions to translate between these formats.

  • GetStringUTFChars: Converts a jstring to a C-style UTF-8 char*. You must call ReleaseStringUTFChars when done.
  • NewStringUTF: Creates a new Java jstring from a C-style UTF-8 char*.
JNIEXPORT void JNICALL Java_com_example_MyClass_processString(JNIEnv *env, jobject thiz, jstring javaString) {
    // Extract C string
    const char *cString = env->GetStringUTFChars(javaString, nullptr);
    if (cString == nullptr) return; // Out of memory
    
    // Use the string (e.g., log it)
    __android_log_print(ANDROID_LOG_INFO, "JNI", "Received: %s", cString);
    
    // MUST release the string to avoid memory leaks
    env->ReleaseStringUTFChars(javaString, cString);
}

Array Passing

Passing arrays requires careful handling. JNI cannot safely pass a raw C pointer to Java, nor can it directly expose Java array memory without coordination with the Garbage Collector.

  • GetIntArrayElements: Retrieves a pointer to the elements of a jintArray. The JVM might pin the array in memory or copy it.
  • ReleaseIntArrayElements: Releases the pointer. You must specify a mode (e.g., 0 to copy changes back and free the buffer, or JNI_ABORT to free the buffer without copying back).
JNIEXPORT jint JNICALL Java_com_example_MyClass_sumArray(JNIEnv *env, jobject thiz, jintArray javaArray) {
    jsize length = env->GetArrayLength(javaArray);
    
    // Get pointer to elements
    jint *elements = env->GetIntArrayElements(javaArray, nullptr);
    if (elements == nullptr) return 0;
    
    jint sum = 0;
    for (int i = 0; i < length; i++) {
        sum += elements[i];
    }
    
    // Release elements (JNI_ABORT means we didn't modify them)
    env->ReleaseIntArrayElements(javaArray, elements, JNI_ABORT);
    return sum;
}

ByteBuffer for Direct Memory Access

When dealing with large volumes of data (e.g., audio buffers, video frames, OpenGL textures), copying arrays via JNI becomes a significant performance bottleneck.

Java's java.nio.ByteBuffer, specifically direct ByteBuffers (ByteBuffer.allocateDirect()), solves this. A direct ByteBuffer is allocated outside the normal Java heap.

Native code can access the raw memory address of a direct ByteBuffer using GetDirectBufferAddress(). This allows zero-copy data transfer between Java and native layers, which is heavily utilized in AOSP multimedia and graphics stacks.

JNIEXPORT void JNICALL Java_com_example_MyClass_processBuffer(JNIEnv *env, jobject thiz, jobject byteBuffer) {
    // Get raw pointer to the buffer's memory
    void* bufferPtr = env->GetDirectBufferAddress(byteBuffer);
    jlong capacity = env->GetDirectBufferCapacity(byteBuffer);
    
    if (bufferPtr == nullptr) {
        // Not a direct buffer!
        return;
    }
    
    // Zero-copy processing on bufferPtr...
}