Java Native Interface (JNI) ist eine mächtige Schnittstelle, die es Java-Anwendungen ermöglicht, direkt mit nativem Code in C oder C++ zu interagieren. Dies ist besonders nützlich, wenn bestimmte Aufgaben effizienter in nativer Sprache umgesetzt werden oder wenn existierende Bibliotheken wiederverwendet werden sollen, die nicht in Java verfügbar sind. Ein häufig auftretender Anwendungsfall ist der Umgang mit List-Strukturen aus Java, die in JNI verarbeitet werden sollen. In diesem Artikel betrachten wir die grundlegenden Prinzipien, typische Fallstricke und praktische Beispiele zur Nutzung von Lists in JNI.

Grundlagen: JNI und Java-Objekte

JNI erlaubt den Zugriff auf Java-Objekte über einen speziellen Zeiger, den sogenannten JNIEnv*. Über diesen Zeiger können native Methoden Java-Objekte erzeugen, Methoden aufrufen oder Felder lesen und schreiben. Dabei ist zu beachten, dass Java-Objekte in JNI als jobject repräsentiert werden. Für spezielle Typen wie String gibt es dedizierte Typen (jstring), für Arrays (jintArray, jobjectArray) und so weiter.

Ein List-Objekt in Java ist jedoch ein Interface (java.util.List) und keine native Struktur. Das bedeutet, dass es in JNI nicht direkt manipuliert werden kann wie ein Array. Stattdessen muss man Methoden des Interfaces über JNI aufrufen.

Zugriff auf Java List in JNI

Angenommen, wir haben eine Java-Methode, die eine List<String> an eine native Methode übergibt:

public class ListExample {
    public native void processList(List<String> items);
}Code-Sprache: PHP (php)

Im C/C++-Code müssen wir zunächst die Klassen- und Methodensignaturen abrufen und Methoden wie size() und get(int index) verwenden, um auf die Listenelemente zuzugreifen:

#include <jni.h>
#include <stdio.h>

