Das Java Native Interface (JNI) ermöglicht Java-Programmen die Interaktion mit nativem Code, meist in C oder C++. Diese Schnittstelle ist besonders dann wichtig, wenn Leistungskritische Operationen oder der Zugriff auf Plattformfunktionen erforderlich sind, die Java selbst nicht direkt bereitstellt. Ein häufiger Anwendungsfall in JNI ist die Arbeit mit Arrays, da Daten häufig in Form von Arrays zwischen Java und nativem Code ausgetauscht werden. Arrays in JNI zu handhaben erfordert jedoch besondere Sorgfalt, da Java-Arrays verwaltet werden und Garbage Collection unterliegen.

Grundlegendes zu Arrays in JNI

In Java gibt es primitive Arrays (z.B. int[], float[]) und Objektarrays (String[], Object[]). JNI unterscheidet diese und stellt für jede Art spezifische Funktionen zur Verfügung. Alle JNI-Arraytypen sind Subtypen von jarray. Zu den wichtigsten Arraytypen gehören:

  • jintArray für int[]
  • jfloatArray für float[]
  • jdoubleArray für double[]
  • jbyteArray für byte[]
  • jcharArray für char[]
  • jbooleanArray für boolean[]
  • jobjectArray für beliebige Objektarrays (Object[])

Arrays in JNI werden über Zeiger auf interne JVM-Strukturen repräsentiert. Das bedeutet, dass man in C/C++ nicht direkt auf den Speicher wie in nativen Arrays zugreifen kann, sondern JNI-Funktionen verwenden muss, um die Daten zu lesen oder zu schreiben.

Zugriff auf Arrayelemente

JNI bietet zwei grundlegende Methoden, um auf Arrayelemente zuzugreifen:

  1. Copy-Methode: Das Array wird kopiert, und Änderungen müssen explizit zurückgeschrieben werden.
  2. Direct-Access-Methode: Es wird ein Zeiger auf die Elemente des Arrays zurückgegeben, der direkten Zugriff ermöglicht. Der Speicher bleibt jedoch weiterhin unter Kontrolle der JVM.

Beispiel für ein int[]

JNIEXPORT void JNICALL Java_MyClass_processArray
  (JNIEnv *env, jobject obj, jintArray array) {

    // Array-Länge ermitteln
    jsize length = (*env)->GetArrayLength(env, array);

    // Zugriff auf die Elemente (Copy)
    jint *elements = (*env)->GetIntArrayElements(env, array, NULL);
    if (elements == NULL) return; // Fehlerbehandlung

    // Verarbeitung
    for (jsize i = 0; i < length; i++) {
        elements[i] *= 2; // Beispiel: alle Werte verdoppeln
    }

    // Änderungen zurückschreiben und Speicher freigeben
    (*env)->ReleaseIntArrayElements(env, array, elements, 0);
}Code-Sprache: PHP (php)

Parameter von ReleaseIntArrayElements:

  • 0: Änderungen übernehmen und Speicher freigeben.
  • JNI_ABORT: Änderungen verwerfen, Speicher freigeben.
  • JNI_COMMIT: Änderungen übernehmen, Speicher aber noch nicht freigeben.

Direktes Arbeiten ohne Kopie

Für sehr große Arrays oder Performance-kritische Anwendungen kann GetPrimitiveArrayCritical verwendet werden, um unnötige Kopien zu vermeiden. Dabei muss jedoch der Zugriff sehr schnell erfolgen, da die JVM währenddessen möglicherweise die Garbage Collection pausiert.

jint *data = (*env)->GetPrimitiveArrayCritical(env, array, NULL);
// Verarbeitung
(*env)->ReleasePrimitiveArrayCritical(env, array, data, 0);Code-Sprache: PHP (php)

Arbeiten mit Objektarrays

Objektarrays (jobjectArray) erfordern besondere Aufmerksamkeit, da die Elemente selbst Objekte sind. Man greift über GetObjectArrayElement auf einzelne Elemente zu und muss diese nach der Nutzung mit DeleteLocalRef freigeben, um den lokalen Referenz-Cache nicht zu überlaufen.

JNIEXPORT void JNICALL Java_MyClass_printStrings
  (JNIEnv *env, jobject obj, jobjectArray array) {

    jsize length = (*env)->GetArrayLength(env, array);

    for (jsize i = 0; i < length; i++) {
        jstring str = (jstring)(*env)->GetObjectArrayElement(env, array, i);

        const char *cstr = (*env)->GetStringUTFChars(env, str, NULL);
        printf("%s\n", cstr);
        (*env)->ReleaseStringUTFChars(env, str, cstr);

        (*env)->DeleteLocalRef(env, str);
    }
}Code-Sprache: PHP (php)

