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 Type | JNI Type | C/C++ Equivalent | Size |
|---|---|---|---|
boolean | jboolean | unsigned char | 8 bits |
byte | jbyte | signed char | 8 bits |
char | jchar | unsigned short | 16 bits |
short | jshort | short | 16 bits |
int | jint | int | 32 bits |
long | jlong | long long | 64 bits |
float | jfloat | float | 32 bits |
double | jdouble | double | 64 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 ajstringto a C-style UTF-8char*. You must callReleaseStringUTFCharswhen done.NewStringUTF: Creates a new Javajstringfrom a C-style UTF-8char*.
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 ajintArray. The JVM might pin the array in memory or copy it.ReleaseIntArrayElements: Releases the pointer. You must specify a mode (e.g.,0to copy changes back and free the buffer, orJNI_ABORTto 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...
}