JNIEXPORT void JNICALL Java_ListExample_processList(JNIEnv *env, jobject obj, jobject list) {
    jclass listClass = (*env)->GetObjectClass(env, list);

    jmethodID sizeMethod = (*env)->GetMethodID(env, listClass, "size", "()I");
    jmethodID getMethod = (*env)->GetMethodID(env, listClass, "get", "(I)Ljava/lang/Object;");

    jint size = (*env)->CallIntMethod(env, list, sizeMethod);

    for (jint i = 0; i < size; i++) {
        jobject element = (*env)->CallObjectMethod(env, list, getMethod, i);

        const char *str = (*env)->GetStringUTFChars(env, (jstring)element, NULL);
        printf("Element %d: %s\n", i, str);
        (*env)->ReleaseStringUTFChars(env, (jstring)element, str);
        (*env)->DeleteLocalRef(env, element);
    }

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

Hier sind einige zentrale Punkte zu beachten:

  1. Klassen- und Methodenermittlung: Jede Methode des List-Interfaces muss über GetMethodID gefunden werden.
  2. Methodenaufrufe: CallObjectMethod und CallIntMethod dienen zum Aufruf von Methoden und zum Abrufen von Rückgabewerten.
  3. Speicherverwaltung: Lokale Referenzen wie element sollten mit DeleteLocalRef freigegeben werden, um Speicherlecks zu vermeiden.
  4. String-Konvertierung: Java-Strings müssen mit GetStringUTFChars in C-kompatible Strings konvertiert werden und anschließend wieder freigegeben werden.

Listen in JNI erstellen

Nicht nur der Zugriff, sondern auch das Erstellen einer neuen List in JNI ist möglich. In der Regel erzeugt man ein ArrayList-Objekt und fügt Elemente über add hinzu:

JNIEXPORT jobject JNICALL Java_ListExample_createList(JNIEnv *env, jobject obj) {
    jclass arrayListClass = (*env)->FindClass(env, "java/util/ArrayList");
    jmethodID constructor = (*env)->GetMethodID(env, arrayListClass, "<init>", "()V");
    jobject arrayList = (*env)->NewObject(env, arrayListClass, constructor);

    jmethodID addMethod = (*env)->GetMethodID(env, arrayListClass, "add", "(Ljava/lang/Object;)Z");

    jstring str1 = (*env)->NewStringUTF(env, "Hallo");
    jstring str2 = (*env)->NewStringUTF(env, "JNI");

    (*env)->CallBooleanMethod(env, arrayList, addMethod, str1);
    (*env)->CallBooleanMethod(env, arrayList, addMethod, str2);

    (*env)->DeleteLocalRef(env, str1);
    (*env)->DeleteLocalRef(env, str2);
    (*env)->DeleteLocalRef(env, arrayListClass);

    return arrayList;
}Code-Sprache: PHP (php)

Dieser Code zeigt, wie man eine Java-ArrayList in C erstellt, Elemente hinzufügt und sie an Java zurückgibt.

Typische Fallstricke

  1. JNI-Referenzen: JNI unterscheidet zwischen lokalen und globalen Referenzen. Lokale Referenzen existieren nur während des Aufrufs und müssen bei großen Schleifen explizit gelöscht werden.
  2. Typensicherheit: List-Elemente sind Object. Ein falscher Cast kann zu Laufzeitfehlern führen.
  3. Performance: Häufiges Aufrufen von get(i) kann ineffizient sein. Für große Listen empfiehlt sich das Zwischenspeichern in einem Array oder die Übergabe als jobjectArray.
  4. Exception Handling: JNI-Methoden lösen Java-Ausnahmen aus, die in C geprüft werden müssen. Ohne Prüfung kann ein Fehler übersehen werden.

Verschachtelte Listen (List<List<String>>)

In vielen Anwendungen ist es nötig, Listen von Listen zu verarbeiten, z. B. eine Tabelle als List<List<String>>. In JNI werden solche Strukturen als List von List-Objekten dargestellt. Der Zugriff erfolgt rekursiv: Zuerst wird die äußere Liste durchlaufen, dann die innere.

Beispiel:

JNIEXPORT void JNICALL Java_ListExample_processNestedList(JNIEnv *env, jobject obj, jobject nestedList) {
    jclass listClass = (*env)->GetObjectClass(env, nestedList);
    jmethodID sizeMethod = (*env)->GetMethodID(env, listClass, "size", "()I");
    jmethodID getMethod = (*env)->GetMethodID(env, listClass, "get", "(I)Ljava/lang/Object;");

    jint outerSize = (*env)->CallIntMethod(env, nestedList, sizeMethod);

    for (jint i = 0; i < outerSize; i++) {
        jobject innerList = (*env)->CallObjectMethod(env, nestedList, getMethod, i);
        jclass innerListClass = (*env)->GetObjectClass(env, innerList);

        jint innerSize = (*env)->CallIntMethod(env, innerList, sizeMethod);
        for (jint j = 0; j < innerSize; j++) {
            jobject element = (*env)->CallObjectMethod(env, innerList, getMethod, j);
            const char *str = (*env)->GetStringUTFChars(env, (jstring)element, NULL);
            printf("Element [%d][%d]: %s\n", i, j, str);
            (*env)->ReleaseStringUTFChars(env, (jstring)element, str);
            (*env)->DeleteLocalRef(env, element);
        }

        (*env)->DeleteLocalRef(env, innerListClass);
        (*env)->DeleteLocalRef(env, innerList);
    }

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

Hinweise:

  • Jede innere Liste wird wie ein separates List-Objekt behandelt.
  • Lokale Referenzen für innere Listen und deren Elemente müssen gelöscht werden.
  • Bei großen Datenmengen kann dies viele JNI-Methodenaufrufe erzeugen und die Performance belasten.

Performance-Optimierungen

Der Zugriff auf List-Elemente über get(i) kann bei großen Listen ineffizient sein, da jedes get einen Methodenaufruf über JNI bedeutet. Einige Optimierungen:

  1. Konvertierung in Arrays Vor dem Aufruf in JNI kann die Liste in ein Object[] umgewandelt werden (toArray()), um die Anzahl der JNI-Aufrufe zu reduzieren:
List<String> list = ...;
String[] array = list.toArray(new String[0]);
processArray(array);Code-Sprache: PHP (php)
  1. Im JNI-Code kann dann direkt auf das Array zugegriffen werden:
jobjectArray array = ...;
jsize length = (*env)->GetArrayLength(env, array);
for (jsize i = 0; i < length; i++) {
    jstring element = (jstring)(*env)->GetObjectArrayElement(env, array, i);
    // ...
    (*env)->DeleteLocalRef(env, element);
}Code-Sprache: PHP (php)
  1. Minimierung von DeleteLocalRef-Aufrufen Bei sehr großen Listen kann es sinnvoll sein, Referenzen nur periodisch zu löschen (z. B. nach jedem 1000. Element), um die JNI-Limitierung für lokale Referenzen zu umgehen.
  2. Globale Referenzen für wiederverwendete Objekte Objekte, die mehrfach in JNI benötigt werden, können als globale Referenzen erstellt werden (NewGlobalRef) und am Ende freigegeben werden (DeleteGlobalRef), um wiederholte lokale Referenzen zu vermeiden.
  3. Batch-Verarbeitung Statt jede Liste einzeln zu verarbeiten, kann die native Methode mehrere Listen auf einmal empfangen oder große Datenblöcke in einem Array aggregieren.

Erstellung verschachtelter Listen in JNI

Man kann auch verschachtelte Listen in JNI erzeugen. Beispiel: List<List<String>>:

jclass arrayListClass = (*env)->FindClass(env, "java/util/ArrayList");
jmethodID constructor = (*env)->GetMethodID(env, arrayListClass, "<init>", "()V");
jmethodID addMethod = (*env)->GetMethodID(env, arrayListClass, "add", "(Ljava/lang/Object;)Z");

jobject outerList = (*env)->NewObject(env, arrayListClass, constructor);

for (int i = 0; i < 3; i++) {
    jobject innerList = (*env)->NewObject(env, arrayListClass, constructor);
    for (int j = 0; j < 2; j++) {
        char buffer[32];
        sprintf(buffer, "Item %d-%d", i, j);
        jstring str = (*env)->NewStringUTF(env, buffer);
        (*env)->CallBooleanMethod(env, innerList, addMethod, str);
        (*env)->DeleteLocalRef(env, str);
    }
    (*env)->CallBooleanMethod(env, outerList, addMethod, innerList);
    (*env)->DeleteLocalRef(env, innerList);
}

(*env)->DeleteLocalRef(env, arrayListClass);
return outerList;Code-Sprache: PHP (php)

Dies erzeugt eine äußere ArrayList, die mehrere innere Listen enthält, jede wiederum gefüllt mit Strings.

Fazit

Die Arbeit mit Lists in JNI erfordert ein Verständnis der Java-Objektmanipulation aus nativer Sicht. Während der Zugriff auf Listenmethoden wie size() und get() recht direkt ist, erfordert die Speicherverwaltung, Typkonvertierung und Ausnahmebehandlung besondere Sorgfalt. Das Erstellen und Zurückgeben von Listen zeigt die Flexibilität von JNI, erlaubt aber gleichzeitig tiefen Einblick in die Mechanismen von Java-Objekten. Wer diese Prinzipien beherrscht, kann die volle Leistungsfähigkeit nativer Bibliotheken nutzen, ohne auf die Vorteile von Java-Datenstrukturen verzichten zu müssen.