Erstellung von Arrays in JNI

JNI erlaubt es auch, neue Arrays im nativen Code zu erstellen und an Java zurückzugeben:

JNIEXPORT jintArray JNICALL Java_MyClass_createArray
  (JNIEnv *env, jobject obj, jint size) {

    jintArray array = (*env)->NewIntArray(env, size);
    if (array == NULL) return NULL; // Fehler

    jint fill[size];
    for (int i = 0; i < size; i++) fill[i] = i;

    (*env)->SetIntArrayRegion(env, array, 0, size, fill);
    return array;
}Code-Sprache: PHP (php)

Für Objektarrays muss zusätzlich die Klasse des Elements angegeben werden:

jclass cls = (*env)->FindClass(env, "java/lang/String");
jobjectArray array = (*env)->NewObjectArray(env, size, cls, NULL);Code-Sprache: PHP (php)

Wichtige Best Practices

  1. Lokale Referenzen freigeben: JNI speichert lokale Referenzen pro Thread; zu viele Referenzen führen zu einem Speicherüberlauf.
  2. Korrektes Freigeben von Speicher: Bei Get*ArrayElements oder GetPrimitiveArrayCritical muss Release* immer aufgerufen werden.
  3. Performance beachten: Für große Arrays oder intensive Verarbeitung Direct-Access-Methoden bevorzugen.
  4. Thread-Sicherheit: JNI-Aufrufe sollten nur von Threads erfolgen, die korrekt an die JVM gebunden sind.
JNI-FunktionBeschreibung
GetArrayLength(JNIEnv*, jarray)Gibt die Länge eines Arrays zurück. Funktioniert für alle Arraytypen.
GetIntArrayElements(JNIEnv*, jintArray, jboolean*)Liefert einen Zeiger auf die Elemente eines int[]. Änderungen können zurückgeschrieben werden.
ReleaseIntArrayElements(JNIEnv*, jintArray, jint*, jint)Gibt die Elemente frei. Parameter steuern, ob Änderungen übernommen werden (0), verworfen werden (JNI_ABORT) oder übernommen, aber Speicher nicht freigegeben wird (JNI_COMMIT).
GetPrimitiveArrayCritical(JNIEnv*, jarray, jboolean*)Liefert direkten Zugriff auf ein primitives Array ohne Kopie. Schneller, aber die JVM kann nicht garbage-collecten, solange das Array gesperrt ist.
ReleasePrimitiveArrayCritical(JNIEnv*, jarray, void*, jint)Gibt ein mit GetPrimitiveArrayCritical gesperrtes Array frei.
GetObjectArrayElement(JNIEnv*, jobjectArray, jsize)Liefert ein einzelnes Objekt aus einem Objektarray (Object[]).
SetObjectArrayElement(JNIEnv*, jobjectArray, jsize, jobject)Setzt ein Element in einem Objektarray.
NewIntArray(JNIEnv*, jsize)Erstellt ein neues int[] in der JVM.
SetIntArrayRegion(JNIEnv*, jintArray, jsize start, jsize len, const jint* buf)Kopiert einen Block von Werten in ein Array.
NewObjectArray(JNIEnv*, jsize, jclass, jobject initialElement)Erstellt ein neues Objektarray (Object[]) mit optionalem Initialelement.
GetStringUTFChars(JNIEnv*, jstring, jboolean*)Liefert ein UTF-8-C-String-Pendant zu einem Java-String. Muss mit ReleaseStringUTFChars freigegeben werden.
ReleaseStringUTFChars(JNIEnv*, jstring, const char*)Gibt den von GetStringUTFChars erhaltenen Speicher frei.
DeleteLocalRef(JNIEnv*, jobject)Löscht eine lokale Referenz, um den Referenz-Cache der JVM nicht zu überlasten.

Diese Tabelle fasst die wichtigsten Funktionen zusammen und zeigt, welche für primitive Arrays und welche für Objektarrays relevant sind.

Fazit

Die Arbeit mit Arrays in JNI ist zentral für effiziente native Java-Erweiterungen. Während primitive Arrays über spezialisierte Funktionen schnell bearbeitet werden können, erfordern Objektarrays besondere Vorsicht hinsichtlich Referenzmanagement. Durch das Verständnis der Copy- und Direct-Access-Methoden, gepaart mit sauberem Speicher- und Referenzhandling, lassen sich sichere und performante JNI-Module entwickeln. Wer JNI korrekt einsetzt, kann die Vorteile nativer Performance nutzen, ohne die Sicherheit und Stabilität der Java-Laufzeitumgebung zu gefährden.